@weni/unnnic-system 3.28.2-alpha.3 → 3.30.0

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.
Files changed (30) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/style.css +1 -1
  3. package/dist/unnnic.mjs +2454 -2445
  4. package/dist/unnnic.umd.js +18 -18
  5. package/package.json +2 -2
  6. package/src/components/Button/__tests__/Button.spec.js +81 -0
  7. package/src/components/Button/__tests__/ButtonIcon.spec.js +56 -0
  8. package/src/components/Carousel/TagCarousel.vue +8 -2
  9. package/src/components/Carousel/__tests__/TagCarousel.spec.js +258 -0
  10. package/src/components/ChartLine/__tests__/ChartLine.spec.js +170 -0
  11. package/src/components/Checkbox/__tests__/Checkbox.spec.js +14 -0
  12. package/src/components/CheckboxGroup/__tests__/CheckboxGroup.spec.js +62 -0
  13. package/src/components/DatePicker/DatePicker.vue +1 -0
  14. package/src/components/DatePicker/__tests__/DatePicker.spec.js +364 -0
  15. package/src/components/Disclaimer/__tests__/Disclaimer.spec.js +16 -0
  16. package/src/components/DropArea/DropArea.vue +1 -2
  17. package/src/components/DropArea/__tests__/DropArea.spec.js +333 -0
  18. package/src/components/MultiSelect/__tests__/MultiSelect.spec.js +109 -2
  19. package/src/components/MultiSelect/index.vue +1 -1
  20. package/src/components/PageHeader/__tests__/PageHeader.spec.js +97 -0
  21. package/src/components/Pagination/__tests__/Pagination.spec.js +99 -0
  22. package/src/components/Radio/__test__/Radio.spec.js +67 -0
  23. package/src/components/RadioGroup/__tests__/RadioGroup.spec.js +100 -0
  24. package/src/components/Slider/__tests__/Slider.spec.js +321 -0
  25. package/src/components/Tag/DefaultTag.vue +4 -3
  26. package/src/components/Tag/__tests__/DefaultTag.spec.js +51 -0
  27. package/src/components/Tag/__tests__/Tag.spec.js +82 -0
  28. package/src/components/TextArea/__test__/TextArea.spec.js +19 -0
  29. package/src/components/ToolTip/__tests__/ToolTip.spec.js +82 -1
  30. package/src/components/ui/table/TableRow.vue +4 -0
