@eturnity/eturnity_reusable_components 9.28.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_reusable_components",
3
- "version": "9.28.1",
3
+ "version": "9.28.2",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -1,77 +1,116 @@
1
+ import { ref } from 'vue'
1
2
  import Toggle from './index.vue'
3
+ import theme from '@/assets/theme'
4
+
5
+ // import Toggle from "@eturnity/eturnity_reusable_components/src/components/inputs/toggle"
6
+ // To use:
7
+ // <toggle
8
+ // @on-toggle-change="onInputChange($event)"
9
+ // :isChecked="toggleValue" // Boolean
10
+ // label="My Label Text"
11
+ // toggleColor="red"
12
+ // size="small" // small, medium
13
+ // backgroundColor="blue"
14
+ // labelAlign="right"
15
+ // fontColor="black"
16
+ // :disabled="true"
17
+ // infoTextMessage="My info message"
18
+ // />
2
19
 
3
20
  export default {
4
21
  title: 'Components/Inputs/Toggle',
5
22
  component: Toggle,
6
- // argTypes: {},
23
+ tags: ['autodocs'],
24
+ parameters: {
25
+ layout: 'centered',
26
+ },
27
+ argTypes: {
28
+ isChecked: { control: 'boolean' },
29
+ label: { control: 'text' },
30
+ size: { control: 'select', options: ['small', 'medium'] },
31
+ labelAlign: { control: 'select', options: ['left', 'right'] },
32
+ toggleColor: { control: 'color' },
33
+ backgroundColor: { control: 'color' },
34
+ fontColor: { control: 'color' },
35
+ disabled: { control: 'boolean' },
36
+ isRequiredLabel: { control: 'boolean' },
37
+ infoTextMessage: { control: 'text' },
38
+ },
7
39
  }
8
40
 
