|
1 | | -import { mount } from '@vue/test-utils'; |
| 1 | +import { render, screen } from '@testing-library/vue'; |
| 2 | +import userEvent from '@testing-library/user-event'; |
2 | 3 | import CollapsibleToolbar from '../index.vue'; |
| 4 | +import { commonStrings } from 'shared/strings/commonStrings'; |
3 | 5 |
|
4 | | -// Minimal KIconButton stub that renders a clickable button with a data-icon attr |
5 | | -const KIconButtonStub = { |
6 | | - name: 'KIconButton', |
7 | | - props: ['icon', 'tooltip', 'ariaLabel', 'disabled', 'color'], |
8 | | - template: ` |
9 | | - <button |
10 | | - :data-icon="icon" |
11 | | - :aria-label="ariaLabel" |
12 | | - :disabled="disabled" |
13 | | - @click="$emit('click')" |
14 | | - > |
15 | | - <slot name="menu" /> |
16 | | - </button> |
17 | | - `, |
18 | | -}; |
19 | | - |
20 | | -const KDropdownMenuStub = { |
21 | | - name: 'KDropdownMenu', |
22 | | - props: ['options'], |
23 | | - template: `<div class="k-dropdown-menu"><slot /></div>`, |
24 | | - methods: { |
25 | | - // expose a helper so tests can directly trigger a select |
26 | | - selectOption(value) { |
27 | | - this.$emit('select', { value }); |
28 | | - }, |
29 | | - }, |
30 | | -}; |
| 6 | +const { optionsLabel$ } = commonStrings; |
31 | 7 |
|
32 | | -function makeAction(overrides = {}) { |
33 | | - return { |
34 | | - id: 'action-1', |
35 | | - icon: 'edit', |
36 | | - label: 'Edit', |
37 | | - handler: jest.fn(), |
38 | | - collapsed: false, |
39 | | - disabled: false, |
40 | | - ...overrides, |
41 | | - }; |
42 | | -} |
| 8 | +const makeAction = (overrides = {}) => ({ |
| 9 | + id: 'action-1', |
| 10 | + icon: 'edit', |
| 11 | + label: 'Edit', |
| 12 | + handler: jest.fn(), |
| 13 | + collapsed: false, |
| 14 | + disabled: false, |
| 15 | + ...overrides, |
| 16 | +}); |
43 | 17 |
|
44 | | -function makeWrapper(actions = [], optionsLabel = 'Options') { |
45 | | - return mount(CollapsibleToolbar, { |
46 | | - propsData: { actions, optionsLabel }, |
47 | | - stubs: { KIconButton: KIconButtonStub, KDropdownMenu: KDropdownMenuStub }, |
| 18 | +const renderComponent = (actions = [], optionsLabel = null) => { |
| 19 | + return render(CollapsibleToolbar, { |
| 20 | + props: { actions, optionsLabel }, |
48 | 21 | }); |
49 | | -} |
| 22 | +}; |
50 | 23 |
|
51 | 24 | describe('CollapsibleToolbar', () => { |
52 | 25 | describe('visible icon actions', () => { |
53 | | - it('renders a KIconButton for each non-collapsed action that has an icon', () => { |
| 26 | + test('renders icon buttons for non-collapsed actions with icons', () => { |
54 | 27 | const actions = [ |
55 | | - makeAction({ id: 'a1', icon: 'edit', collapsed: false }), |
56 | | - makeAction({ id: 'a2', icon: 'delete', collapsed: false }), |
| 28 | + makeAction({ id: 'a1', label: 'Edit', icon: 'edit', collapsed: false }), |
| 29 | + makeAction({ id: 'a2', label: 'Move up', icon: 'chevronUp', collapsed: false }), |
57 | 30 | ]; |
58 | | - const wrapper = makeWrapper(actions); |
59 | | - const iconBtns = wrapper.find('.icon-actions-wrapper').findAll('button'); |
60 | | - expect(iconBtns.length).toBe(2); |
| 31 | + renderComponent(actions); |
| 32 | + expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument(); |
| 33 | + expect(screen.getByRole('button', { name: 'Move up' })).toBeInTheDocument(); |
61 | 34 | }); |
62 | 35 |
|
63 | | - it('does not render icon buttons for collapsed actions', () => { |
| 36 | + test('does not render an icon button for collapsed actions', () => { |
64 | 37 | const actions = [ |
65 | | - makeAction({ id: 'a1', icon: 'edit', collapsed: false }), |
66 | | - makeAction({ id: 'a2', icon: 'delete', collapsed: true }), |
| 38 | + makeAction({ id: 'a1', label: 'Edit', icon: 'edit', collapsed: false }), |
| 39 | + makeAction({ id: 'a2', label: 'Delete', icon: 'delete', collapsed: true }), |
67 | 40 | ]; |
68 | | - const wrapper = makeWrapper(actions); |
69 | | - const iconBtns = wrapper.find('.icon-actions-wrapper').findAll('button'); |
70 | | - expect(iconBtns.length).toBe(1); |
| 41 | + renderComponent(actions); |
| 42 | + expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument(); |
| 43 | + // 'Delete' only appears inside the dropdown, not as a standalone icon button |
| 44 | + expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); |
71 | 45 | }); |
72 | 46 |
|
73 | | - it('does not render icon buttons for actions without an icon', () => { |
74 | | - const actions = [makeAction({ id: 'a1', icon: null, collapsed: false })]; |
75 | | - const wrapper = makeWrapper(actions); |
76 | | - const iconBtns = wrapper.find('.icon-actions-wrapper').findAll('button'); |
77 | | - expect(iconBtns.length).toBe(0); |
78 | | - }); |
79 | | - |
80 | | - it('calls the action handler when an icon button is clicked', async () => { |
| 47 | + test('calls the action handler when an icon button is clicked', async () => { |
| 48 | + const user = userEvent.setup(); |
81 | 49 | const handler = jest.fn(); |
82 | | - const actions = [makeAction({ id: 'a1', icon: 'edit', collapsed: false, handler })]; |
83 | | - const wrapper = makeWrapper(actions); |
84 | | - await wrapper.find('.icon-actions-wrapper button').trigger('click'); |
85 | | - expect(handler).toHaveBeenCalledTimes(1); |
86 | | - }); |
87 | | - |
88 | | - it('passes the correct ariaLabel to each icon button', () => { |
89 | 50 | const actions = [ |
90 | | - makeAction({ id: 'a1', icon: 'edit', label: 'Edit item', collapsed: false }), |
| 51 | + makeAction({ id: 'a1', label: 'Edit', icon: 'edit', collapsed: false, handler }), |
91 | 52 | ]; |
92 | | - const wrapper = makeWrapper(actions); |
93 | | - const btn = wrapper.find('.icon-actions-wrapper button'); |
94 | | - expect(btn.attributes('aria-label')).toBe('Edit item'); |
| 53 | + renderComponent(actions); |
| 54 | + await user.click(screen.getByRole('button', { name: 'Edit' })); |
| 55 | + expect(handler).toHaveBeenCalledTimes(1); |
95 | 56 | }); |
96 | 57 | }); |
97 | 58 |
|
98 | 59 | describe('collapsed dropdown menu', () => { |
99 | | - it('does not render the options menu button when all actions are visible', () => { |
| 60 | + test('does not render the options button when there are no collapsed actions', () => { |
100 | 61 | const actions = [makeAction({ id: 'a1', icon: 'edit', collapsed: false })]; |
101 | | - const wrapper = makeWrapper(actions); |
102 | | - // Only 1 button should exist (the single icon action) — no options button |
103 | | - expect(wrapper.findAll('button').length).toBe(1); |
| 62 | + renderComponent(actions); |
| 63 | + expect(screen.queryByRole('button', { name: optionsLabel$() })).not.toBeInTheDocument(); |
104 | 64 | }); |
105 | 65 |
|
106 | | - it('renders the options menu button when there are collapsed actions', () => { |
| 66 | + test('renders the options button when there are collapsed actions', () => { |
107 | 67 | const actions = [ |
108 | 68 | makeAction({ id: 'a1', icon: 'edit', collapsed: false }), |
109 | 69 | makeAction({ id: 'a2', icon: null, label: 'Delete', collapsed: true, handler: jest.fn() }), |
110 | 70 | ]; |
111 | | - const wrapper = makeWrapper(actions); |
112 | | - // 1 icon button + 1 options button |
113 | | - expect(wrapper.findAll('button').length).toBe(2); |
| 71 | + renderComponent(actions); |
| 72 | + expect(screen.getByRole('button', { name: optionsLabel$() })).toBeInTheDocument(); |
114 | 73 | }); |
115 | 74 |
|
116 | | - it('renders the options menu button when action has no icon (forces menu)', () => { |
117 | | - // Even if collapsed: false, no icon → goes to menu |
118 | | - const actions = [makeAction({ id: 'a1', icon: null, collapsed: false })]; |
119 | | - const wrapper = makeWrapper(actions); |
120 | | - // 0 icon buttons + 1 options button |
121 | | - expect(wrapper.findAll('button').length).toBe(1); |
122 | | - // The options button should have the optionsVertical icon |
123 | | - expect(wrapper.find('button').attributes('data-icon')).toBe('optionsVertical'); |
| 75 | + test('renders the options button when an action has no icon (forces it to menu)', () => { |
| 76 | + const actions = [makeAction({ id: 'a1', icon: null, label: 'Delete', collapsed: false })]; |
| 77 | + renderComponent(actions); |
| 78 | + expect(screen.getByRole('button', { name: optionsLabel$() })).toBeInTheDocument(); |
124 | 79 | }); |
125 | 80 |
|
126 | | - it('calls the correct handler when a dropdown option is selected', async () => { |
127 | | - const handler = jest.fn(); |
128 | | - const actions = [ |
129 | | - makeAction({ id: 'delete', icon: null, label: 'Delete', collapsed: true, handler }), |
130 | | - ]; |
131 | | - const wrapper = makeWrapper(actions); |
132 | | - // Programmatically emit a select from the dropdown stub |
133 | | - const dropdown = wrapper.findComponent(KDropdownMenuStub); |
134 | | - await dropdown.vm.selectOption('delete'); |
135 | | - expect(handler).toHaveBeenCalledTimes(1); |
| 81 | + test('uses the provided optionsLabel prop for the menu button', () => { |
| 82 | + const actions = [makeAction({ id: 'a1', icon: null, label: 'Delete', collapsed: true })]; |
| 83 | + renderComponent(actions, 'Custom options label'); |
| 84 | + expect(screen.getByRole('button', { name: 'Custom options label' })).toBeInTheDocument(); |
136 | 85 | }); |
137 | 86 | }); |
138 | 87 |
|
139 | 88 | describe('disabled state', () => { |
140 | | - it('renders a disabled icon button when action.disabled is true', () => { |
141 | | - const actions = [makeAction({ id: 'a1', icon: 'edit', collapsed: false, disabled: true })]; |
142 | | - const wrapper = makeWrapper(actions); |
143 | | - const btn = wrapper.find('.icon-actions-wrapper button'); |
144 | | - expect(btn.attributes('disabled')).toBeDefined(); |
| 89 | + test('renders the icon button as disabled when action.disabled is true', () => { |
| 90 | + const actions = [ |
| 91 | + makeAction({ id: 'a1', label: 'Edit', icon: 'edit', collapsed: false, disabled: true }), |
| 92 | + ]; |
| 93 | + renderComponent(actions); |
| 94 | + expect(screen.getByRole('button', { name: 'Edit' })).toBeDisabled(); |
145 | 95 | }); |
146 | 96 | }); |
147 | 97 | }); |
0 commit comments