@eturnity/eturnity_reusable_components 9.28.0 → 9.28.2

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.
@@ -10,7 +10,11 @@ export default {
10
10
  docs: {
11
11
  description: {
12
12
  component:
13
- 'Low-level modal shell: backdrop, centered container, optional close control, loading state, and a default slot for any body content. Parent handles layout and actions inside the slot. Emits `on-close` when the close button is used or Escape is pressed (if `closeOnEscape` is true).',
13
+ 'Low-level modal shell: backdrop, centered container, optional ' +
14
+ 'close control, loading state, and a default slot for any body ' +
15
+ 'content. Parent handles layout and actions inside the slot. Emits ' +
16
+ '`on-close` when the close button is used or Escape is pressed ' +
17
+ '(if `closeOnEscape` is true).',
14
18
  },
15
19
  },
16
20
  },
@@ -21,7 +25,8 @@ export default {
21
25
  },
22
26
  isLoading: {
23
27
  control: 'boolean',
24
- description: 'Shows a large spinner and hides slot content and close button',
28
+ description:
29
+ 'Shows a large spinner and hides slot content and close button',
25
30
  },
26
31
  hideClose: {
27
32
  control: 'boolean',
@@ -55,7 +60,8 @@ export default {
55
60
  },
56
61
  addPaddingTop: {
57
62
  control: 'boolean',
58
- description: 'Adds top padding to the overlay (e.g. clear a fixed header)',
63
+ description:
64
+ 'Adds top padding to the overlay (e.g. clear a fixed header)',
59
65
  },
60
66
  disableDefaultMediaQuery: {
61
67
  control: 'boolean',
@@ -131,7 +137,8 @@ export const WithoutCloseButton = {
131
137
  parameters: {
132
138
  docs: {
133
139
  description: {
134
- story: 'Use when the user must complete an action in the slot (e.g. explicit Save/Cancel inside the body).',
140
+ story:
141
+ 'Use when the user must complete an action in the slot (e.g. explicit Save/Cancel inside the body).',
135
142
  },
136
143
  },
137
144
  },
@@ -156,7 +163,8 @@ export const InsetNotFullScreen = {
156
163
  parameters: {
157
164
  docs: {
158
165
  description: {
159
- story: 'Overlay does not cover the full viewport; useful when embedding preview areas need visible context.',
166
+ story:
167
+ 'Overlay does not cover the full viewport; useful when embedding preview areas need visible context.',
160
168
  },
161
169
  },
162
170
  },
@@ -219,7 +227,8 @@ export const WithTopPadding = {
219
227
  parameters: {
220
228
  docs: {
221
229
  description: {
222
- story: 'Extra top padding on the overlay when a fixed app bar occupies the top of the screen.',
230
+ story:
231
+ 'Extra top padding on the overlay when a fixed app bar occupies the top of the screen.',
223
232
  },
224
233
  },
225
234
  },
@@ -285,7 +294,8 @@ export const InteractiveOpenAndClose = {
285
294
  parameters: {
286
295
  docs: {
287
296
  description: {
288
- story: 'Minimal host page with local `isOpen` state to demonstrate open/close behaviour.',
297
+ story:
298
+ 'Minimal host page with local `isOpen` state to demonstrate open/close behaviour.',
289
299
  },
290
300
  },
291
301
  },
@@ -0,0 +1,95 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import NavigationTabs from '@/components/navigationTabs'
4
+
5
+ // `theme` and `$gettext` are provided globally by jest.setup.js, so specs do
6
+ // not need to re-provide them. See the canonical example at
7
+ // src/components/buttons/closeButton/closeButton.spec.js.
8
+ //
9
+ // The component renders only styled-components (no data-test-id hooks) and must
10
+ // not be changed, so the tab elements are selected structurally: the root
11
+ // wrapper holds one Tab div per item followed by a trailing BottomLine div.
12
+
13
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
14
+ // need to mock this due to how jest handles import.meta
15
+ fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
16
+ }))
17
+
18
+ const tabsData = [
19
+ { id: 1, slug: 'overview', label: 'Overview' },
20
+ {
21
+ id: 2,
22
+ slug: 'billing',
23
+ label: 'Billing',
24
+ labelInfoText: 'Invoices and payment methods',
25
+ labelInfoAlign: 'left',
26
+ },
27
+ { id: 3, slug: 'users', label: 'Users', isDisabled: true },
28
+ ]
29
+
30
+ describe('NavigationTabs', () => {
31
+ const mountTabs = (props = {}) =>
32
+ mount(NavigationTabs, {
33
+ props: {
34
+ tabsData,
35
+ activeTab: 'billing',
36
+ tabKey: 'slug',
37
+ tabLabel: 'label',
38
+ ...props,
39
+ },
40
+ })
41
+
42
+ // The Tab divs are every direct child of the root except the trailing
43
+ // BottomLine div.
44
+ const getTabs = (wrapper) => {
45
+ const tabEls = Array.from(wrapper.element.children).slice(0, -1)
46
+ return wrapper.findAll('div').filter((d) => tabEls.includes(d.element))
47
+ }
48
+
49
+ it('renders one tab per item with its label', () => {
50
+ const wrapper = mountTabs()
51
+ const tabs = getTabs(wrapper)
52
+ expect(tabs).toHaveLength(tabsData.length)
53
+ expect(wrapper.text()).toContain('Overview')
54
+ expect(wrapper.text()).toContain('Billing')
55
+ expect(wrapper.text()).toContain('Users')
56
+ })
57
+
58
+ it('emits tab-click with the tabKey value when isIndexKey is false', async () => {
59
+ const wrapper = mountTabs()
60
+ const tabs = getTabs(wrapper)
61
+
62
+ await tabs[0].trigger('click')
63
+ await tabs[2].trigger('click')
64
+
65
+ const emitted = wrapper.emitted('tab-click')
66
+ expect(emitted).toHaveLength(2)
67
+ expect(emitted[0]).toEqual(['overview'])
68
+ expect(emitted[1]).toEqual(['users'])
69
+ })
70
+
71
+ it('emits tab-click with the index when isIndexKey is true', async () => {
72
+ const wrapper = mountTabs({ activeTab: 0, isIndexKey: true })
73
+ const tabs = getTabs(wrapper)
74
+
75
+ await tabs[2].trigger('click')
76
+
77
+ const emitted = wrapper.emitted('tab-click')
78
+ expect(emitted).toHaveLength(1)
79
+ expect(emitted[0]).toEqual([2])
80
+ })
81
+
82
+ it('styles the active tab differently from an inactive tab', () => {
83
+ const wrapper = mountTabs()
84
+ const tabs = getTabs(wrapper)
85
+ // activeTab is 'billing' (index 1); its generated class must differ from an
86
+ // inactive tab's class.
87
+ expect(tabs[1].classes()).not.toEqual(tabs[0].classes())
88
+ })
89
+
90
+ it('renders info text only for tabs that provide labelInfoText', () => {
91
+ const wrapper = mountTabs()
92
+ const infoTexts = wrapper.findAll('[data-test-id="infoText_container"]')
93
+ expect(infoTexts).toHaveLength(1)
94
+ })
95
+ })
@@ -0,0 +1,109 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import SideMenu from '@/components/sideMenu'
3
+ import Spinner from '@/components/spinner'
4
+
5
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
6
+ // need to mock this due to how jest handles import.meta
7
+ fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
8
+ }))
9
+
10
+ const tabsData = [
11
+ { key: 'dashboard', label: 'Dashboard', icon: 'house' },
12
+ { key: 'projects', label: 'Projects', icon: 'house' },
13
+ {
14
+ key: 'settings',
15
+ label: 'Settings',
16
+ icon: 'gear',
17
+ children: [
18
+ { key: 'general', label: 'General' },
19
+ { key: 'billing', label: 'Billing' },
20
+ ],
21
+ },
22
+ ]
23
+
24
+ const mountMenu = (props = {}) =>
25
+ mount(SideMenu, {
26
+ props: { tabsData, activeTab: 'dashboard', ...props },
27
+ })
28
+
29
+ const item = (wrapper, key) =>
30
+ wrapper.find(`[data-id="sub_menu_settings_${key}"]`)
31
+
32
+ describe('sideMenu/index.vue', () => {
33
+ it('shows a spinner and no menu items while tabsData is empty', () => {
34
+ const wrapper = mount(SideMenu, {
35
+ props: { tabsData: [], activeTab: 'dashboard' },
36
+ })
37
+
38
+ expect(wrapper.findComponent(Spinner).exists()).toBe(true)
39
+ expect(item(wrapper, 'dashboard').exists()).toBe(false)
40
+ })
41
+
42
+ it('renders a menu item with its label for each flat tab', () => {
43
+ const wrapper = mountMenu()
44
+
45
+ expect(item(wrapper, 'dashboard').exists()).toBe(true)
46
+ expect(item(wrapper, 'dashboard').text()).toContain('Dashboard')
47
+ expect(item(wrapper, 'projects').text()).toContain('Projects')
48
+ })
49
+
50
+ it('marks the active tab with data-active="true"', () => {
51
+ const wrapper = mountMenu({ activeTab: 'projects' })
52
+
53
+ expect(item(wrapper, 'projects').attributes('data-active')).toBe('true')
54
+ expect(item(wrapper, 'dashboard').attributes('data-active')).toBe('false')
55
+ })
56
+
57
+ it('emits tab-click with the activeKey when a flat item is clicked', async () => {
58
+ const wrapper = mountMenu()
59
+
60
+ await item(wrapper, 'projects').trigger('click')
61
+
62
+ expect(wrapper.emitted('tab-click')).toHaveLength(1)
63
+ expect(wrapper.emitted('tab-click')[0]).toEqual([{ activeKey: 'projects' }])
64
+ })
65
+
66
+ it('hides nested children until the parent is expanded', async () => {
67
+ const wrapper = mountMenu()
68
+
69
+ expect(item(wrapper, 'general').exists()).toBe(false)
70
+
71
+ await item(wrapper, 'settings').trigger('click')
72
+ expect(item(wrapper, 'general').exists()).toBe(true)
73
+ expect(item(wrapper, 'billing').exists()).toBe(true)
74
+
75
+ // clicking the parent again collapses it
76
+ await item(wrapper, 'settings').trigger('click')
77
+ expect(item(wrapper, 'general').exists()).toBe(false)
78
+ })
79
+
80
+ it('emits tab-click with activeKey and parentKey when a child is clicked', async () => {
81
+ const wrapper = mountMenu()
82
+
83
+ await item(wrapper, 'settings').trigger('click')
84
+ await item(wrapper, 'general').trigger('click')
85
+
86
+ expect(wrapper.emitted('tab-click')[0]).toEqual([
87
+ { activeKey: 'general', parentKey: 'settings' },
88
+ ])
89
+ })
90
+
91
+ it('renders the logout button and app version, and emits on-logout on click', async () => {
92
+ const wrapper = mountMenu({ hasLogout: true, appVersion: 'v1.2.3' })
93
+
94
+ const logout = wrapper.find('[data-id="button_settings_logout"]')
95
+ expect(logout.exists()).toBe(true)
96
+ expect(wrapper.text()).toContain('v1.2.3')
97
+
98
+ await logout.trigger('click')
99
+ expect(wrapper.emitted('on-logout')).toHaveLength(1)
100
+ })
101
+
102
+ it('hides the logout section when hasLogout is false', () => {
103
+ const wrapper = mountMenu({ hasLogout: false })
104
+
105
+ expect(wrapper.find('[data-id="button_settings_logout"]').exists()).toBe(
106
+ false
107
+ )
108
+ })
109
+ })
@@ -7,6 +7,19 @@ const flatMenu = [
7
7
  { key: 'settings', label: 'Settings', icon: 'House' },
8
8
  ]