9
- const Template = (args, { argTypes }) => ({
10
- // Components used in your story `template` are defined in the `components` object
41
+ const Template = (args) => ({
11
42
  components: { Toggle },
12
- // The story's `args` need to be mapped into the template through the `setup()` method
13
- props: Object.keys(argTypes),
14
- template: '<toggle v-bind="$props" />',
15
-
16
- // import Toggle from "@eturnity/eturnity_reusable_components/src/components/inputs/toggle"
17
- //To use:
18
- // <toggle
19
- // @on-toggle-change="onInputChange($event)"
20
- // :isChecked="toggleValue" // Boolean
21
- // label="My Label Text"
22
- // toggleColor="red"
23
- // size="small" // small, medium
24
- // backgroundColor="blue"
25
- // labelAlign="right"
26
- // fontColor="black"
27
- // :disabled="true"
28
- // />
43
+ setup() {
44
+ const isChecked = ref(args.isChecked)
45
+ return { args, isChecked }
46
+ },
47
+ template: `
48
+ <div data-test-id="toggle-story" style="padding: 16px; min-width: 240px;">
49
+ <Toggle
50
+ v-bind="args"
51
+ :isChecked="isChecked"
52
+ @on-toggle-change="isChecked = $event"
53
+ />
54
+ <p style="margin-top: 12px; font-size: 12px; color: #666;">isChecked: {{ isChecked }}</p>
55
+ </div>
56
+ `,
57
+ provide: {
58
+ theme,
59
+ },
29
60
  })
30
61
 
31
62
  export const Default = Template.bind({})
32
63
  Default.args = {
33
64
  isChecked: true,
34
- size: 'small', // "small" or "medium"
65
+ size: 'small',
35
66
  disabled: false,
36
67
  }
37
68
 
38
69
  export const Medium = Template.bind({})
39
70
  Medium.args = {
40
- isChecked: true,
41
- size: 'medium', // "small" or "medium"
42
- disabled: false,
71
+ ...Default.args,
72
+ size: 'medium',
43
73
  }
44
74
 
45
75
  export const CustomColor = Template.bind({})
46
76
  CustomColor.args = {
47
- isChecked: true,
48
- size: 'small', // "small" or "medium"
49
- disabled: false,
77
+ ...Default.args,
50
78
  toggleColor: 'red',
51
79
  backgroundColor: 'blue',
52
80
  }
53
81
 
54
82
  export const LabelLeft = Template.bind({})
55
83
  LabelLeft.args = {
56
- isChecked: true,
57
- size: 'small', // "small" or "medium"
84
+ ...Default.args,
58
85
  label: 'My Label Text',
59
- disabled: false,
60
86
  labelAlign: 'left',
61
87
  }
62
88
 
63
89
  export const LabelRight = Template.bind({})
64
90
  LabelRight.args = {
65
- isChecked: true,
66
- size: 'small', // "small" or "medium"
91
+ ...Default.args,
67
92
  label: 'My Label Text',
68
- disabled: false,
69
93
  labelAlign: 'right',
70
94
  }
71
95
 
96
+ export const WithInfoText = Template.bind({})
97
+ WithInfoText.args = {
98
+ ...Default.args,
99
+ label: 'My Label Text',
100
+ infoTextMessage: 'Applies to this location only.',
101
+ }
102
+
103
+ export const Required = Template.bind({})
104
+ Required.args = {
105
+ ...Default.args,
106
+ label: 'My Label Text',
107
+ labelAlign: 'left',
108
+ isRequiredLabel: true,
109
+ }
110
+
72
111
  export const Disabled = Template.bind({})
73
112
  Disabled.args = {
113
+ ...Default.args,
74
114
  isChecked: false,
75
- size: 'small', // "small" or "medium"
76
115
  disabled: true,
77
116
  }
@@ -1,102 +1,141 @@
1
1
  /* eslint-disable */
2
2
  import { mount } from '@vue/test-utils'
3
3
  import RCToggle from '@/components/inputs/toggle'
4
- import theme from '@/assets/theme'
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.
5
8
 
6
9
  jest.mock('@/components/icon/iconCache.mjs', () => ({
7
10
  // need to mock this due to how jest handles import.meta
8
11
  fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
9
12
  }))
10
13
 
11
- describe('RCToggle.vue', () => {
12
- it('toggle is rendered and emits correct payload on change', async () => {
13
- const wrapper = mount(RCToggle, {
14
- props: { isChecked: true, label: 'Test label', disabled: false },
15
- global: {
16
- provide: {
17
- theme,
18
- },
19
- },
20
- })
21
- const toggleWrapper = wrapper.find('[data-test-id="toggle_wrapper"]')
14
+ describe('RCToggle', () => {
15
+ const mountToggle = (props = {}) =>
16
+ mount(RCToggle, { props: { isChecked: false, ...props } })
22
17
 
23
- // check that the element exists
18
+ it('renders the toggle and reflects the checked state', () => {
19
+ const wrapper = mountToggle({ isChecked: true })
20
+ const toggleWrapper = wrapper.find('[data-test-id="toggle_wrapper"]')
24
21
  expect(toggleWrapper.exists()).toBe(true)
25
22
  expect(toggleWrapper.attributes('checked')).toBe('true')
23
+ })
24
+
25
+ it('reflects the unchecked state', () => {
26
+ const wrapper = mountToggle({ isChecked: false })
27
+ const toggleWrapper = wrapper.find('[data-test-id="toggle_wrapper"]')
28
+ expect(toggleWrapper.attributes('checked')).toBe('false')
29
+ })
26
30
 
27
- // Log attributes to see what is rendered (commented out just for reference)
28
- // console.log('attributes', toggleWrapper.attributes())
29
- // console.log('All of the Toggles' attributes', wrapper.vm)
31
+ it('emits on-toggle-change with the negated value when clicked', async () => {
32
+ const wrapper = mountToggle({ isChecked: true })
33
+ await wrapper.find('[data-test-id="page_wrapper"]').trigger('click')
30
34
 
31
- // Test the label
32
- expect(wrapper.text()).toContain('Test label')
35
+ const emitted = wrapper.emitted('on-toggle-change')
36
+ expect(emitted).toHaveLength(1)
37
+ expect(emitted[0]).toEqual([false])
38
+ })
33
39
 
34
- // Test the click
40
+ it('emits on-toggle-change when the space key is pressed', async () => {
41
+ const wrapper = mountToggle({ isChecked: false })
42
+ await wrapper
43
+ .find('[data-test-id="toggle_wrapper"]')
44
+ .trigger('keydown.space')
45
+
46
+ const emitted = wrapper.emitted('on-toggle-change')
47
+ expect(emitted).toHaveLength(1)
48
+ expect(emitted[0]).toEqual([true])
49
+ })
50
+
51
+ it('does not emit when disabled and clicked', async () => {
52
+ const wrapper = mountToggle({ isChecked: false, disabled: true })
53
+ await wrapper.find('[data-test-id="page_wrapper"]').trigger('click')
54
+
55
+ expect(wrapper.emitted('on-toggle-change')).toBeUndefined()
56
+ })
57
+
58
+ it('toggles back and forth across successive clicks', async () => {
59
+ const wrapper = mountToggle({ isChecked: false })
35
60
  const pageWrapper = wrapper.find('[data-test-id="page_wrapper"]')
61
+
36
62
  await pageWrapper.trigger('click')
37
- expect(wrapper.emitted('on-toggle-change')).toBeTruthy()
38
- const emittedEvent = wrapper.emitted('on-toggle-change')
39
- // To inspect emitted events:
40
- // console.log('Emitted events', wrapper.emitted())
41
- expect(emittedEvent).toHaveLength(1) // Check if the event was emitted exactly once
42
- // Check the payload of the event
43
- expect(emittedEvent[0]).toEqual([false])
44
- }),
45
- it('toggle disabled does not emit anything', async () => {
46
- const wrapper = mount(RCToggle, {
47
- props: { isChecked: false, disabled: true },
48
- global: {
49
- provide: {
50
- theme,
51
- },
52
- },
53
- })
54
- const toggleWrapper = wrapper.find('[data-test-id="toggle_wrapper"]')
55
- expect(toggleWrapper.attributes('checked')).toBe('false')
56
-
57
- // Test the click
58
- const pageWrapper = wrapper.find('[data-test-id="page_wrapper"]')
59
- await pageWrapper.trigger('click')
60
-
61
- // Inspect emitted events
62
- const emittedEvents = wrapper.emitted('on-toggle-change')
63
-
64
- // Verify that no events were emitted
65
- expect(emittedEvents).toBeUndefined() // No event should be emitted
66
- }),
67
- it('should toggle back and forth on multiple clicks', async () => {
68
- const wrapper = mount(RCToggle, {
69
- props: { isChecked: false, disabled: false },
70
- global: {
71
- provide: {
72
- theme,
73
- },
74
- },
63
+ await wrapper.setProps({ isChecked: true })
64
+ await pageWrapper.trigger('click')
65
+ await wrapper.setProps({ isChecked: false })
66
+ await pageWrapper.trigger('click')
67
+
68
+ const emitted = wrapper.emitted('on-toggle-change')
69
+ expect(emitted).toHaveLength(3)
70
+ expect(emitted[0]).toEqual([true])
71
+ expect(emitted[1]).toEqual([false])
72
+ expect(emitted[2]).toEqual([true])
73
+ })
74
+
75
+ it('renders no label container when no label is provided', () => {
76
+ const wrapper = mountToggle()
77
+ expect(wrapper.find('[data-test-id="label_left_container"]').exists()).toBe(
78
+ false
79
+ )
80
+ expect(wrapper.find('[data-test-id="label_right_container"]').exists()).toBe(
81
+ false
82
+ )
83
+ })
84
+
85
+ it('renders the label on the right by default', () => {
86
+ const wrapper = mountToggle({ label: 'My Label' })
87
+ expect(wrapper.find('[data-test-id="label_right_container"]').exists()).toBe(
88
+ true
89
+ )
90
+ expect(wrapper.find('[data-test-id="label_left_container"]').exists()).toBe(
91
+ false
92
+ )
93
+ expect(wrapper.text()).toContain('My Label')
94
+ })
95
+
96
+ it('renders the label on the left when labelAlign is "left"', () => {
97
+ const wrapper = mountToggle({ label: 'My Label', labelAlign: 'left' })
98
+ expect(wrapper.find('[data-test-id="label_left_container"]').exists()).toBe(
99
+ true
100
+ )
101
+ expect(wrapper.find('[data-test-id="label_right_container"]').exists()).toBe(
102
+ false
103
+ )
104
+ })
105
+
106
+ it('renders the info text only when infoTextMessage is provided', () => {
107
+ expect(
108
+ mountToggle({ label: 'My Label', labelAlign: 'left' })
109
+ .find('[data-test-id="infoText_container"]')
110
+ .exists()
111
+ ).toBe(false)
112
+
113
+ expect(
114
+ mountToggle({
115
+ label: 'My Label',
116
+ labelAlign: 'left',
117
+ infoTextMessage: 'Some info',
75
118
  })
76
- const toggleWrapper = wrapper.find('[data-test-id="toggle_wrapper"]')
77
- // Initial state
78
- expect(toggleWrapper.attributes('checked')).toBe('false')
79
-
80
- // Trigger click and check events
81
- const pageWrapper = wrapper.find('[data-test-id="page_wrapper"]')
82
- await pageWrapper.trigger('click')
83
- let emittedEvent = wrapper.emitted('on-toggle-change')
84
- expect(emittedEvent).toBeTruthy() // Ensure that the event was emitted
85
- expect(emittedEvent).toHaveLength(1) // Ensure that only one event was emitted
86
- expect(emittedEvent[0]).toEqual([true]) // Check the payload of the first click
87
- await wrapper.setProps({ isChecked: true }) // manually update the props
88
-
89
- // Trigger click again and check events
90
- await pageWrapper.trigger('click')
91
- emittedEvent = wrapper.emitted('on-toggle-change')
92
- expect(emittedEvent).toHaveLength(2) // Ensure that the event count has increased
93
- expect(emittedEvent[1]).toEqual([false]) // Check the payload of the second click
94
- await wrapper.setProps({ isChecked: false }) // manually update the props
95
-
96
- // Trigger click again and check events
97
- await pageWrapper.trigger('click')
98
- emittedEvent = wrapper.emitted('on-toggle-change')
99
- expect(emittedEvent).toHaveLength(3) // Ensure that the event count has increased
100
- expect(emittedEvent[2]).toEqual([true]) // Check the payload of the third click
119
+ .find('[data-test-id="infoText_container"]')
120
+ .exists()
121
+ ).toBe(true)
122
+ })
123
+
124
+ it('renders the required star only when isRequiredLabel is true', () => {
125
+ const withStar = mountToggle({
126
+ label: 'My Label',
127
+ labelAlign: 'left',
128
+ isRequiredLabel: true,
101
129
  })
130
+ expect(withStar.text()).toContain('*')
131
+
132
+ const withoutStar = mountToggle({ label: 'My Label', labelAlign: 'left' })
133
+ expect(withoutStar.text()).not.toContain('*')
134
+ })
135
+
136
+ it('exposes dataId and dataQaId as data attributes', () => {
137
+ const wrapper = mountToggle({ dataId: 'my_toggle', dataQaId: 'qa_toggle' })
138
+ expect(wrapper.find('[data-id="my_toggle"]').exists()).toBe(true)
139
+ expect(wrapper.find('[data-qa-id="qa_toggle"]').exists()).toBe(true)
140
+ })
102
141
  })
@@ -0,0 +1,55 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import Label from '@/components/label'
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
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
10
+ // need to mock this due to how jest handles import.meta
11
+ fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
12
+ }))
13
+
14
+ describe('Label', () => {
15
+ const mountLabel = (props = {}, slot = 'My label') =>
16
+ mount(Label, { props, slots: { default: slot } })
17
+
18
+ it('renders the default slot content', () => {
19
+ const wrapper = mountLabel({}, 'Project name')
20
+ expect(wrapper.text()).toContain('Project name')
21
+ })
22
+
23
+ it('renders the required star only when isRequiredLabel is true', () => {
24
+ expect(mountLabel({ isRequiredLabel: true }).text()).toContain('*')
25
+ expect(mountLabel({ isRequiredLabel: false }).text()).not.toContain('*')
26
+ })
27
+
28
+ it('renders the optional label only when labelOptional is true', () => {
29
+ expect(mountLabel({ labelOptional: true }).text()).toContain('(optional)')
30
+ expect(mountLabel({ labelOptional: false }).text()).not.toContain(
31
+ '(optional)'
32
+ )
33
+ })
34
+
35
+ it('renders the info text only when infoTextMessage is provided', () => {
36
+ expect(
37
+ mountLabel().find('[data-test-id="infoText_container"]').exists()
38
+ ).toBe(false)
39
+
40
+ expect(
41
+ mountLabel({ infoTextMessage: 'Some info' })
42
+ .find('[data-test-id="infoText_container"]')
43
+ .exists()
44
+ ).toBe(true)
45
+ })
46
+
47
+ it('renders both the required star and the optional label together', () => {
48
+ const text = mountLabel({
49
+ isRequiredLabel: true,
50
+ labelOptional: true,
51
+ }).text()
52
+ expect(text).toContain('*')
53
+ expect(text).toContain('(optional)')
54
+ })
55
+ })
@@ -16,6 +16,7 @@ export default {
16
16
  labelOptional: { control: 'boolean' },
17
17
  infoTextMessage: { control: 'text' },
18
18
  infoTextAlign: { control: 'select', options: ['left', 'right'] },
19
+ appTheme: { control: 'select', options: ['light', 'dark'] },
19
20
  },
20
21
  }
21
22
 
@@ -67,3 +68,38 @@ HorizontalLayout.args = {
67
68
  slotLabel: 'Budget (CHF)',
68
69
  labelAlign: 'horizontal',
69
70
  }
71
+
72
+ export const CustomFontStyle = Template.bind({})
73
+ CustomFontStyle.args = {
74
+ ...Default.args,
75
+ slotLabel: 'Section heading',
76
+ fontSize: '20px',
77
+ labelFontColor: 'green',
78
+ }
79
+
80
+ const DarkTemplate = (args) => ({
81
+ components: { LabelText },
82
+ setup() {
83
+ return { args }
84
+ },
85
+ template: `
86
+ <div
87
+ data-test-id="label-story"
88
+ style="padding: 16px; max-width: 420px; background: #1f1f1f;"
89
+ >
90
+ <LabelText v-bind="args">
91
+ {{ args.slotLabel }}
92
+ </LabelText>
93
+ </div>
94
+ `,
95
+ provide: {
96
+ theme,
97
+ },
98
+ })
99
+
100
+ export const DarkTheme = DarkTemplate.bind({})
101
+ DarkTheme.args = {
102
+ ...Default.args,
103
+ slotLabel: 'Project name',
104
+ appTheme: 'dark',
105
+ }
@@ -0,0 +1,46 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import MarkerItem from '@/components/markerItem'
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
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
10
+ // need to mock this due to how jest handles import.meta
11
+ fetchIcon: jest.fn(() => Promise.resolve('mocked-icon-url.svg')),
12
+ }))
13
+
14
+ describe('MarkerItem', () => {
15
+ const mountMarker = (props = {}) =>
16
+ mount(MarkerItem, { props: { label: 'Marker', ...props } })
17
+
18
+ it('renders the label text', () => {
19
+ const wrapper = mountMarker({ label: 'Highlights' })
20
+ expect(wrapper.text()).toContain('Highlights')
21
+ })
22
+
23
+ it('exposes the label as the span title attribute', () => {
24
+ const wrapper = mountMarker({ label: 'Highlights' })
25
+ expect(wrapper.find('span').attributes('title')).toBe('Highlights')
26
+ })
27
+
28
+ it('renders an icon only when iconName is provided', () => {
29
+ expect(
30
+ mountMarker({ iconName: 'House' })
31
+ .find('[data-test-id="icon_wrapper"]')
32
+ .exists()
33
+ ).toBe(true)
34
+
35
+ expect(
36
+ mountMarker().find('[data-test-id="icon_wrapper"]').exists()
37
+ ).toBe(false)
38
+ })
39
+
40
+ it('passes the iconName through to the Icon component', () => {
41
+ const wrapper = mountMarker({ iconName: 'House' })
42
+ expect(wrapper.findComponent({ name: 'EturnityIcon' }).exists()).toBe(true)
43
+ // EturnityIcon forwards `name` to the inner Icon, which declares it as a prop
44
+ expect(wrapper.findComponent({ name: 'Icon' }).props('name')).toBe('House')
45
+ })
46
+ })
@@ -44,3 +44,10 @@ TextOnly.args = {
44
44
  label: 'No icon variant',
45
45
  cursor: 'default',
46
46
  }
47
+
48
+ export const CustomColor = Template.bind({})
49
+ CustomColor.args = {
50
+ label: 'On hold',
51
+ backgroundColor: '#ab5348',
52
+ cursor: 'default',
53
+ }
@@ -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
+ })