@fiscozen/navbar 0.1.11 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,8 @@
1
1
  import { FzNavbarProps } from './types';
2
+ declare function handleMenuButtonClick(): void;
2
3
  declare function __VLS_template(): {
3
4
  attrs: Partial<{}>;
4
- slots: Partial<Record<NonNullable<"notifications" | "user-menu">, (_: {
5
- isHorizontal: boolean;
6
- isVertical: boolean;
7
- isMobile: true;
8
- }) => any>> & {
5
+ slots: {
9
6
  'brand-logo'?(_: {
10
7
  isMobile: false;
11
8
  isHorizontal: boolean;
@@ -26,11 +23,25 @@ declare function __VLS_template(): {
26
23
  isVertical: boolean;
27
24
  isMobile: false;
28
25
  }): any;
26
+ notifications?(_: {
27
+ isHorizontal: boolean;
28
+ isVertical: boolean;
29
+ isMobile: true;
30
+ }): any;
29
31
  'user-menu'?(_: {
30
32
  isHorizontal: boolean;
31
33
  isMobile: false;
32
34
  isVertical: boolean;
33
35
  }): any;
36
+ 'user-menu'?(_: {
37
+ isHorizontal: boolean;
38
+ isMobile: true;
39
+ isVertical: boolean;
40
+ }): any;
41
+ 'menu-button'?(_: {
42
+ isOpen: boolean;
43
+ toggle: typeof handleMenuButtonClick;
44
+ }): any;
34
45
  };
35
46
  refs: {};
36
47
  rootEl: HTMLElement;
@@ -38,11 +49,16 @@ declare function __VLS_template(): {
38
49
  type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
39
50
  declare const __VLS_component: import('vue').DefineComponent<FzNavbarProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
40
51
  "fznavbar:menuButtonClick": () => any;
52
+ "update:isMenuOpen": (value: boolean) => any;
41
53
  }, string, import('vue').PublicProps, Readonly<FzNavbarProps> & Readonly<{
42
54
  "onFznavbar:menuButtonClick"?: (() => any) | undefined;
55
+ "onUpdate:isMenuOpen"?: ((value: boolean) => any) | undefined;
43
56
  }>, {
44
57
  variant: import('./types').FzNavbarVariant;
58
+ position: import('./types').FzNavbarPosition;
45
59
  breakpoints: Partial<Record<import('@fiscozen/style').Breakpoint, `${number}px`>>;
60
+ mobileBreakpoint: number | `${number}px`;
61
+ respectSafeArea: boolean;
46
62
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLElement>;
47
63
  declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
48
64
  export default _default;
@@ -1,20 +1,44 @@
1
1
  import { Breakpoint } from '@fiscozen/style';
2
2
  export type FzNavbarVariant = 'horizontal' | 'vertical';
3
+ export type FzNavbarPosition = 'static' | 'fixed' | 'sticky';
3
4
  interface FzNavbarProps {
4
5
  /**
5
6
  * The main direction of the navbar
6
7
  */
7
- variant: FzNavbarVariant;
8
+ variant?: FzNavbarVariant;
8
9
  /**
9
- * Whether the main navigation menu is open (mobile)
10
+ * Whether the main navigation menu is open (mobile). Supports v-model.
10
11
  */
11
12
  isMenuOpen?: boolean;
12
13
  /**
13
- * Override breakpoint for manage custom size inside navbar
14
+ * @deprecated Use `mobileBreakpoint` instead. When both are passed, `mobileBreakpoint` takes precedence.
15
+ *
16
+ * Override breakpoints for managing custom size inside the navbar.
14
17
  */
15
18
  breakpoints?: Partial<Record<Breakpoint, `${number}px`>>;
19
+ /**
20
+ * Width (in pixels) at and above which the navbar renders its desktop layout.
21
+ * Below this value the compact mobile layout is used.
22
+ *
23
+ * Replaces the global mutation of `@fiscozen/style` breakpoints that consumers
24
+ * used to perform to align with their own desktop threshold.
25
+ */
26
+ mobileBreakpoint?: number | `${number}px`;
27
+ /**
28
+ * CSS positioning strategy applied to the root `<header>`. Replaces the need
29
+ * for consumers to add `class="fixed top-0 left-0"` (or similar) on the call site.
30
+ */
31
+ position?: FzNavbarPosition;
32
+ /**
33
+ * When `true`, the navbar adds `env(safe-area-inset-*)` to its top, left and right padding
34
+ * so it renders correctly on devices with a notch / dynamic island.
35
+ *
36
+ * Defaults to `false` to preserve current visual behavior for existing consumers.
37
+ */
38
+ respectSafeArea?: boolean;
16
39
  }
17
40
  type FzNavbarEmits = {
18
41
  'fznavbar:menuButtonClick': [];
42
+ 'update:isMenuOpen': [value: boolean];
19
43
  };
20
44
  export type { FzNavbarProps, FzNavbarEmits };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiscozen/navbar",
3
- "version": "0.1.11",
3
+ "version": "0.2.1",
4
4
  "description": "Design System Navbar Component",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "license": "ISC",
34
34
  "dependencies": {
35
- "@fiscozen/composables": "1.0.3",
36
35
  "@fiscozen/button": "3.0.1",
36
+ "@fiscozen/composables": "1.0.3",
37
37
  "@fiscozen/style": "0.3.0"
38
38
  },
39
39
  "scripts": {
package/src/FzNavbar.vue CHANGED
@@ -7,29 +7,59 @@ import { useBreakpoints } from '@fiscozen/composables'
7
7
 
8
8
  const props = withDefaults(defineProps<FzNavbarProps>(), {
9
9
  variant: 'horizontal',
10
- breakpoints: undefined
10
+ breakpoints: undefined,
11
+ mobileBreakpoint: undefined,
12
+ position: 'static',
13
+ respectSafeArea: false
11
14
  })
12
15
 
16
+ const emit = defineEmits<FzNavbarEmits>()
17
+
18
+ if (props.breakpoints !== undefined) {
19
+ // eslint-disable-next-line no-console
20
+ console.warn(
21
+ '[FzNavbar] The `breakpoints` prop is deprecated and will be removed in a future major. Use `mobileBreakpoint` instead.'
22
+ )
23
+ }
24
+
13
25
  const computedBreakpoints = computed(() => {
26
+ if (props.mobileBreakpoint !== undefined) {
27
+ const value =
28
+ typeof props.mobileBreakpoint === 'number'
29
+ ? (`${props.mobileBreakpoint}px` as `${number}px`)
30
+ : props.mobileBreakpoint
31
+ return { ...breakpoints, lg: value }
32
+ }
14
33
  return {
15
34
  ...breakpoints,
16
35
  ...(props.breakpoints ?? {})
17
36
  }
18
37
  })
19
38
 
20
- const emit = defineEmits<FzNavbarEmits>()
21
-
22
39
  const { isGreater } = useBreakpoints(computedBreakpoints.value)
23
40
  const isGreaterThanLg = isGreater('lg')
24
41
  const isMobile = computed(() => !isGreaterThanLg.value)
25
- const isVertical = computed(() => Boolean(props.variant === 'vertical'))
26
- const isHorizontal = computed(() => Boolean(props.variant === 'horizontal'))
42
+ const isVertical = computed(() => props.variant === 'vertical')
43
+ const isHorizontal = computed(() => props.variant === 'horizontal')
44
+
45
+ const localMenuOpen = computed<boolean | undefined>({
46
+ get: () => props.isMenuOpen,
47
+ set: (value) => emit('update:isMenuOpen', Boolean(value))
48
+ })
49
+
50
+ function handleMenuButtonClick() {
51
+ localMenuOpen.value = !localMenuOpen.value
52
+ emit('fznavbar:menuButtonClick')
53
+ }
27
54
  </script>
28
55
 
29
56
  <template>
30
57
  <header
31
- class="z-10 flex p-12 shadow"
58
+ class="fz-navbar z-10 m-0 box-border flex items-center border-0 p-12 shadow"
32
59
  :class="{
60
+ 'fz-navbar--fixed': position === 'fixed',
61
+ 'fz-navbar--sticky': position === 'sticky',
62
+ 'fz-navbar--safe-area': respectSafeArea,
33
63
  'justify-between': isMobile,
34
64
  'h-full w-56 flex-col': isVertical && !isMobile,
35
65
  'h-56 w-full': isHorizontal || isMobile
@@ -39,11 +69,14 @@ const isHorizontal = computed(() => Boolean(props.variant === 'horizontal'))
39
69
  <div :class="{ 'mr-32': isHorizontal, 'mb-32': isVertical }">
40
70
  <slot name="brand-logo" :isMobile :isHorizontal :isVertical></slot>
41
71
  </div>
42
- <div class="flex gap-4" :class="{ 'flex-row': isHorizontal, 'flex-col': isVertical }">
72
+ <div
73
+ class="flex items-center gap-4"
74
+ :class="{ 'flex-row': isHorizontal, 'flex-col': isVertical }"
75
+ >
43
76
  <slot name="navigation" :isVertical :isHorizontal :isMobile></slot>
44
77
  </div>
45
78
  <div
46
- class="flex gap-16"
79
+ class="flex items-center gap-16"
47
80
  :class="{ 'ml-auto flex-row': isHorizontal, 'mt-auto flex-col': isVertical }"
48
81
  >
49
82
  <slot name="notifications" :isHorizontal :isVertical :isMobile></slot>
@@ -51,25 +84,93 @@ const isHorizontal = computed(() => Boolean(props.variant === 'horizontal'))
51
84
  </div>
52
85
  </template>
53
86
  <template v-else>
54
- <FzIconButton
55
- :iconName="isMenuOpen ? 'xmark' : 'bars'"
56
- variant="secondary"
57
- tooltip="menu"
58
- @click="emit('fznavbar:menuButtonClick')"
59
- />
87
+ <slot name="menu-button" :isOpen="Boolean(localMenuOpen)" :toggle="handleMenuButtonClick">
88
+ <FzIconButton
89
+ :iconName="localMenuOpen ? 'xmark' : 'bars'"
90
+ variant="secondary"
91
+ tooltip="menu"
92
+ @click="handleMenuButtonClick"
93
+ />
94
+ </slot>
60
95
  <div>
61
96
  <slot name="brand-logo" :isMobile :isHorizontal :isVertical></slot>
62
97
  </div>
63
- <div>
64
- <slot
65
- :name="isHorizontal ? 'notifications' : 'user-menu'"
66
- :isHorizontal
67
- :isVertical
68
- :isMobile
69
- ></slot>
98
+ <div class="flex items-center gap-16">
99
+ <slot name="notifications" :isHorizontal :isVertical :isMobile></slot>
100
+ <slot name="user-menu" :isHorizontal :isMobile :isVertical></slot>
70
101
  </div>
71
102
  </template>
72
103
  </header>
73
104
  </template>
74
105
 
75
- <style></style>
106
+ <style>
107
+ /*
108
+ * CSS custom properties — defaults are pinned to the `@fiscozen/style` pixel
109
+ * spacing tokens that match the Tailwind utility classes used in the template
110
+ * (p-12 = 12px, h-56/w-56 = 56px, mr-32/mb-32 = 32px, gap-16 = 16px). The
111
+ * package's CSS rule `.fz-navbar.{w-56,h-56,...}` has higher specificity than
112
+ * the consumer's generated Tailwind class on its own, so the fallback values
113
+ * must match the design-system pixel scale — otherwise stock-Tailwind rem
114
+ * defaults (14rem/3rem/8rem/4rem) leak through and the navbar renders ~4×
115
+ * larger than intended.
116
+ *
117
+ * Consumers can still override per-instance via inline style or scoped CSS.
118
+ */
119
+
120
+ .fz-navbar {
121
+ z-index: var(--fz-navbar-z-index, 10);
122
+ padding: var(--fz-navbar-padding, 12px);
123
+ box-shadow: var(
124
+ --fz-navbar-shadow,
125
+ 0 1px 3px 0 rgb(0 0 0 / 0.1),
126
+ 0 1px 2px -1px rgb(0 0 0 / 0.1)
127
+ );
128
+ background: var(--fz-navbar-bg, transparent);
129
+ }
130
+
131
+ .fz-navbar.h-56 {
132
+ height: var(--fz-navbar-height, 56px);
133
+ }
134
+
135
+ .fz-navbar.w-56 {
136
+ width: var(--fz-navbar-width, 56px);
137
+ }
138
+
139
+ .fz-navbar > .mr-32 {
140
+ margin-right: var(--fz-navbar-brand-gap, 32px);
141
+ }
142
+
143
+ .fz-navbar > .mb-32 {
144
+ margin-bottom: var(--fz-navbar-brand-gap, 32px);
145
+ }
146
+
147
+ .fz-navbar > .gap-16 {
148
+ gap: var(--fz-navbar-actions-gap, 16px);
149
+ }
150
+
151
+ .fz-navbar--fixed {
152
+ position: fixed;
153
+ top: 0;
154
+ left: 0;
155
+ }
156
+
157
+ .fz-navbar--sticky {
158
+ position: sticky;
159
+ top: 0;
160
+ }
161
+
162
+ .fz-navbar--safe-area {
163
+ padding-top: calc(
164
+ var(--fz-navbar-padding, 12px) +
165
+ max(env(safe-area-inset-top, 0px), var(--safe-area-inset-top, 0px))
166
+ );
167
+ padding-left: calc(
168
+ var(--fz-navbar-padding, 12px) +
169
+ max(env(safe-area-inset-left, 0px), var(--safe-area-inset-left, 0px))
170
+ );
171
+ padding-right: calc(
172
+ var(--fz-navbar-padding, 12px) +
173
+ max(env(safe-area-inset-right, 0px), var(--safe-area-inset-right, 0px))
174
+ );
175
+ }
176
+ </style>
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
2
  import { mount, VueWrapper } from '@vue/test-utils'
3
3
  import FzNavbar from '../FzNavbar.vue'
4
4
  import { FzIconButton } from '@fiscozen/button'
5
+ import { breakpoints } from '@fiscozen/style'
5
6
 
6
7
  const navigation = `
7
8
  <div class="link">one</div>
@@ -847,4 +848,277 @@ describe('FzNavbar', () => {
847
848
  expect(wrapper.html()).toMatchSnapshot()
848
849
  })
849
850
  })
851
+
852
+ // ============================================
853
+ // v0.2.0 — ADDITIVE API
854
+ // ============================================
855
+ describe('mobileBreakpoint prop', () => {
856
+ it('should accept a numeric value', () => {
857
+ wrapper = mount(FzNavbar, {
858
+ props: {
859
+ variant: 'horizontal',
860
+ mobileBreakpoint: 1200
861
+ }
862
+ })
863
+ expect(wrapper.exists()).toBe(true)
864
+ })
865
+
866
+ it('should accept a pixel-string value', () => {
867
+ wrapper = mount(FzNavbar, {
868
+ props: {
869
+ variant: 'horizontal',
870
+ mobileBreakpoint: '1200px'
871
+ }
872
+ })
873
+ expect(wrapper.exists()).toBe(true)
874
+ })
875
+
876
+ it('should not mutate the imported @fiscozen/style breakpoints object', () => {
877
+ const before = { ...breakpoints }
878
+ mount(FzNavbar, {
879
+ props: {
880
+ variant: 'horizontal',
881
+ mobileBreakpoint: 1400
882
+ }
883
+ })
884
+ expect(breakpoints).toEqual(before)
885
+ })
886
+
887
+ it('should take precedence over the legacy `breakpoints` prop when both are provided', () => {
888
+ wrapper = mount(FzNavbar, {
889
+ props: {
890
+ variant: 'horizontal',
891
+ breakpoints: { lg: '900px' },
892
+ mobileBreakpoint: 1500
893
+ }
894
+ })
895
+ expect(wrapper.exists()).toBe(true)
896
+ })
897
+ })
898
+
899
+ describe('position prop', () => {
900
+ it('should not add positioning classes by default', () => {
901
+ wrapper = mount(FzNavbar, {
902
+ props: { variant: 'horizontal' }
903
+ })
904
+ const header = wrapper.find('header')
905
+ expect(header.classes()).not.toContain('fz-navbar--fixed')
906
+ expect(header.classes()).not.toContain('fz-navbar--sticky')
907
+ })
908
+
909
+ it('should add the fixed-position class when position="fixed"', () => {
910
+ wrapper = mount(FzNavbar, {
911
+ props: { variant: 'horizontal', position: 'fixed' }
912
+ })
913
+ expect(wrapper.find('header').classes()).toContain('fz-navbar--fixed')
914
+ })
915
+
916
+ it('should add the sticky-position class when position="sticky"', () => {
917
+ wrapper = mount(FzNavbar, {
918
+ props: { variant: 'horizontal', position: 'sticky' }
919
+ })
920
+ expect(wrapper.find('header').classes()).toContain('fz-navbar--sticky')
921
+ })
922
+ })
923
+
924
+ describe('respectSafeArea prop', () => {
925
+ it('should not add the safe-area class by default', () => {
926
+ wrapper = mount(FzNavbar, {
927
+ props: { variant: 'horizontal' }
928
+ })
929
+ expect(wrapper.find('header').classes()).not.toContain('fz-navbar--safe-area')
930
+ })
931
+
932
+ it('should add the safe-area class when respectSafeArea is true', () => {
933
+ wrapper = mount(FzNavbar, {
934
+ props: { variant: 'horizontal', respectSafeArea: true }
935
+ })
936
+ expect(wrapper.find('header').classes()).toContain('fz-navbar--safe-area')
937
+ })
938
+ })
939
+
940
+ describe('v-model:isMenuOpen', () => {
941
+ it('should emit update:isMenuOpen when the default menu button is clicked', async () => {
942
+ window.innerWidth = 1023
943
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
944
+
945
+ wrapper = mount(FzNavbar, {
946
+ props: { variant: 'horizontal', isMenuOpen: false }
947
+ })
948
+ await wrapper.vm.$nextTick()
949
+ const menuButton = wrapper.findComponent(FzIconButton)
950
+ const buttonElement = menuButton.find('button')
951
+ await (buttonElement.exists() ? buttonElement : menuButton).trigger('click')
952
+
953
+ const updates = wrapper.emitted('update:isMenuOpen')
954
+ expect(updates).toBeTruthy()
955
+ expect(updates![0]).toEqual([true])
956
+ })
957
+
958
+ it('should emit `false` when toggling from an open state', async () => {
959
+ window.innerWidth = 1023
960
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
961
+
962
+ wrapper = mount(FzNavbar, {
963
+ props: { variant: 'horizontal', isMenuOpen: true }
964
+ })
965
+ await wrapper.vm.$nextTick()
966
+ const menuButton = wrapper.findComponent(FzIconButton)
967
+ const buttonElement = menuButton.find('button')
968
+ await (buttonElement.exists() ? buttonElement : menuButton).trigger('click')
969
+
970
+ const updates = wrapper.emitted('update:isMenuOpen')
971
+ expect(updates).toBeTruthy()
972
+ expect(updates![0]).toEqual([false])
973
+ })
974
+
975
+ it('should still emit fznavbar:menuButtonClick alongside update:isMenuOpen', async () => {
976
+ window.innerWidth = 1023
977
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
978
+
979
+ wrapper = mount(FzNavbar, {
980
+ props: { variant: 'horizontal', isMenuOpen: false }
981
+ })
982
+ await wrapper.vm.$nextTick()
983
+ const menuButton = wrapper.findComponent(FzIconButton)
984
+ const buttonElement = menuButton.find('button')
985
+ await (buttonElement.exists() ? buttonElement : menuButton).trigger('click')
986
+
987
+ expect(wrapper.emitted('fznavbar:menuButtonClick')).toHaveLength(1)
988
+ expect(wrapper.emitted('update:isMenuOpen')).toHaveLength(1)
989
+ })
990
+ })
991
+
992
+ describe('menu-button slot', () => {
993
+ it('should render the default FzIconButton when the slot is not provided', async () => {
994
+ window.innerWidth = 1023
995
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
996
+
997
+ wrapper = mount(FzNavbar, {
998
+ props: { variant: 'horizontal' }
999
+ })
1000
+ await wrapper.vm.$nextTick()
1001
+ expect(wrapper.findComponent(FzIconButton).exists()).toBe(true)
1002
+ })
1003
+
1004
+ it('should replace the default button when the menu-button slot is provided', async () => {
1005
+ window.innerWidth = 1023
1006
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
1007
+
1008
+ wrapper = mount(FzNavbar, {
1009
+ props: { variant: 'horizontal', isMenuOpen: false },
1010
+ slots: {
1011
+ 'menu-button': `
1012
+ <template #menu-button="{ isOpen, toggle }">
1013
+ <button id="custom-menu" :data-is-open="isOpen" @click="toggle">menu</button>
1014
+ </template>
1015
+ `
1016
+ }
1017
+ })
1018
+ await wrapper.vm.$nextTick()
1019
+ expect(wrapper.find('#custom-menu').exists()).toBe(true)
1020
+ expect(wrapper.findComponent(FzIconButton).exists()).toBe(false)
1021
+ })
1022
+
1023
+ it('should expose isOpen and toggle in the slot scope', async () => {
1024
+ window.innerWidth = 1023
1025
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
1026
+
1027
+ wrapper = mount(FzNavbar, {
1028
+ props: { variant: 'horizontal', isMenuOpen: true },
1029
+ slots: {
1030
+ 'menu-button': `
1031
+ <template #menu-button="{ isOpen, toggle }">
1032
+ <button id="custom-menu" :data-is-open="isOpen" @click="toggle">menu</button>
1033
+ </template>
1034
+ `
1035
+ }
1036
+ })
1037
+ await wrapper.vm.$nextTick()
1038
+ const customButton = wrapper.find('#custom-menu')
1039
+ expect(customButton.attributes('data-is-open')).toBe('true')
1040
+
1041
+ await customButton.trigger('click')
1042
+ const updates = wrapper.emitted('update:isMenuOpen')
1043
+ expect(updates).toBeTruthy()
1044
+ expect(updates![0]).toEqual([false])
1045
+ })
1046
+ })
1047
+
1048
+ describe('mobile slot rendering (no XOR)', () => {
1049
+ it('should render both notifications and user-menu slots on mobile when both are filled', async () => {
1050
+ window.innerWidth = 1023
1051
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
1052
+
1053
+ wrapper = mount(FzNavbar, {
1054
+ props: { variant: 'horizontal' },
1055
+ slots: {
1056
+ notifications: '<div id="notification"></div>',
1057
+ 'user-menu': '<div id="avatar"></div>'
1058
+ }
1059
+ })
1060
+ await wrapper.vm.$nextTick()
1061
+ expect(wrapper.find('#notification').exists()).toBe(true)
1062
+ expect(wrapper.find('#avatar').exists()).toBe(true)
1063
+ })
1064
+
1065
+ it('should render only notifications on mobile when user-menu is empty', async () => {
1066
+ window.innerWidth = 1023
1067
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
1068
+
1069
+ wrapper = mount(FzNavbar, {
1070
+ props: { variant: 'horizontal' },
1071
+ slots: {
1072
+ notifications: '<div id="notification"></div>'
1073
+ }
1074
+ })
1075
+ await wrapper.vm.$nextTick()
1076
+ expect(wrapper.find('#notification').exists()).toBe(true)
1077
+ expect(wrapper.find('#avatar').exists()).toBe(false)
1078
+ })
1079
+
1080
+ it('should render only user-menu on mobile when notifications is empty', async () => {
1081
+ window.innerWidth = 1023
1082
+ vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1023)
1083
+
1084
+ wrapper = mount(FzNavbar, {
1085
+ props: { variant: 'vertical' },
1086
+ slots: {
1087
+ 'user-menu': '<div id="avatar"></div>'
1088
+ }
1089
+ })
1090
+ await wrapper.vm.$nextTick()
1091
+ expect(wrapper.find('#avatar').exists()).toBe(true)
1092
+ expect(wrapper.find('#notification').exists()).toBe(false)
1093
+ })
1094
+ })
1095
+
1096
+ describe('breakpoints prop deprecation warning', () => {
1097
+ it('should emit a console.warn in dev when the deprecated `breakpoints` prop is used', () => {
1098
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
1099
+
1100
+ mount(FzNavbar, {
1101
+ props: {
1102
+ variant: 'horizontal',
1103
+ breakpoints: { lg: '1200px' }
1104
+ }
1105
+ })
1106
+
1107
+ expect(warnSpy).toHaveBeenCalled()
1108
+ expect(warnSpy.mock.calls[0][0]).toMatch(/breakpoints.*deprecated/i)
1109
+
1110
+ warnSpy.mockRestore()
1111
+ })
1112
+
1113
+ it('should not warn when the deprecated prop is not used', () => {
1114
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
1115
+
1116
+ mount(FzNavbar, {
1117
+ props: { variant: 'horizontal' }
1118
+ })
1119
+
1120
+ expect(warnSpy).not.toHaveBeenCalled()
1121
+ warnSpy.mockRestore()
1122
+ })
1123
+ })
850
1124
  })