9
9
 
10
+ const nestedMenu = [
11
+ { key: 'dashboard', label: 'Dashboard', icon: 'House' },
12
+ {
13
+ key: 'settings',
14
+ label: 'Settings',
15
+ icon: 'House',
16
+ children: [
17
+ { key: 'general', label: 'General' },
18
+ { key: 'billing', label: 'Billing' },
19
+ ],
20
+ },
21
+ ]
22
+
10
23
  export default {
11
24
  title: 'Components/Navigation/SideMenu',
12
25
  component: SideMenu,
@@ -51,3 +64,33 @@ SecondActive.args = {
51
64
  ...FlatItems.args,
52
65
  activeTab: 'projects',
53
66
  }
67
+
68
+ // A collapsible parent item — click "Settings" to expand its children.
69
+ export const NestedItems = Template.bind({})
70
+ NestedItems.args = {
71
+ tabsData: nestedMenu,
72
+ activeTab: 'general',
73
+ activeParentTab: 'settings',
74
+ hasLogout: false,
75
+ appVersion: 'Storybook',
76
+ }
77
+
78
+ // Bottom section with the logout button and the app version.
79
+ export const WithLogout = Template.bind({})
80
+ WithLogout.args = {
81
+ tabsData: flatMenu,
82
+ activeTab: 'dashboard',
83
+ activeParentTab: null,
84
+ hasLogout: true,
85
+ appVersion: 'v1.2.3',
86
+ }
87
+
88
+ // While tabsData is empty the menu shows a loading spinner.
89
+ export const Loading = Template.bind({})
90
+ Loading.args = {
91
+ tabsData: [],
92
+ activeTab: 'dashboard',
93
+ activeParentTab: null,
94
+ hasLogout: false,
95
+ appVersion: 'Storybook',
96
+ }