@@ -0,0 +1,333 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+
4
+ import DropArea from '../DropArea.vue';
5
+ import i18n from '@/utils/plugins/i18n';
6
+
7
+ const createFile = (name, type, sizeBytes = 1024) =>
8
+ new File(['x'.repeat(sizeBytes)], name, { type });
9
+
10
+ const mountDropArea = (props = {}, listeners = {}) =>
11
+ mount(DropArea, {
12
+ props: {
13
+ currentFiles: [],
14
+ ...props,
15
+ },
16
+ attrs: listeners,
17
+ global: {
18
+ plugins: [i18n],
19
+ stubs: {
20
+ UnnnicIcon: true,
21
+ },
22
+ },
23
+ });
24
+
25
+ describe('DropArea', () => {
26
+ beforeEach(() => {
27
+ vi.useFakeTimers();
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.useRealTimers();
32
+ });
33
+
34
+ it('renders default title and supported formats', () => {
35
+ const wrapper = mountDropArea({ supportedFormats: '.pdf,.png' });
36
+
37
+ expect(wrapper.text()).toContain('Drag your file here or');
38
+ expect(wrapper.attributes('title')).toBeUndefined();
39
+ expect(
40
+ wrapper
41
+ .find('.unnnic-upload-area__dropzone__content__subtitle')
42
+ .attributes('title'),
43
+ ).toBe('.PDF, .PNG');
44
+ });
45
+
46
+ it('uses custom subtitle when provided', () => {
47
+ const wrapper = mountDropArea({ subtitle: 'Custom subtitle' });
48
+
49
+ expect(wrapper.text()).toContain('Custom subtitle');
50
+ });
51
+
52
+ it('opens file picker when dropzone is clicked', async () => {
53
+ const wrapper = mountDropArea();
54
+ const clickSpy = vi.spyOn(wrapper.vm.$refs.file, 'click');
55
+
56
+ await wrapper.find('.unnnic-upload-area__dropzone').trigger('click');
57
+
58
+ expect(clickSpy).toHaveBeenCalled();
59
+ });
60
+
61
+ it('does not react when disabled', async () => {
62
+ const wrapper = mountDropArea({ disabled: true });
63
+ const clickSpy = vi.spyOn(wrapper.vm.$refs.file, 'click');
64
+ const file = createFile('notes.txt', 'text/plain');
65
+
66
+ await wrapper.trigger('dragenter');
67
+ await wrapper.trigger('dragover');
68
+ await wrapper.trigger('dragleave');
69
+ await wrapper.trigger('dragend');
70
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
71
+ await wrapper.trigger('click');
72
+
73
+ const input = wrapper.find('input[type="file"]');
74
+ Object.defineProperty(input.element, 'files', {
75
+ configurable: true,
76
+ value: [file],
77
+ });
78
+ await input.trigger('input');
79
+
80
+ expect(wrapper.classes()).not.toContain(
81
+ 'unnnic-upload-area__dropzone__is-dragover',
82
+ );
83
+ expect(clickSpy).not.toHaveBeenCalled();
84
+ expect(wrapper.emitted('update:currentFiles')).toBeUndefined();
85
+ });
86
+
87
+ it('handles drag enter, over, leave, and end states', async () => {
88
+ const wrapper = mountDropArea();
89
+
90
+ await wrapper.trigger('dragenter');
91
+ expect(wrapper.classes()).toContain(
92
+ 'unnnic-upload-area__dropzone__is-dragover',
93
+ );
94
+
95
+ await wrapper.trigger('dragover');
96
+ await wrapper.trigger('dragleave');
97
+ expect(wrapper.classes()).not.toContain(
98
+ 'unnnic-upload-area__dropzone__is-dragover',
99
+ );
100
+
101
+ await wrapper.trigger('dragenter');
102
+ await wrapper.trigger('dragend');
103
+ expect(wrapper.classes()).not.toContain(
104
+ 'unnnic-upload-area__dropzone__is-dragover',
105
+ );
106
+ });
107
+
108
+ it('adds valid files on drop', async () => {
109
+ const wrapper = mountDropArea({ supportedFormats: '.txt' });
110
+ const file = createFile('notes.txt', 'text/plain');
111
+ const dataTransfer = { files: [file] };
112
+
113
+ await wrapper.trigger('drop', { dataTransfer });
114
+
115
+ expect(wrapper.emitted('update:currentFiles')[0][0]).toEqual([file]);
116
+ expect(wrapper.emitted('fileChange')[0][0]).toEqual([file]);
117
+ });
118
+
119
+ it('adds valid files from file input change', async () => {
120
+ const wrapper = mountDropArea({ supportedFormats: '.txt' });
121
+ const file = createFile('notes.txt', 'text/plain');
122
+ const input = wrapper.find('input[type="file"]');
123
+
124
+ Object.defineProperty(input.element, 'files', {
125
+ configurable: true,
126
+ value: [file],
127
+ });
128
+
129
+ await input.trigger('input');
130
+
131
+ expect(wrapper.emitted('update:currentFiles')).toBeTruthy();
132
+ expect(input.element.value).toBe('');
133
+ });
134
+
135
+ it('shows error when multiple files are not allowed', async () => {
136
+ const wrapper = mountDropArea({ acceptMultiple: false });
137
+ const files = [
138
+ createFile('a.txt', 'text/plain'),
139
+ createFile('b.txt', 'text/plain'),
140
+ ];
141
+
142
+ await wrapper.trigger('drop', { dataTransfer: { files } });
143
+
144
+ expect(wrapper.classes()).toContain(
145
+ 'unnnic-upload-area__dropzone__has-error',
146
+ );
147
+ expect(wrapper.emitted('update:currentFiles')).toBeUndefined();
148
+
149
+ vi.advanceTimersByTime(5000);
150
+ await flushPromises();
151
+ expect(wrapper.classes()).not.toContain(
152
+ 'unnnic-upload-area__dropzone__has-error',
153
+ );
154
+ });
155
+
156
+ it('emits unsupportedFormat when handler is provided', async () => {
157
+ const wrapper = mountDropArea(
158
+ { supportedFormats: '.pdf' },
159
+ { onUnsupportedFormat: () => {} },
160
+ );
161
+ const file = createFile('notes.txt', 'text/plain');
162
+
163
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
164
+
165
+ expect(wrapper.emitted('unsupportedFormat')).toBeTruthy();
166
+ });
167
+
168
+ it('shows error for unsupported format without custom handler', async () => {
169
+ const wrapper = mountDropArea({ supportedFormats: '.pdf' });
170
+ const file = createFile('notes.txt', 'text/plain');
171
+
172
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
173
+
174
+ expect(wrapper.classes()).toContain(
175
+ 'unnnic-upload-area__dropzone__has-error',
176
+ );
177
+ });
178
+
179
+ it('emits exceededTheMaximumFileSizeLimit when handler is provided', async () => {
180
+ const wrapper = mountDropArea(
181
+ { maxFileSize: 0.001 },
182
+ { onExceededTheMaximumFileSizeLimit: () => {} },
183
+ );
184
+ const file = createFile('large.txt', 'text/plain', 1024 * 1024);
185
+
186
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
187
+
188
+ expect(wrapper.emitted('exceededTheMaximumFileSizeLimit')).toBeTruthy();
189
+ });
190
+
191
+ it('shows error when file exceeds max size without custom handler', async () => {
192
+ const wrapper = mountDropArea({ maxFileSize: 0.001 });
193
+ const file = createFile('large.txt', 'text/plain', 1024 * 1024);
194
+
195
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
196
+
197
+ expect(wrapper.classes()).toContain(
198
+ 'unnnic-upload-area__dropzone__has-error',
199
+ );
200
+ });
201
+
202
+ it('accepts any format when supportedFormats is *', async () => {
203
+ const wrapper = mountDropArea({ supportedFormats: '*' });
204
+ const file = createFile('archive.zip', 'application/zip');
205
+
206
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
207
+
208
+ expect(wrapper.emitted('update:currentFiles')).toBeTruthy();
209
+ });
210
+
211
+ it('shows error when maximum uploads is exceeded', async () => {
212
+ const existing = createFile('existing.txt', 'text/plain');
213
+ const wrapper = mountDropArea({
214
+ currentFiles: [existing],
215
+ maximumUploads: 1,
216
+ supportedFormats: '.txt',
217
+ });
218
+ const file = createFile('new.txt', 'text/plain');
219
+
220
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
221
+
222
+ expect(wrapper.classes()).toContain(
223
+ 'unnnic-upload-area__dropzone__has-error',
224
+ );
225
+ });
226
+
227
+ it('replaces files when shouldReplace is true', async () => {
228
+ const existing = createFile('existing.txt', 'text/plain');
229
+ const replacement = createFile('new.txt', 'text/plain');
230
+ const wrapper = mountDropArea({
231
+ currentFiles: [existing],
232
+ shouldReplace: true,
233
+ supportedFormats: '.txt',
234
+ });
235
+
236
+ await wrapper.trigger('drop', { dataTransfer: { files: [replacement] } });
237
+
238
+ expect(wrapper.emitted('update:currentFiles')[0][0]).toEqual([replacement]);
239
+ });
240
+
241
+ it('appends files to currentFiles by default', async () => {
242
+ const existing = createFile('existing.txt', 'text/plain');
243
+ const incoming = createFile('new.txt', 'text/plain');
244
+ const wrapper = mountDropArea({
245
+ currentFiles: [existing],
246
+ maximumUploads: 2,
247
+ supportedFormats: '.txt',
248
+ });
249
+
250
+ await wrapper.trigger('drop', { dataTransfer: { files: [incoming] } });
251
+
252
+ expect(wrapper.emitted('update:currentFiles')[0][0]).toEqual([
253
+ existing,
254
+ incoming,
255
+ ]);
256
+ });
257
+
258
+ it('filters out files that exceed max size during addFiles', async () => {
259
+ const wrapper = mountDropArea({
260
+ supportedFormats: '.txt',
261
+ maxFileSize: 0.001,
262
+ maximumUploads: 2,
263
+ });
264
+ const validFile = createFile('notes.txt', 'text/plain', 100);
265
+ const largeFile = createFile('large.txt', 'text/plain', 1024 * 1024);
266
+
267
+ await wrapper.trigger('drop', {
268
+ dataTransfer: { files: [validFile, largeFile] },
269
+ });
270
+
271
+ expect(wrapper.emitted('update:currentFiles')[0][0]).toEqual([validFile]);
272
+ });
273
+
274
+ it('shows invalid subtitle when hasError is true', async () => {
275
+ const wrapper = mountDropArea({ supportedFormats: '.pdf' });
276
+ const file = createFile('notes.txt', 'text/plain');
277
+
278
+ await wrapper.trigger('drop', { dataTransfer: { files: [file] } });
279
+
280
+ expect(wrapper.text()).toContain('File not supported');
281
+ });
282
+
283
+ it('keeps dragover state until all dragenter events are reversed', async () => {
284
+ const wrapper = mountDropArea();
285
+
286
+ await wrapper.trigger('dragenter');
287
+ await wrapper.trigger('dragenter');
288
+ await wrapper.trigger('dragleave');
289
+
290
+ expect(wrapper.classes()).toContain(
291
+ 'unnnic-upload-area__dropzone__is-dragover',
292
+ );
293
+
294
+ await wrapper.trigger('dragleave');
295
+
296
+ expect(wrapper.classes()).not.toContain(
297
+ 'unnnic-upload-area__dropzone__is-dragover',
298
+ );
299
+ });
300
+
301
+ it('does not add files from input when validation fails', async () => {
302
+ const wrapper = mountDropArea({ supportedFormats: '.pdf' });
303
+ const file = createFile('notes.txt', 'text/plain');
304
+ const input = wrapper.find('input[type="file"]');
305
+
306
+ Object.defineProperty(input.element, 'files', {
307
+ configurable: true,
308
+ value: [file],
309
+ });
310
+
311
+ await input.trigger('input');
312
+
313
+ expect(wrapper.emitted('update:currentFiles')).toBeUndefined();
314
+ expect(input.element.value).toBe('');
315
+ });
316
+
317
+ it('renders custom title and subtitle slots', () => {
318
+ const wrapper = mount(DropArea, {
319
+ props: { currentFiles: [] },
320
+ slots: {
321
+ title: '<strong data-testid="custom-title">Upload</strong>',
322
+ subtitle: '<em data-testid="custom-subtitle">Only CSV</em>',
323
+ },
324
+ global: {
325
+ plugins: [i18n],
326
+ stubs: { UnnnicIcon: true },
327
+ },
328
+ });
329
+
330
+ expect(wrapper.find('[data-testid="custom-title"]').exists()).toBe(true);
331
+ expect(wrapper.find('[data-testid="custom-subtitle"]').exists()).toBe(true);
332
+ });
333
+ });
@@ -1,4 +1,4 @@
1
- import { mount } from '@vue/test-utils';
1
+ import { mount, flushPromises } from '@vue/test-utils';
2
2
  import { beforeEach, describe, expect, afterEach, test } from 'vitest';
