@eturnity/eturnity_reusable_components 9.28.1 → 9.28.3

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.
@@ -0,0 +1,193 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import TableDropdown from './index.vue'
4
+ import SearchInput from '@/components/inputs/searchInput'
5
+ import InputText from '@/components/inputs/inputText'
6
+ import MainButton from '@/components/buttons/mainButton'
7
+ import Spinner from '@/components/spinner'
8
+
9
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
10
+ fetchIcon: jest.fn(() => Promise.resolve('')),
11
+ }))
12
+
13
+ // Jest config maps .svg but not .png; stub the raster asset the component imports.
14
+ jest.mock('@/assets/icons/file_icon.png', () => 'file-icon-mock', {
15
+ virtual: true,
16
+ })
17
+
18
+ beforeAll(() => {
19
+ global.ResizeObserver = jest.fn().mockImplementation(() => ({
20
+ observe: jest.fn(),
21
+ unobserve: jest.fn(),
22
+ disconnect: jest.fn(),
23
+ }))
24
+ })
25
+
26
+ const mountDropdown = (props = {}) =>
27
+ mount(TableDropdown, {
28
+ props: {
29
+ colSpan: 2,
30
+ tableItems: [{ type: 'text', value: 'Selected Item' }],
31
+ isOpen: false,
32
+ optionItems: [
33
+ { display_name: 'Item 1', company_item_number: '123' },
34
+ { display_name: 'Item 2', company_item_number: '456' },
35
+ ],
36
+ optionsDisplay: ['display_name', 'company_item_number'],
37
+ ...props,
38
+ },
39
+ })
40
+
41
+ const findByText = (wrapper, selector, text) =>
42
+ wrapper.findAll(selector).find((el) => el.text() === text)
43
+
44
+ describe('TableDropdown', () => {
45
+ describe('rendering table items', () => {
46
+ it('renders a plain text item value', () => {
47
+ expect(mountDropdown().text()).toContain('Selected Item')
48
+ })
49
+
50
+ it('renders the "Use template..." button for an empty template item', () => {
51
+ const wrapper = mountDropdown({
52
+ tableItems: [{ type: 'template', value: '', row: { id: 1 } }],
53
+ })
54
+ expect(wrapper.findComponent(MainButton).props('text')).toBe(
55
+ 'Use template...'
56
+ )
57
+ })
58
+
59
+ it('renders a template link with the value for a filled template item', () => {
60
+ const wrapper = mountDropdown({
61
+ tableItems: [{ type: 'template', value: 'My Template', row: { id: 2 } }],
62
+ })
63
+ expect(wrapper.text()).toContain('My Template')
64
+ expect(wrapper.findComponent(MainButton).exists()).toBe(false)
65
+ })
66
+
67
+ it('renders the no-template placeholder text', () => {
68
+ const wrapper = mountDropdown({
69
+ tableItems: [{ type: 'no-template' }],
70
+ })
71
+ expect(wrapper.text()).toContain('No main component template')
72
+ })
73
+
74
+ it('renders an editable input when customInputDisabled is false', () => {
75
+ const wrapper = mountDropdown({
76
+ tableItems: [{ type: 'input', value: 'abc' }],
77
+ customInputDisabled: false,
78
+ })
79
+ expect(wrapper.findComponent(InputText).exists()).toBe(true)
80
+ })
81
+
82
+ it('renders a read-only placeholder when customInputDisabled is true', () => {
83
+ const wrapper = mountDropdown({
84
+ tableItems: [{ type: 'input', value: 'abc' }],
85
+ customInputDisabled: true,
86
+ })
87
+ expect(wrapper.findComponent(InputText).exists()).toBe(false)
88
+ expect(wrapper.find('.input-placeholder').text()).toContain('abc')
89
+ })
90
+
91
+ it('applies rowDataId as the data-id on the matching row', () => {
92
+ const wrapper = mountDropdown({
93
+ tableItems: [{ type: 'input', value: 'x', inputType: 'price' }],
94
+ rowDataId: [{ inputType: 'price', dataId: 'row_price' }],
95
+ })
96
+ expect(wrapper.find('[data-id="row_price"]').exists()).toBe(true)
97
+ })
98
+ })
99
+
100
+ describe('open state', () => {
101
+ it('does not render the options container when closed', () => {
102
+ const wrapper = mountDropdown({ isOpen: false })
103
+ expect(wrapper.findComponent(SearchInput).exists()).toBe(false)
104
+ })
105
+
106
+ it('renders the search input and options when open', () => {
107
+ const wrapper = mountDropdown({ isOpen: true })
108
+ expect(wrapper.findComponent(SearchInput).exists()).toBe(true)
109
+ expect(wrapper.text()).toContain('Item 1')
110
+ expect(wrapper.text()).toContain('Item 2')
111
+ })
112
+
113
+ it('shows the spinner while options are loading', () => {
114
+ const wrapper = mountDropdown({ isOpen: true, optionsLoading: true })
115
+ expect(wrapper.findComponent(Spinner).exists()).toBe(true)
116
+ })
117
+
118
+ it('shows the default empty text when there are no options', () => {
119
+ const wrapper = mountDropdown({ isOpen: true, optionItems: [] })
120
+ expect(wrapper.text()).toContain('No components found.')
121
+ })
122
+
123
+ it('shows a custom empty text when provided', () => {
124
+ const wrapper = mountDropdown({
125
+ isOpen: true,
126
+ optionItems: [],
127
+ emptyText: 'Nothing here',
128
+ })
129
+ expect(wrapper.text()).toContain('Nothing here')
130
+ })
131
+ })
132
+
133
+ describe('interactions', () => {
134
+ it('emits toggle-dropdown-open with close:true when an open row is clicked', async () => {
135
+ const wrapper = mountDropdown({ isOpen: true })
136
+ await wrapper.trigger('click')
137
+ expect(wrapper.emitted('toggle-dropdown-open')).toEqual([
138
+ [{ close: true }],
139
+ ])
140
+ })
141
+
142
+ it('does not emit toggle when the row is disabled', async () => {
143
+ const wrapper = mountDropdown({ isOpen: false, disabled: true })
144
+ await wrapper.trigger('click')
145
+ expect(wrapper.emitted('toggle-dropdown-open')).toBeUndefined()
146
+ })
147
+
148
+ it('emits dropdown-search when the search input changes', async () => {
149
+ const wrapper = mountDropdown({ isOpen: true })
150
+ await wrapper.findComponent(SearchInput).vm.$emit('on-change', 'panel')
151
+ expect(wrapper.emitted('dropdown-search').at(-1)).toEqual(['panel'])
152
+ })
153
+
154
+ it('emits item-selected with the option when an option is clicked', async () => {
155
+ const wrapper = mountDropdown({ isOpen: true })
156
+ await findByText(wrapper, 'span', 'Item 1').trigger('click')
157
+ expect(wrapper.emitted('item-selected')[0][0]).toMatchObject({
158
+ display_name: 'Item 1',
159
+ })
160
+ })
161
+
162
+ it('emits on-template-click with the row when the template button is clicked', async () => {
163
+ const wrapper = mountDropdown({
164
+ isOpen: true,
165
+ tableItems: [{ type: 'template', value: '', row: { id: 7 } }],
166
+ })
167
+ await wrapper.findComponent(MainButton).trigger('click')
168
+ expect(wrapper.emitted('on-template-click')).toEqual([[{ id: 7 }]])
169
+ })
170
+
171
+ it('emits on-selected-template-click with the row when the template link is clicked', async () => {
172
+ const wrapper = mountDropdown({
173
+ isOpen: true,
174
+ tableItems: [{ type: 'template', value: 'T1', row: { id: 8 } }],
175
+ })
176
+ // Click the innermost value div so the event bubbles to the link's handler.
177
+ const links = wrapper.findAll('div').filter((d) => d.text() === 'T1')
178
+ await links.at(-1).trigger('click')
179
+ expect(wrapper.emitted('on-selected-template-click')).toEqual([
180
+ [{ id: 8 }],
181
+ ])
182
+ })
183
+
184
+ it('emits custom-input-change when the editable input changes', async () => {
185
+ const wrapper = mountDropdown({
186
+ tableItems: [{ type: 'input', value: 'abc' }],
187
+ customInputDisabled: false,
188
+ })
189
+ await wrapper.findComponent(InputText).vm.$emit('input-change', 'xyz')
190
+ expect(wrapper.emitted('custom-input-change')).toEqual([['xyz']])
191
+ })
192
+ })
193
+ })
@@ -0,0 +1,101 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import MainTable from './index.vue'
4
+ import Spinner from '@/components/spinner'
5
+
6
+ beforeAll(() => {
7
+ global.ResizeObserver = jest.fn().mockImplementation(() => ({
8
+ observe: jest.fn(),
9
+ unobserve: jest.fn(),
10
+ disconnect: jest.fn(),
11
+ }))
12
+ })
13
+
14
+ const SLOT = `
15
+ <tbody>
16
+ <tr>
17
+ <td data-test-id="r1c1">A</td>
18
+ <td data-test-id="r1c2">B</td>
19
+ </tr>
20
+ <tr>
21
+ <td data-test-id="r2c1">C</td>
22
+ <td data-test-id="r2c2">D</td>
23
+ </tr>
24
+ </tbody>
25
+ `
26
+
27
+ const mountTable = (props = {}, options = {}) =>
28
+ mount(MainTable, {
29
+ props,
30
+ slots: { default: SLOT },
31
+ ...options,
32
+ })
33
+
34
+ describe('MainTable', () => {
35
+ describe('title', () => {
36
+ it('renders the title when titleText is provided', () => {
37
+ expect(mountTable({ titleText: 'projects' }).text()).toContain('projects')
38
+ })
39
+
40
+ it('does not render a title by default', () => {
41
+ const wrapper = mountTable()
42
+ expect(wrapper.text()).not.toContain('projects')
43
+ })
44
+ })
45
+
46
+ describe('loading', () => {
47
+ it('shows the spinner and hides the table while loading', () => {
48
+ const wrapper = mountTable({ isLoading: true })
49
+ expect(wrapper.findComponent(Spinner).exists()).toBe(true)
50
+ expect(wrapper.find('table').exists()).toBe(false)
51
+ })
52
+
53
+ it('renders the table with slot content when not loading', () => {
54
+ const wrapper = mountTable()
55
+ expect(wrapper.findComponent(Spinner).exists()).toBe(false)
56
+ expect(wrapper.find('table').exists()).toBe(true)
57
+ expect(wrapper.find('[data-test-id="r1c1"]').text()).toBe('A')
58
+ })
59
+ })
60
+
61
+ describe('column hover (hasAimHover)', () => {
62
+ it('highlights the whole column on mouseover and clears it on mouseleave', async () => {
63
+ const wrapper = mountTable(
64
+ { hasAimHover: true },
65
+ { attachTo: document.body }
66
+ )
67
+
68
+ await wrapper.find('[data-test-id="r1c1"]').trigger('mouseover')
69
+ expect(
70
+ wrapper.find('[data-test-id="r1c1"]').classes()
71
+ ).toContain('hovered-column')
72
+ expect(
73
+ wrapper.find('[data-test-id="r2c1"]').classes()
74
+ ).toContain('hovered-column')
75
+ expect(
76
+ wrapper.find('[data-test-id="r1c2"]').classes()
77
+ ).not.toContain('hovered-column')
78
+
79
+ await wrapper.find('table').trigger('mouseleave')
80
+ expect(
81
+ wrapper.find('[data-test-id="r2c1"]').classes()
82
+ ).not.toContain('hovered-column')
83
+
84
+ wrapper.unmount()
85
+ })
86
+
87
+ it('does not highlight columns when hasAimHover is false', async () => {
88
+ const wrapper = mountTable(
89
+ { hasAimHover: false },
90
+ { attachTo: document.body }
91
+ )
92
+
93
+ await wrapper.find('[data-test-id="r1c1"]').trigger('mouseover')
94
+ expect(
95
+ wrapper.find('[data-test-id="r2c1"]').classes()
96
+ ).not.toContain('hovered-column')
97
+
98
+ wrapper.unmount()
99
+ })
100
+ })
101
+ })
@@ -0,0 +1,121 @@
1
+ /* eslint-disable */
2
+ import { mount, DOMWrapper } from '@vue/test-utils'
3
+ import ThreeDots from './index.vue'
4
+
5
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
6
+ fetchIcon: jest.fn(() => Promise.resolve('')),
7
+ }))
8
+
9
+ const options = [
10
+ { value: 'edit', name: 'Edit' },
11
+ { value: 'del', name: 'Delete', disabled: true },
12
+ {
13
+ value: 'more',
14
+ name: 'More',
15
+ children: [
16
+ { value: 'c1', name: 'Child 1' },
17
+ { value: 'c2', name: 'Child 2' },
18
+ ],
19
+ },
20
+ ]
21
+
22
+ // The dropdown is teleported to <body>, so query it there rather than in the wrapper.
23
+ const inBody = (selector) => document.body.querySelector(selector)
24
+ const optionEl = (value) =>
25
+ inBody(`[data-id="three_dots_option_item_0_${value}"]`)
26
+ const click = (el) => new DOMWrapper(el).trigger('click')
27
+
28
+ let wrapper
29
+ const mountDots = (props = {}) => {
30
+ wrapper = mount(ThreeDots, {
31
+ props: { options, ...props },
32
+ attachTo: document.body,
33
+ })
34
+ return wrapper
35
+ }
36
+
37
+ afterEach(() => {
38
+ wrapper?.unmount()
39
+ wrapper = null
40
+ })
41
+
42
+ describe('ThreeDots', () => {
43
+ it('exposes dataId and dataQaId on the trigger', () => {
44
+ const w = mountDots({ dataId: 'row_actions', dataQaId: 'qa_actions' })
45
+ expect(w.find('[data-id="row_actions"]').exists()).toBe(true)
46
+ expect(w.find('[data-qa-id="qa_actions"]').exists()).toBe(true)
47
+ })
48
+
49
+ it('does not render the dropdown until the trigger is clicked', async () => {
50
+ mountDots()
51
+ expect(optionEl('edit')).toBeNull()
52
+ })
53
+
54
+ it('opens the dropdown and lists the options on click', async () => {
55
+ const w = mountDots()
56
+ await w.trigger('click')
57
+
58
+ expect(optionEl('edit')).not.toBeNull()
59
+ expect(optionEl('del')).not.toBeNull()
60
+ expect(optionEl('more')).not.toBeNull()
61
+ expect(document.body.textContent).toContain('Edit')
62
+ })
63
+
64
+ it('shows the spinner and no options while loading', async () => {
65
+ const w = mountDots({ isLoading: true })
66
+ await w.trigger('click')
67
+
68
+ expect(inBody('[data-test-id="spinner_container"]')).not.toBeNull()
69
+ expect(optionEl('edit')).toBeNull()
70
+ })
71
+
72
+ it('emits on-select and closes when a plain option is clicked', async () => {
73
+ const w = mountDots()
74
+ await w.trigger('click')
75
+
76
+ await click(optionEl('edit'))
77
+
78
+ expect(w.emitted('on-select')).toHaveLength(1)
79
+ expect(w.emitted('on-select')[0][0]).toMatchObject({ value: 'edit' })
80
+ expect(w.emitted('on-select')[0][0].name).toBe('Edit')
81
+ expect(optionEl('edit')).toBeNull()
82
+ })
83
+
84
+ it('emits on-click (not on-select) and stays open for a disabled option', async () => {
85
+ const w = mountDots()
86
+ await w.trigger('click')
87
+
88
+ await click(optionEl('del'))
89
+
90
+ expect(w.emitted('on-click')[0][0]).toMatchObject({ value: 'del' })
91
+ expect(w.emitted('on-select')).toBeUndefined()
92
+ expect(optionEl('del')).not.toBeNull()
93
+ })
94
+
95
+ it('emits on-click and stays open for an option with children', async () => {
96
+ const w = mountDots()
97
+ await w.trigger('click')
98
+
99
+ await click(optionEl('more'))
100
+
101
+ expect(w.emitted('on-click')[0][0]).toMatchObject({ value: 'more' })
102
+ expect(w.emitted('on-select')).toBeUndefined()
103
+ expect(optionEl('more')).not.toBeNull()
104
+ })
105
+
106
+ it('reveals children on hover and emits on-select when a child is clicked', async () => {
107
+ const w = mountDots()
108
+ await w.trigger('click')
109
+
110
+ await new DOMWrapper(optionEl('more')).trigger('mouseover')
111
+ expect(document.body.textContent).toContain('Child 1')
112
+
113
+ const child = new DOMWrapper(document.body)
114
+ .findAll('div')
115
+ .find((d) => d.text() === 'Child 1')
116
+ await child.trigger('click')
117
+
118
+ expect(w.emitted('on-select')).toHaveLength(1)
119
+ expect(w.emitted('on-select')[0][0]).toMatchObject({ value: 'c1' })
120
+ })
121
+ })
@@ -7,6 +7,25 @@ const basicOptions = [
7
7
  { value: 'delete', name: 'Delete' },
8
8
  ]