3
3
  import UnnnicMultiSelect from '../index.vue';
4
4
  import i18n from '@/utils/plugins/i18n';
@@ -15,10 +15,13 @@ describe('UnnnicMultiSelect.vue', () => {
15
15
  modelValue: [],
16
16
  };
17
17
 
18
- const mountWrapper = (props = {}, slots = {}) => {
18
+ const mountWrapper = (props = {}, slots = {}, options = {}) => {
19
19
  return mount(UnnnicMultiSelect, {
20
+ attachTo: document.body,
21
+ ...options,
20
22
  global: {
21
23
  plugins: [i18n],
24
+ ...(options.global || {}),
22
25
  },
23
26
  props: {
24
27
  ...defaultProps,
@@ -269,6 +272,30 @@ describe('UnnnicMultiSelect.vue', () => {
269
272
  const filteredOptions = wrapper.vm.filteredOptions;
270
273
  expect(filteredOptions.length).toBe(3);
271
274
  });
275
+
276
+ test('shows no results message when search has no matches', async () => {
277
+ await wrapper.setProps({ enableSearch: true, search: 'no-match' });
278
+ wrapper.vm.setOpenPopover(true);
279
+ await wrapper.vm.$nextTick();
280
+
281
+ expect(wrapper.vm.filteredOptions).toEqual([]);
282
+ const noResults = wrapper.find(
283
+ '.unnnic-multi-select__content-no-results',
284
+ );
285
+ if (noResults.exists()) {
286
+ expect(noResults.text()).toContain('No results found');
287
+ }
288
+ });
289
+
290
+ test('clears search when popover closes', async () => {
291
+ await wrapper.setProps({ enableSearch: true, search: 'option' });
292
+ wrapper.vm.setOpenPopover(true);
293
+ await wrapper.vm.$nextTick();
294
+ wrapper.vm.setOpenPopover(false);
295
+ await wrapper.vm.$nextTick();
296
+
297
+ expect(wrapper.emitted('update:search')).toContainEqual(['']);
298
+ });
272
299
  });
273
300
 
274
301
  describe('computed properties', () => {
@@ -489,6 +516,16 @@ describe('UnnnicMultiSelect.vue', () => {
489
516
  expect(wrapper.emitted('update:modelValue')).toBeTruthy();
490
517
  expect(wrapper.emitted('update:modelValue')[0]).toEqual([[]]);
491
518
  });
519
+
520
+ test('shows clear button when clearable and items are selected', async () => {
521
+ await wrapper.setProps({
522
+ clearable: true,
523
+ modelValue: ['option1'],
524
+ });
525
+
526
+ const input = wrapper.findComponent({ name: 'UnnnicInput' });
527
+ expect(input.props('showClear')).toBe(true);
528
+ });
492
529
  });
493
530
 
494
531
  describe('getActivatedOptionStatus', () => {
@@ -520,6 +557,76 @@ describe('UnnnicMultiSelect.vue', () => {
520
557
  });
521
558
  });
522
559
 
560
+ describe('keyboard navigation', () => {
561
+ test('closes popover through keyboard close callback', async () => {
562
+ wrapper.vm.setOpenPopover(true);
563
+ await flushPromises();
564
+ await flushPromises();
565
+
566
+ document.dispatchEvent(
567
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
568
+ );
569
+ await flushPromises();
570
+ document.dispatchEvent(
571
+ new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }),
572
+ );
573
+ await flushPromises();
574
+
575
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy();
576
+ });
577
+
578
+ test('ignores disabled options on Enter key', async () => {
579
+ await wrapper.setProps({
580
+ options: [
581
+ { label: 'Disabled', value: 'disabled', disabled: true },
582
+ { label: 'Option 2', value: 'option2' },
583
+ ],
584
+ });
585
+ wrapper.vm.setOpenPopover(true);
586
+ await flushPromises();
587
+ await flushPromises();
588
+
589
+ document.dispatchEvent(
590
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
591
+ );
592
+ await flushPromises();
593
+ document.dispatchEvent(
594
+ new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }),
595
+ );
596
+ await flushPromises();
597
+
598
+ expect(wrapper.emitted('update:modelValue')).toBeFalsy();
599
+ });
600
+
601
+ test('toggles option through option component emit', async () => {
602
+ await wrapper.setProps({ modelValue: ['option1'] });
603
+ wrapper.vm.setOpenPopover(true);
604
+ await wrapper.vm.$nextTick();
605
+ const options = wrapper.findAllComponents({
606
+ name: 'UnnnicMultiSelectOption',
607
+ });
608
+
609
+ await options[0].vm.$emit('update:model-value', false);
610
+
611
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy();
612
+ });
613
+
614
+ test('closes popover through popover update event', async () => {
615
+ wrapper.vm.setOpenPopover(true);
616
+ await wrapper.vm.$nextTick();
617
+
618
+ const popover = wrapper.findComponent({ name: 'UnnnicPopover' });
619
+ if (popover.exists()) {
620
+ await popover.vm.$emit('update:open', false);
621
+ } else {
622
+ wrapper.vm.setOpenPopover(false);
623
+ }
624
+ await wrapper.vm.$nextTick();
625
+
626
+ expect(wrapper.vm.openPopover).toBe(false);
627
+ });
628
+ });
629
+
523
630
  describe('snapshot testing', () => {
524
631
  test('matches snapshot with default props', () => {
525
632
  expect(wrapper.html()).toMatchSnapshot();
@@ -175,7 +175,7 @@ const keyboard = useSelectKeyboard<SelectOption>(
175
175
  },
176
176
  {
177
177
  closeOnSelect: false,
178
- closePopover: () => (base.openPopover.value = false),
178
+ closePopover: () => setOpenPopover(false),
179
179
  openPopoverRef: base.openPopover,
180
180
  },
181
181
  );
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+
4
+ import PageHeader from '../PageHeader.vue';
5
+
6
+ const mountPageHeader = (props = {}, slots = {}) =>
7
+ mount(PageHeader, {
8
+ props: {
9
+ title: 'Page title',
10
+ ...props,
11
+ },
12
+ slots,
13
+ global: {
14
+ stubs: {
15
+ UnnnicButton: {
16
+ template:
17
+ '<button data-testid="back-button-stub" @click="$emit(\'click\')"><slot /></button>',
18
+ },
19
+ },
20
+ },
21
+ });
22
+
23
+ describe('PageHeader', () => {
24
+ it('renders title and description', () => {
25
+ const wrapper = mountPageHeader({
26
+ description: 'Page description',
27
+ });
28
+
29
+ expect(wrapper.find('[data-testid="page-title"]').text()).toBe(
30
+ 'Page title',
31
+ );
32
+ expect(wrapper.find('[data-testid="page-description"]').text()).toBe(
33
+ 'Page description',
34
+ );
35
+ });
36
+
37
+ it('renders back button and emits back event', async () => {
38
+ const wrapper = mountPageHeader({ hasBackButton: true });
39
+
40
+ expect(wrapper.classes()).toContain('page-header--has-back-button');
41
+ await wrapper.find('[data-testid="back-button"]').trigger('click');
42
+ expect(wrapper.emitted('back')).toBeTruthy();
43
+ });
44
+
45
+ it('renders actions slot', () => {
46
+ const wrapper = mountPageHeader(
47
+ {},
48
+ {
49
+ actions: '<button data-testid="action-btn">Action</button>',
50
+ },
51
+ );
52
+
53
+ expect(wrapper.find('[data-testid="page-actions"]').exists()).toBe(true);
54
+ expect(wrapper.find('[data-testid="action-btn"]').exists()).toBe(true);
55
+ });
56
+
57
+ it('renders tabs slot and removes divider class', () => {
58
+ const wrapper = mountPageHeader(
59
+ {},
60
+ {
61
+ tabs: '<nav data-testid="tabs-nav">Tabs</nav>',
62
+ },
63
+ );
64
+
65
+ expect(wrapper.find('[data-testid="page-tabs"]').exists()).toBe(true);
66
+ expect(wrapper.classes()).toContain('page-header--no-divider');
67
+ });
68
+
69
+ it('hides divider when hideDivider is true', () => {
70
+ const wrapper = mountPageHeader({ hideDivider: true });
71
+
72
+ expect(wrapper.classes()).toContain('page-header--no-divider');
73
+ });
74
+
75
+ it('renders custom infos slot', () => {
76
+ const wrapper = mountPageHeader(
77
+ {},
78
+ {
79
+ infos: '<section data-testid="custom-infos">Custom</section>',
80
+ },
81
+ );
82
+
83
+ expect(wrapper.find('[data-testid="custom-infos"]').exists()).toBe(true);
84
+ expect(wrapper.find('[data-testid="page-title"]').exists()).toBe(false);
85
+ });
86
+
87
+ it('renders tag slot inside default infos', () => {
88
+ const wrapper = mountPageHeader(
89
+ {},
90
+ {
91
+ tag: '<span data-testid="header-tag">Tag</span>',
92
+ },
93
+ );
94
+
95
+ expect(wrapper.find('[data-testid="header-tag"]').exists()).toBe(true);
96
+ });
97
+ });
@@ -205,4 +205,103 @@ describe('Pagination', () => {
205
205
  });
206
206
  });
207
207
  });