9
9
 
10
+ const nestedOptions = [
11
+ { value: 'edit', name: 'Edit' },
12
+ {
13
+ value: 'move',
14
+ name: 'Move to',
15
+ children: [
16
+ { value: 'folder_a', name: 'Folder A' },
17
+ { value: 'folder_b', name: 'Folder B' },
18
+ ],
19
+ },
20
+ { value: 'delete', name: 'Delete' },
21
+ ]
22
+
23
+ const withDisabledOptions = [
24
+ { value: 'edit', name: 'Edit' },
25
+ { value: 'duplicate', name: 'Duplicate', disabled: true },
26
+ { value: 'delete', name: 'Delete' },
27
+ ]
28
+
10
29
  export default {
11
30
  title: 'Components/ThreeDots/ThreeDots',
12
31
  component: ThreeDots,
@@ -57,3 +76,33 @@ Loading.args = {
57
76
  ...Default.args,
58
77
  isLoading: true,
59
78
  }
79
+
80
+ export const WithNestedChildren = Template.bind({})
81
+ WithNestedChildren.args = {
82
+ ...Default.args,
83
+ options: nestedOptions,
84
+ }
85
+
86
+ export const WithDisabledOption = Template.bind({})
87
+ WithDisabledOption.args = {
88
+ ...Default.args,
89
+ options: withDisabledOptions,
90
+ }
91
+
92
+ export const DarkTheme = Template.bind({})
93
+ DarkTheme.args = {
94
+ ...Default.args,
95
+ colorTheme: 'dark',
96
+ }
97
+
98
+ export const NoTextWrap = Template.bind({})
99
+ NoTextWrap.args = {
100
+ ...Default.args,
101
+ textWrap: false,
102
+ }
103
+
104
+ export const CustomIconColor = Template.bind({})
105
+ CustomIconColor.args = {
106
+ ...Default.args,
107
+ iconColor: '#2196F3',
108
+ }
@@ -0,0 +1,46 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import VideoThumbnail from './index.vue'
4
+ import Icon from '@/components/icon'
5
+
6
+ jest.mock('@/components/icon/iconCache.mjs', () => ({
7
+ fetchIcon: jest.fn(() => Promise.resolve('')),
8
+ }))
9
+
10
+ const SRC = 'https://example.com/thumb.jpg'
11
+
12
+ const mountThumbnail = (props = {}) =>
13
+ mount(VideoThumbnail, { props: { src: SRC, ...props } })
14
+
15
+ describe('VideoThumbnail', () => {
16
+ it('renders the image with the provided src', () => {
17
+ expect(mountThumbnail().find('img').attributes('src')).toBe(SRC)
18
+ })
19
+
20
+ it('updates the image when the src prop changes', async () => {
21
+ const wrapper = mountThumbnail()
22
+ await wrapper.setProps({ src: 'https://example.com/other.jpg' })
23
+ expect(wrapper.find('img').attributes('src')).toBe(
24
+ 'https://example.com/other.jpg'
25
+ )
26
+ })
27
+
28
+ it('renders the play icon', () => {
29
+ expect(mountThumbnail().findComponent(Icon).attributes('name')).toBe('play')
30
+ })
31
+
32
+ it('uses the default play icon color and size', () => {
33
+ const icon = mountThumbnail().findComponent(Icon)
34
+ expect(icon.attributes('color')).toBe('blue')
35
+ expect(icon.attributes('size')).toBe('50px')
36
+ })
37
+
38
+ it('forwards playIconColor and playIconSize to the icon', () => {
39
+ const icon = mountThumbnail({
40
+ playIconColor: 'red',
41
+ playIconSize: '20px',
42
+ }).findComponent(Icon)
43
+ expect(icon.attributes('color')).toBe('red')
44
+ expect(icon.attributes('size')).toBe('20px')
45
+ })
46
+ })