208
+
209
+ describe('edge cases', () => {
210
+ it('renders single page pagination', () => {
211
+ const wrapper = setup({ modelValue: 1, max: 1, disabled: false });
212
+
213
+ expect(wrapper.findAll('[data-test="page-button"]')).toHaveLength(1);
214
+ expect(
215
+ wrapper.find('[data-test="next-button"]').isDisabled(),
216
+ ).toBeTruthy();
217
+ });
218
+ });
219
+
220
+ describe('when previous is not available', () => {
221
+ it('does not emit when previous button is clicked on first page', async () => {
222
+ const wrapper = setup({
223
+ modelValue: 1,
224
+ max: 10,
225
+ disabled: false,
226
+ });
227
+
228
+ await wrapper.find('[data-test="previous-button"]').trigger('click');
229
+ expect(wrapper.emitted('update:model-value')).toBeUndefined();
230
+ });
231
+ });
232
+
233
+ describe('when next is not available', () => {
234
+ it('does not emit when next button is clicked on last page', async () => {
235
+ const wrapper = setup({
236
+ modelValue: 10,
237
+ max: 10,
238
+ disabled: false,
239
+ });
240
+
241
+ await wrapper.find('[data-test="next-button"]').trigger('click');
242
+ expect(wrapper.emitted('update:model-value')).toBeUndefined();
243
+ });
244
+ });
245
+
246
+ describe('when pagination is disabled', () => {
247
+ it('disables all page buttons', () => {
248
+ const wrapper = setup({
249
+ modelValue: 2,
250
+ max: 5,
251
+ disabled: true,
252
+ });
253
+
254
+ wrapper.findAll('[data-test="page-button"]').forEach((button) => {
255
+ expect(button.isDisabled()).toBeTruthy();
256
+ });
257
+ expect(
258
+ wrapper.find('[data-test="previous-button"]').isDisabled(),
259
+ ).toBeTruthy();
260
+ expect(
261
+ wrapper.find('[data-test="next-button"]').isDisabled(),
262
+ ).toBeTruthy();
263
+ });
264
+ });
265
+
266
+ describe('when previous page is outside visible pages', () => {
267
+ it('updates reference when navigating to a hidden page', async () => {
268
+ const wrapper = setup({
269
+ modelValue: 4,
270
+ max: 10,
271
+ disabled: false,
272
+ });
273
+
274
+ await wrapper.find('[data-test="previous-button"]').trigger('click');
275
+
276
+ expect(wrapper.emitted('update:model-value')).toContainEqual([3]);
277
+ expect(wrapper.vm.reference).toBe(3);
278
+ });
279
+ });
280
+
281
+ describe('when next page is outside visible pages', () => {
282
+ it('updates reference when navigating forward to a hidden page', async () => {
283
+ const wrapper = setup({
284
+ modelValue: 6,
285
+ max: 10,
286
+ disabled: false,
287
+ });
288
+
289
+ await wrapper.find('[data-test="next-button"]').trigger('click');
290
+
291
+ expect(wrapper.emitted('update:model-value')).toContainEqual([7]);
292
+ expect(wrapper.vm.reference).toBe(7);
293
+ });
294
+ });
295
+
296
+ describe('when initialized on a page outside visible range', () => {
297
+ it('sets reference to the current page', () => {
298
+ const wrapper = setup({
299
+ modelValue: 8,
300
+ max: 20,
301
+ disabled: false,
302
+ });
303
+
304
+ expect(wrapper.vm.reference).toBe(8);
305
+ });
306
+ });
208
307
  });