@fiscozen/input 0.1.17 → 1.0.0-next.1

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.
@@ -1,181 +1,1046 @@
1
- import { describe, it } from "vitest";
2
- import { mount } from "@vue/test-utils";
3
- import { FzInput } from "..";
4
-
5
- const NUMBER_OF_INPUTS = 1000;
6
-
7
- describe.concurrent("FzInput", () => {
8
- it("renders label", async ({ expect }) => {
9
- const wrapper = mount(FzInput, {
10
- props: {
11
- label: "Label",
12
- },
13
- slots: {},
14
- });
15
-
16
- expect(wrapper.text()).toContain("Label");
17
- });
18
-
19
- it("renders leftIcon", async ({ expect }) => {
20
- const wrapper = mount(FzInput, {
21
- props: {
22
- label: "Label",
23
- leftIcon: "calendar-lines",
24
- },
25
- slots: {},
26
- });
27
-
28
- expect(wrapper.find(".fa-calendar-lines")).toBeTruthy();
29
- });
30
-
31
- it("renders rightIcon", async ({ expect }) => {
32
- const wrapper = mount(FzInput, {
33
- props: {
34
- label: "Label",
35
- rightIcon: "credit-card",
36
- },
37
- slots: {},
38
- });
39
-
40
- expect(wrapper.find(".fa-credit-card")).toBeTruthy();
41
- });
42
-
43
- it("renders helpText", async ({ expect }) => {
44
- const wrapper = mount(FzInput, {
45
- props: {
46
- label: "Label",
47
- },
48
- slots: {
49
- helpText: "This is a helper text",
50
- },
51
- });
52
-
53
- await wrapper.vm.$nextTick();
54
- expect(wrapper.text()).toContain("This is a helper text");
55
- });
56
-
57
- it("renders errorMessage", async ({ expect }) => {
58
- const wrapper = mount(FzInput, {
59
- props: {
60
- label: "Label",
61
- error: true,
62
- },
63
- slots: {
64
- errorMessage: "This is an error message",
65
- },
66
- });
67
-
68
- await wrapper.vm.$nextTick();
69
- expect(wrapper.text()).toContain("This is an error message");
70
- });
71
-
72
- it("renders disabled", async ({ expect }) => {
73
- const wrapper = mount(FzInput, {
74
- props: {
75
- label: "Label",
76
- disabled: true,
77
- },
78
- slots: {},
79
- });
80
-
81
- expect(wrapper.find("input").attributes("disabled")).toBe("");
82
- });
83
-
84
- it("renders required", async ({ expect }) => {
85
- const wrapper = mount(FzInput, {
86
- props: {
87
- label: "Label",
88
- required: true,
89
- },
90
- slots: {},
91
- });
92
-
93
- await wrapper.vm.$nextTick();
94
-
95
- expect(wrapper.find("input").attributes("required")).toBe("");
96
- expect(wrapper.text()).toContain("*");
97
- });
98
-
99
- it("renders email type", async ({ expect }) => {
100
- const wrapper = mount(FzInput, {
101
- props: {
102
- label: "Label",
103
- type: "email",
104
- },
105
- slots: {},
106
- });
107
-
108
- expect(wrapper.find("input").attributes("type")).toBe("email");
109
- });
110
-
111
- it("renders tel type", async ({ expect }) => {
112
- const wrapper = mount(FzInput, {
113
- props: {
114
- label: "Label",
115
- type: "tel",
116
- },
117
- slots: {},
118
- });
119
-
120
- expect(wrapper.find("input").attributes("type")).toBe("tel");
121
- });
122
-
123
- it("renders password type", async ({ expect }) => {
124
- const wrapper = mount(FzInput, {
125
- props: {
126
- label: "Label",
127
- type: "password",
128
- },
129
- slots: {},
130
- });
131
-
132
- expect(wrapper.find("input").attributes("type")).toBe("password");
133
- });
134
-
135
- it(`renders ${NUMBER_OF_INPUTS} input with different ids`, async ({
136
- expect,
137
- }) => {
138
- const wrapperList = Array.from({ length: NUMBER_OF_INPUTS }).map((_, i) =>
139
- mount(FzInput, {
140
- props: {
141
- label: `Label ${i}`,
142
- },
143
- slots: {},
144
- }),
145
- );
146
-
147
- await Promise.all(wrapperList.map((w) => w.vm.$nextTick()));
148
-
149
- const ids = wrapperList.map((w) => w.find("input").attributes("id"));
150
-
151
- expect(new Set(ids).size).toBe(NUMBER_OF_INPUTS);
152
- });
153
-
154
- it("emits fzinput:right-icon-click event", async ({ expect }) => {
155
- const wrapper = mount(FzInput, {
156
- props: {
157
- label: "Label",
158
- rightIcon: "eye",
159
- },
160
- slots: {},
161
- });
162
-
163
- await wrapper.find(".fa-eye").trigger("click");
164
-
165
- expect(wrapper.emitted("fzinput:right-icon-click")).toBeTruthy();
166
- });
167
-
168
- it("emits fzinput:left-icon-click event", async ({ expect }) => {
169
- const wrapper = mount(FzInput, {
170
- props: {
171
- label: "Label",
172
- leftIcon: "eye",
173
- },
174
- slots: {},
175
- });
176
-
177
- await wrapper.find(".fa-eye").trigger("click");
178
-
179
- expect(wrapper.emitted("fzinput:left-icon-click")).toBeTruthy();
180
- });
181
- });
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { FzInput } from '..'
4
+
5
+ const NUMBER_OF_INPUTS = 1000
6
+
7
+ describe('FzInput', () => {
8
+ describe('Rendering', () => {
9
+ it('renders label', async () => {
10
+ const wrapper = mount(FzInput, {
11
+ props: {
12
+ label: 'Label',
13
+ },
14
+ slots: {},
15
+ })
16
+
17
+ expect(wrapper.text()).toContain('Label')
18
+ })
19
+
20
+ it('renders leftIcon', async () => {
21
+ const wrapper = mount(FzInput, {
22
+ props: {
23
+ label: 'Label',
24
+ leftIcon: 'calendar-lines',
25
+ },
26
+ slots: {},
27
+ })
28
+
29
+ expect(wrapper.find('.fa-calendar-lines')).toBeTruthy()
30
+ })
31
+
32
+ it('renders rightIcon', async () => {
33
+ const wrapper = mount(FzInput, {
34
+ props: {
35
+ label: 'Label',
36
+ rightIcon: 'credit-card',
37
+ },
38
+ slots: {},
39
+ })
40
+
41
+ expect(wrapper.find('.fa-credit-card')).toBeTruthy()
42
+ })
43
+
44
+ it('renders helpText', async () => {
45
+ const wrapper = mount(FzInput, {
46
+ props: {
47
+ label: 'Label',
48
+ },
49
+ slots: {
50
+ helpText: 'This is a helper text',
51
+ },
52
+ })
53
+
54
+ await wrapper.vm.$nextTick()
55
+ expect(wrapper.text()).toContain('This is a helper text')
56
+ })
57
+
58
+ it('renders errorMessage', async () => {
59
+ const wrapper = mount(FzInput, {
60
+ props: {
61
+ label: 'Label',
62
+ error: true,
63
+ },
64
+ slots: {
65
+ errorMessage: 'This is an error message',
66
+ },
67
+ })
68
+
69
+ await wrapper.vm.$nextTick()
70
+ expect(wrapper.text()).toContain('This is an error message')
71
+ })
72
+ })
73
+
74
+ describe('Input types', () => {
75
+ it('renders email type', async () => {
76
+ const wrapper = mount(FzInput, {
77
+ props: {
78
+ label: 'Label',
79
+ type: 'email',
80
+ },
81
+ slots: {},
82
+ })
83
+
84
+ expect(wrapper.find('input').attributes('type')).toBe('email')
85
+ })
86
+
87
+ it('renders tel type', async () => {
88
+ const wrapper = mount(FzInput, {
89
+ props: {
90
+ label: 'Label',
91
+ type: 'tel',
92
+ },
93
+ slots: {},
94
+ })
95
+
96
+ expect(wrapper.find('input').attributes('type')).toBe('tel')
97
+ })
98
+
99
+ it('renders password type', async () => {
100
+ const wrapper = mount(FzInput, {
101
+ props: {
102
+ label: 'Label',
103
+ type: 'password',
104
+ },
105
+ slots: {},
106
+ })
107
+
108
+ expect(wrapper.find('input').attributes('type')).toBe('password')
109
+ })
110
+ })
111
+
112
+ describe('Input states', () => {
113
+ it('renders disabled', async () => {
114
+ const wrapper = mount(FzInput, {
115
+ props: {
116
+ label: 'Label',
117
+ disabled: true,
118
+ },
119
+ slots: {},
120
+ })
121
+
122
+ expect(wrapper.find('input').attributes('disabled')).toBe('')
123
+ })
124
+
125
+ it('renders required', async () => {
126
+ const wrapper = mount(FzInput, {
127
+ props: {
128
+ label: 'Label',
129
+ required: true,
130
+ },
131
+ slots: {},
132
+ })
133
+
134
+ await wrapper.vm.$nextTick()
135
+
136
+ expect(wrapper.find('input').attributes('required')).toBe('')
137
+ expect(wrapper.text()).toContain('*')
138
+ })
139
+
140
+ it('applies autocomplete="off" by default', async () => {
141
+ const wrapper = mount(FzInput, {
142
+ props: {
143
+ label: 'Label',
144
+ },
145
+ slots: {},
146
+ })
147
+
148
+ await wrapper.vm.$nextTick()
149
+
150
+ expect(wrapper.find('input').attributes('autocomplete')).toBe('off')
151
+ })
152
+
153
+ it('applies autocomplete="off" when autocomplete is false', async () => {
154
+ const wrapper = mount(FzInput, {
155
+ props: {
156
+ label: 'Label',
157
+ autocomplete: false,
158
+ },
159
+ slots: {},
160
+ })
161
+
162
+ await wrapper.vm.$nextTick()
163
+
164
+ expect(wrapper.find('input').attributes('autocomplete')).toBe('off')
165
+ })
166
+
167
+ it('applies autocomplete="on" when autocomplete is true', async () => {
168
+ const wrapper = mount(FzInput, {
169
+ props: {
170
+ label: 'Label',
171
+ autocomplete: true,
172
+ },
173
+ slots: {},
174
+ })
175
+
176
+ await wrapper.vm.$nextTick()
177
+
178
+ expect(wrapper.find('input').attributes('autocomplete')).toBe('on')
179
+ })
180
+ })
181
+
182
+ describe('Events', () => {
183
+ it('emits fzinput:right-icon-click event', async () => {
184
+ const wrapper = mount(FzInput, {
185
+ props: {
186
+ label: 'Label',
187
+ rightIcon: 'eye',
188
+ },
189
+ slots: {},
190
+ })
191
+
192
+ await wrapper.find('.fa-eye').trigger('click')
193
+
194
+ expect(wrapper.emitted('fzinput:right-icon-click')).toBeTruthy()
195
+ })
196
+
197
+ it('emits fzinput:left-icon-click event', async () => {
198
+ const wrapper = mount(FzInput, {
199
+ props: {
200
+ label: 'Label',
201
+ leftIcon: 'eye',
202
+ },
203
+ slots: {},
204
+ })
205
+
206
+ await wrapper.find('.fa-eye').trigger('click')
207
+
208
+ expect(wrapper.emitted('fzinput:left-icon-click')).toBeTruthy()
209
+ })
210
+
211
+ it('does not emit fzinput:right-icon-click event when disabled', async () => {
212
+ const wrapper = mount(FzInput, {
213
+ props: {
214
+ label: 'Label',
215
+ rightIcon: 'eye',
216
+ disabled: true,
217
+ },
218
+ slots: {},
219
+ })
220
+
221
+ await wrapper.find('.fa-eye').trigger('click')
222
+
223
+ expect(wrapper.emitted('fzinput:right-icon-click')).toBeFalsy()
224
+ })
225
+
226
+ it('does not emit fzinput:right-icon-click event when readonly', async () => {
227
+ const wrapper = mount(FzInput, {
228
+ props: {
229
+ label: 'Label',
230
+ rightIcon: 'eye',
231
+ readonly: true,
232
+ },
233
+ slots: {},
234
+ })
235
+
236
+ await wrapper.find('.fa-eye').trigger('click')
237
+
238
+ expect(wrapper.emitted('fzinput:right-icon-click')).toBeFalsy()
239
+ })
240
+
241
+ it('does not emit fzinput:left-icon-click event when disabled', async () => {
242
+ const wrapper = mount(FzInput, {
243
+ props: {
244
+ label: 'Label',
245
+ leftIcon: 'eye',
246
+ disabled: true,
247
+ },
248
+ slots: {},
249
+ })
250
+
251
+ await wrapper.find('.fa-eye').trigger('click')
252
+
253
+ expect(wrapper.emitted('fzinput:left-icon-click')).toBeFalsy()
254
+ })
255
+
256
+ it('does not emit fzinput:left-icon-click event when readonly', async () => {
257
+ const wrapper = mount(FzInput, {
258
+ props: {
259
+ label: 'Label',
260
+ leftIcon: 'eye',
261
+ readonly: true,
262
+ },
263
+ slots: {},
264
+ })
265
+
266
+ await wrapper.find('.fa-eye').trigger('click')
267
+
268
+ expect(wrapper.emitted('fzinput:left-icon-click')).toBeFalsy()
269
+ })
270
+
271
+ it('does not emit fzinput:second-right-icon-click event when disabled', async () => {
272
+ const wrapper = mount(FzInput, {
273
+ props: {
274
+ label: 'Label',
275
+ secondRightIcon: 'eye',
276
+ disabled: true,
277
+ },
278
+ slots: {},
279
+ })
280
+
281
+ await wrapper.find('.fa-eye').trigger('click')
282
+
283
+ expect(wrapper.emitted('fzinput:second-right-icon-click')).toBeFalsy()
284
+ })
285
+
286
+ it('does not emit fzinput:second-right-icon-click event when readonly', async () => {
287
+ const wrapper = mount(FzInput, {
288
+ props: {
289
+ label: 'Label',
290
+ secondRightIcon: 'eye',
291
+ readonly: true,
292
+ },
293
+ slots: {},
294
+ })
295
+
296
+ await wrapper.find('.fa-eye').trigger('click')
297
+
298
+ expect(wrapper.emitted('fzinput:second-right-icon-click')).toBeFalsy()
299
+ })
300
+
301
+ it('does not emit fzinput:right-icon-click event when disabled and rightIconButton is true', async () => {
302
+ const wrapper = mount(FzInput, {
303
+ props: {
304
+ label: 'Label',
305
+ rightIcon: 'eye',
306
+ rightIconButton: true,
307
+ disabled: true,
308
+ },
309
+ slots: {},
310
+ })
311
+
312
+ const button = wrapper.findComponent({ name: 'FzIconButton' })
313
+ await button.trigger('click')
314
+
315
+ expect(wrapper.emitted('fzinput:right-icon-click')).toBeFalsy()
316
+ })
317
+
318
+ it('does not emit fzinput:second-right-icon-click event when disabled and secondRightIconButton is true', async () => {
319
+ const wrapper = mount(FzInput, {
320
+ props: {
321
+ label: 'Label',
322
+ secondRightIcon: 'eye',
323
+ secondRightIconButton: true,
324
+ disabled: true,
325
+ },
326
+ slots: {},
327
+ })
328
+
329
+ const buttons = wrapper.findAllComponents({ name: 'FzIconButton' })
330
+ await buttons[0].trigger('click')
331
+
332
+ expect(wrapper.emitted('fzinput:second-right-icon-click')).toBeFalsy()
333
+ })
334
+ })
335
+
336
+ describe('Accessibility', () => {
337
+ describe('Input ARIA attributes', () => {
338
+ it('applies aria-required when required is true', async () => {
339
+ const wrapper = mount(FzInput, {
340
+ props: {
341
+ label: 'Label',
342
+ required: true,
343
+ },
344
+ slots: {},
345
+ })
346
+
347
+ await wrapper.vm.$nextTick()
348
+
349
+ const input = wrapper.find('input').element as HTMLInputElement
350
+ expect(input.getAttribute('aria-required')).toBe('true')
351
+ })
352
+
353
+ it('applies aria-required="false" when required is false', async () => {
354
+ const wrapper = mount(FzInput, {
355
+ props: {
356
+ label: 'Label',
357
+ required: false,
358
+ },
359
+ slots: {},
360
+ })
361
+
362
+ await wrapper.vm.$nextTick()
363
+
364
+ const input = wrapper.find('input').element as HTMLInputElement
365
+ expect(input.getAttribute('aria-required')).toBe('false')
366
+ })
367
+
368
+ it('applies aria-invalid when error is true', async () => {
369
+ const wrapper = mount(FzInput, {
370
+ props: {
371
+ label: 'Label',
372
+ error: true,
373
+ },
374
+ slots: {
375
+ errorMessage: 'Error message',
376
+ },
377
+ })
378
+
379
+ await wrapper.vm.$nextTick()
380
+
381
+ const input = wrapper.find('input').element as HTMLInputElement
382
+ expect(input.getAttribute('aria-invalid')).toBe('true')
383
+ })
384
+
385
+ it('applies aria-invalid="false" when error is false', async () => {
386
+ const wrapper = mount(FzInput, {
387
+ props: {
388
+ label: 'Label',
389
+ error: false,
390
+ },
391
+ slots: {},
392
+ })
393
+
394
+ await wrapper.vm.$nextTick()
395
+
396
+ const input = wrapper.find('input').element as HTMLInputElement
397
+ expect(input.getAttribute('aria-invalid')).toBe('false')
398
+ })
399
+
400
+ it('applies aria-disabled when disabled is true', async () => {
401
+ const wrapper = mount(FzInput, {
402
+ props: {
403
+ label: 'Label',
404
+ disabled: true,
405
+ },
406
+ slots: {},
407
+ })
408
+
409
+ await wrapper.vm.$nextTick()
410
+
411
+ const input = wrapper.find('input').element as HTMLInputElement
412
+ expect(input.getAttribute('aria-disabled')).toBe('true')
413
+ })
414
+
415
+ it('applies aria-disabled="false" when disabled is false', async () => {
416
+ const wrapper = mount(FzInput, {
417
+ props: {
418
+ label: 'Label',
419
+ disabled: false,
420
+ },
421
+ slots: {},
422
+ })
423
+
424
+ await wrapper.vm.$nextTick()
425
+
426
+ const input = wrapper.find('input').element as HTMLInputElement
427
+ expect(input.getAttribute('aria-disabled')).toBe('false')
428
+ })
429
+
430
+ it('applies aria-labelledby when label is provided', async () => {
431
+ const wrapper = mount(FzInput, {
432
+ props: {
433
+ label: 'Test Label',
434
+ },
435
+ slots: {},
436
+ })
437
+
438
+ await wrapper.vm.$nextTick()
439
+
440
+ const input = wrapper.find('input').element as HTMLInputElement
441
+ const labelId = input.getAttribute('aria-labelledby')
442
+ expect(labelId).toBeTruthy()
443
+
444
+ // Verify label element exists with matching id
445
+ const label = wrapper.find('label').element as HTMLLabelElement
446
+ expect(label.getAttribute('id')).toBe(labelId)
447
+ })
448
+
449
+ it('does not apply aria-labelledby when label is not provided', async () => {
450
+ const wrapper = mount(FzInput, {
451
+ props: {},
452
+ slots: {},
453
+ })
454
+
455
+ await wrapper.vm.$nextTick()
456
+
457
+ const input = wrapper.find('input').element as HTMLInputElement
458
+ expect(input.getAttribute('aria-labelledby')).toBeNull()
459
+ })
460
+
461
+ it('does not apply aria-labelledby when custom label slot is provided', async () => {
462
+ const wrapper = mount(FzInput, {
463
+ props: {
464
+ label: 'Test Label',
465
+ },
466
+ slots: {
467
+ label: () => 'Custom Label Slot',
468
+ },
469
+ })
470
+
471
+ await wrapper.vm.$nextTick()
472
+
473
+ const input = wrapper.find('input').element as HTMLInputElement
474
+ const ariaLabelledBy = input.getAttribute('aria-labelledby')
475
+
476
+ // aria-labelledby should not be set because the default label element
477
+ // with id="${uniqueId}-label" doesn't exist when custom slot is used
478
+ expect(ariaLabelledBy).toBeNull()
479
+
480
+ // Verify default label element is not rendered
481
+ const defaultLabel = wrapper.find('label')
482
+ expect(defaultLabel.exists()).toBe(false)
483
+ })
484
+
485
+ it('applies aria-describedby when helpText slot is provided', async () => {
486
+ const wrapper = mount(FzInput, {
487
+ props: {
488
+ label: 'Label',
489
+ },
490
+ slots: {
491
+ helpText: 'Help text',
492
+ },
493
+ })
494
+
495
+ await wrapper.vm.$nextTick()
496
+
497
+ const input = wrapper.find('input').element as HTMLInputElement
498
+ const describedBy = input.getAttribute('aria-describedby')
499
+ expect(describedBy).toBeTruthy()
500
+
501
+ // Verify help text element exists with matching id
502
+ const helpText = wrapper.find(`#${describedBy}`)
503
+ expect(helpText.exists()).toBe(true)
504
+ expect(helpText.text()).toContain('Help text')
505
+ })
506
+
507
+ it('applies aria-describedby when errorMessage slot is provided', async () => {
508
+ const wrapper = mount(FzInput, {
509
+ props: {
510
+ label: 'Label',
511
+ error: true,
512
+ },
513
+ slots: {
514
+ errorMessage: 'Error message',
515
+ },
516
+ })
517
+
518
+ await wrapper.vm.$nextTick()
519
+
520
+ const input = wrapper.find('input').element as HTMLInputElement
521
+ const describedBy = input.getAttribute('aria-describedby')
522
+ expect(describedBy).toBeTruthy()
523
+
524
+ // Verify error message element exists with matching id
525
+ const errorMessage = wrapper.find(`#${describedBy}`)
526
+ expect(errorMessage.exists()).toBe(true)
527
+ expect(errorMessage.text()).toContain('Error message')
528
+ })
529
+
530
+ it('does not apply aria-describedby when neither helpText nor errorMessage are provided', async () => {
531
+ const wrapper = mount(FzInput, {
532
+ props: {
533
+ label: 'Label',
534
+ },
535
+ slots: {},
536
+ })
537
+
538
+ await wrapper.vm.$nextTick()
539
+
540
+ const input = wrapper.find('input').element as HTMLInputElement
541
+ expect(input.getAttribute('aria-describedby')).toBeNull()
542
+ })
543
+ })
544
+
545
+ describe('Error message accessibility', () => {
546
+ it('applies role="alert" to error message container', async () => {
547
+ const wrapper = mount(FzInput, {
548
+ props: {
549
+ label: 'Label',
550
+ error: true,
551
+ },
552
+ slots: {
553
+ errorMessage: 'Error message',
554
+ },
555
+ })
556
+
557
+ await wrapper.vm.$nextTick()
558
+
559
+ const errorContainer = wrapper.find('[role="alert"]')
560
+ expect(errorContainer.exists()).toBe(true)
561
+ expect(errorContainer.text()).toContain('Error message')
562
+ })
563
+
564
+ it('does not render error container when error is false', async () => {
565
+ const wrapper = mount(FzInput, {
566
+ props: {
567
+ label: 'Label',
568
+ error: false,
569
+ },
570
+ slots: {
571
+ errorMessage: 'Error message',
572
+ },
573
+ })
574
+
575
+ await wrapper.vm.$nextTick()
576
+
577
+ const errorContainer = wrapper.find('[role="alert"]')
578
+ expect(errorContainer.exists()).toBe(false)
579
+ })
580
+ })
581
+
582
+ describe('Decorative icons accessibility', () => {
583
+ it('applies aria-hidden="true" to valid checkmark icon', async () => {
584
+ const wrapper = mount(FzInput, {
585
+ props: {
586
+ label: 'Label',
587
+ valid: true,
588
+ },
589
+ slots: {},
590
+ })
591
+
592
+ await wrapper.vm.$nextTick()
593
+
594
+ // Find the check icon (FzIcon with name="check")
595
+ const checkIcons = wrapper.findAllComponents({ name: 'FzIcon' })
596
+ const checkIcon = checkIcons.find((icon) => icon.props('name') === 'check')
597
+
598
+ expect(checkIcon?.exists()).toBe(true)
599
+ const rootElement = checkIcon?.element as HTMLElement
600
+ expect(rootElement.getAttribute('aria-hidden')).toBe('true')
601
+ })
602
+
603
+ it('applies aria-hidden="true" to error icon', async () => {
604
+ const wrapper = mount(FzInput, {
605
+ props: {
606
+ label: 'Label',
607
+ error: true,
608
+ },
609
+ slots: {
610
+ errorMessage: 'Error message',
611
+ },
612
+ })
613
+
614
+ await wrapper.vm.$nextTick()
615
+
616
+ // Find the error icon (FzIcon with name="circle-xmark")
617
+ const errorIcons = wrapper.findAllComponents({ name: 'FzIcon' })
618
+ const errorIcon = errorIcons.find((icon) => icon.props('name') === 'circle-xmark')
619
+
620
+ expect(errorIcon?.exists()).toBe(true)
621
+ const rootElement = errorIcon?.element as HTMLElement
622
+ expect(rootElement.getAttribute('aria-hidden')).toBe('true')
623
+ })
624
+ })
625
+
626
+ describe('Container accessibility', () => {
627
+ it('applies tabindex="0" to container when not disabled', async () => {
628
+ const wrapper = mount(FzInput, {
629
+ props: {
630
+ label: 'Label',
631
+ },
632
+ slots: {},
633
+ })
634
+
635
+ await wrapper.vm.$nextTick()
636
+
637
+ const container = wrapper.find('.fz-input > div').element as HTMLElement
638
+ expect(container.getAttribute('tabindex')).toBe('0')
639
+ })
640
+
641
+ it('removes tabindex from container when disabled', async () => {
642
+ const wrapper = mount(FzInput, {
643
+ props: {
644
+ label: 'Label',
645
+ disabled: true,
646
+ },
647
+ slots: {},
648
+ })
649
+
650
+ await wrapper.vm.$nextTick()
651
+
652
+ const container = wrapper.find('.fz-input > div').element as HTMLElement
653
+ expect(container.getAttribute('tabindex')).toBeNull()
654
+ })
655
+
656
+ it('removes tabindex from container when readonly', async () => {
657
+ const wrapper = mount(FzInput, {
658
+ props: {
659
+ label: 'Label',
660
+ readonly: true,
661
+ },
662
+ slots: {},
663
+ })
664
+
665
+ await wrapper.vm.$nextTick()
666
+
667
+ const container = wrapper.find('.fz-input > div').element as HTMLElement
668
+ expect(container.getAttribute('tabindex')).toBeNull()
669
+ })
670
+ })
671
+
672
+ describe('Left icon accessibility', () => {
673
+ it('applies accessibility attributes when leftIconAriaLabel is provided', async () => {
674
+ const wrapper = mount(FzInput, {
675
+ props: {
676
+ label: 'Label',
677
+ leftIcon: 'calendar-lines',
678
+ leftIconAriaLabel: 'Open calendar',
679
+ },
680
+ slots: {},
681
+ })
682
+
683
+ await wrapper.vm.$nextTick()
684
+
685
+ // Find the FzIcon component wrapper div (root element)
686
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
687
+ expect(iconComponent.exists()).toBe(true)
688
+
689
+ // Get the root element (div wrapper)
690
+ const rootElement = iconComponent.element as HTMLElement
691
+
692
+ // Verify attributes are on the root element
693
+ expect(rootElement.getAttribute('role')).toBe('button')
694
+ expect(rootElement.getAttribute('aria-label')).toBe('Open calendar')
695
+ expect(rootElement.getAttribute('tabindex')).toBe('0')
696
+ })
697
+
698
+ it('does not apply accessibility attributes when leftIconAriaLabel is not provided', async () => {
699
+ const wrapper = mount(FzInput, {
700
+ props: {
701
+ label: 'Label',
702
+ leftIcon: 'calendar-lines',
703
+ },
704
+ slots: {},
705
+ })
706
+
707
+ await wrapper.vm.$nextTick()
708
+
709
+ // Find the FzIcon component wrapper
710
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
711
+ expect(iconComponent.exists()).toBe(true)
712
+
713
+ // Get the root element (div wrapper)
714
+ const rootElement = iconComponent.element as HTMLElement
715
+
716
+ // Verify attributes are not on the root element
717
+ expect(rootElement.getAttribute('role')).toBeNull()
718
+ expect(rootElement.getAttribute('aria-label')).toBeNull()
719
+ expect(rootElement.getAttribute('tabindex')).toBeNull()
720
+ })
721
+
722
+ it('removes tabindex when disabled and leftIconAriaLabel is provided', async () => {
723
+ const wrapper = mount(FzInput, {
724
+ props: {
725
+ label: 'Label',
726
+ leftIcon: 'calendar-lines',
727
+ leftIconAriaLabel: 'Open calendar',
728
+ disabled: true,
729
+ },
730
+ slots: {},
731
+ })
732
+
733
+ await wrapper.vm.$nextTick()
734
+
735
+ // Find the FzIcon component wrapper
736
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
737
+ expect(iconComponent.exists()).toBe(true)
738
+
739
+ // Get the root element (div wrapper)
740
+ const rootElement = iconComponent.element as HTMLElement
741
+
742
+ // Verify attributes are on the root element
743
+ expect(rootElement.getAttribute('role')).toBe('button')
744
+ expect(rootElement.getAttribute('aria-label')).toBe('Open calendar')
745
+ expect(rootElement.getAttribute('tabindex')).toBeNull() // Removed when disabled
746
+ expect(rootElement.getAttribute('aria-disabled')).toBe('true')
747
+ })
748
+
749
+ it('has keyboard handler when leftIconAriaLabel is provided', async () => {
750
+ const wrapper = mount(FzInput, {
751
+ props: {
752
+ label: 'Label',
753
+ leftIcon: 'calendar-lines',
754
+ leftIconAriaLabel: 'Open calendar',
755
+ },
756
+ slots: {},
757
+ })
758
+
759
+ await wrapper.vm.$nextTick()
760
+
761
+ // Find the FzIcon component wrapper
762
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
763
+ expect(iconComponent.exists()).toBe(true)
764
+
765
+ // Get the root element (div wrapper)
766
+ const rootElement = iconComponent.element as HTMLElement
767
+
768
+ // Verify icon is keyboard accessible (has tabindex)
769
+ expect(rootElement.getAttribute('tabindex')).toBe('0')
770
+ // Keyboard interaction is tested in Storybook play functions
771
+ })
772
+ })
773
+
774
+ describe('Right icon accessibility', () => {
775
+ it('applies accessibility attributes when rightIconAriaLabel is provided', async () => {
776
+ const wrapper = mount(FzInput, {
777
+ props: {
778
+ label: 'Label',
779
+ rightIcon: 'eye',
780
+ rightIconAriaLabel: 'Toggle visibility',
781
+ },
782
+ slots: {},
783
+ })
784
+
785
+ await wrapper.vm.$nextTick()
786
+
787
+ // Find the FzIcon component wrapper (not the inner img)
788
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
789
+ expect(iconComponent.exists()).toBe(true)
790
+
791
+ // Get the root element (div wrapper)
792
+ const rootElement = iconComponent.element as HTMLElement
793
+
794
+ // Verify attributes are on the root element
795
+ expect(rootElement.getAttribute('role')).toBe('button')
796
+ expect(rootElement.getAttribute('aria-label')).toBe('Toggle visibility')
797
+ expect(rootElement.getAttribute('tabindex')).toBe('0')
798
+ })
799
+
800
+ it('does not apply accessibility attributes when rightIconAriaLabel is not provided', async () => {
801
+ const wrapper = mount(FzInput, {
802
+ props: {
803
+ label: 'Label',
804
+ rightIcon: 'eye',
805
+ },
806
+ slots: {},
807
+ })
808
+
809
+ await wrapper.vm.$nextTick()
810
+
811
+ // Find the FzIcon component wrapper
812
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
813
+ expect(iconComponent.exists()).toBe(true)
814
+
815
+ // Get the root element (div wrapper)
816
+ const rootElement = iconComponent.element as HTMLElement
817
+
818
+ // Verify attributes are not on the root element
819
+ expect(rootElement.getAttribute('role')).toBeNull()
820
+ expect(rootElement.getAttribute('aria-label')).toBeNull()
821
+ expect(rootElement.getAttribute('tabindex')).toBeNull()
822
+ })
823
+
824
+ it('removes tabindex when disabled and rightIconAriaLabel is provided', async () => {
825
+ const wrapper = mount(FzInput, {
826
+ props: {
827
+ label: 'Label',
828
+ rightIcon: 'eye',
829
+ rightIconAriaLabel: 'Toggle visibility',
830
+ disabled: true,
831
+ },
832
+ slots: {},
833
+ })
834
+
835
+ await wrapper.vm.$nextTick()
836
+
837
+ // Find the FzIcon component wrapper
838
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
839
+ expect(iconComponent.exists()).toBe(true)
840
+
841
+ // Get the root element (div wrapper)
842
+ const rootElement = iconComponent.element as HTMLElement
843
+
844
+ // Verify attributes are on the root element
845
+ expect(rootElement.getAttribute('role')).toBe('button')
846
+ expect(rootElement.getAttribute('aria-label')).toBe('Toggle visibility')
847
+ expect(rootElement.getAttribute('tabindex')).toBeNull() // Removed when disabled
848
+ expect(rootElement.getAttribute('aria-disabled')).toBe('true')
849
+ })
850
+
851
+ it('removes tabindex when readonly and rightIconAriaLabel is provided', async () => {
852
+ const wrapper = mount(FzInput, {
853
+ props: {
854
+ label: 'Label',
855
+ rightIcon: 'eye',
856
+ rightIconAriaLabel: 'Toggle visibility',
857
+ readonly: true,
858
+ },
859
+ slots: {},
860
+ })
861
+
862
+ await wrapper.vm.$nextTick()
863
+
864
+ // Find the FzIcon component wrapper
865
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
866
+ expect(iconComponent.exists()).toBe(true)
867
+
868
+ // Get the root element (div wrapper)
869
+ const rootElement = iconComponent.element as HTMLElement
870
+
871
+ // Verify attributes are on the root element
872
+ expect(rootElement.getAttribute('role')).toBe('button')
873
+ expect(rootElement.getAttribute('aria-label')).toBe('Toggle visibility')
874
+ expect(rootElement.getAttribute('tabindex')).toBeNull() // Removed when readonly
875
+ expect(rootElement.getAttribute('aria-disabled')).toBe('true')
876
+ })
877
+
878
+ it('does not apply accessibility attributes when rightIconButton is true', async () => {
879
+ const wrapper = mount(FzInput, {
880
+ props: {
881
+ label: 'Label',
882
+ rightIcon: 'eye',
883
+ rightIconButton: true,
884
+ rightIconAriaLabel: 'Toggle visibility',
885
+ },
886
+ slots: {},
887
+ })
888
+
889
+ await wrapper.vm.$nextTick()
890
+
891
+ // When rightIconButton is true, FzIconButton is used instead of FzIcon
892
+ const iconButton = wrapper.findComponent({ name: 'FzIconButton' })
893
+ expect(iconButton.exists()).toBe(true)
894
+ })
895
+
896
+ it('has keyboard handler when rightIconAriaLabel is provided', async () => {
897
+ const wrapper = mount(FzInput, {
898
+ props: {
899
+ label: 'Label',
900
+ rightIcon: 'eye',
901
+ rightIconAriaLabel: 'Toggle visibility',
902
+ },
903
+ slots: {},
904
+ })
905
+
906
+ await wrapper.vm.$nextTick()
907
+
908
+ // Find the FzIcon component wrapper
909
+ const iconComponent = wrapper.findComponent({ name: 'FzIcon' })
910
+ expect(iconComponent.exists()).toBe(true)
911
+
912
+ // Get the root element (div wrapper)
913
+ const rootElement = iconComponent.element as HTMLElement
914
+
915
+ // Verify icon is keyboard accessible (has tabindex)
916
+ expect(rootElement.getAttribute('tabindex')).toBe('0')
917
+ // Keyboard interaction is tested in Storybook play functions
918
+ })
919
+ })
920
+
921
+ describe('Second right icon accessibility', () => {
922
+ it('applies accessibility attributes when secondRightIconAriaLabel is provided', async () => {
923
+ const wrapper = mount(FzInput, {
924
+ props: {
925
+ label: 'Label',
926
+ secondRightIcon: 'info-circle',
927
+ secondRightIconAriaLabel: 'Show information',
928
+ },
929
+ slots: {},
930
+ })
931
+
932
+ await wrapper.vm.$nextTick()
933
+
934
+ // Find all FzIcon components and get the one with secondRightIcon
935
+ const iconComponents = wrapper.findAllComponents({ name: 'FzIcon' })
936
+ const secondIconComponent = iconComponents.find((icon) =>
937
+ icon.props('name') === 'info-circle'
938
+ )
939
+
940
+ expect(secondIconComponent?.exists()).toBe(true)
941
+
942
+ // Get the root element (div wrapper)
943
+ const rootElement = secondIconComponent?.element as HTMLElement
944
+
945
+ // Verify attributes are on the root element
946
+ expect(rootElement.getAttribute('role')).toBe('button')
947
+ expect(rootElement.getAttribute('aria-label')).toBe('Show information')
948
+ expect(rootElement.getAttribute('tabindex')).toBe('0')
949
+ })
950
+
951
+ it('removes tabindex when readonly and secondRightIconAriaLabel is provided', async () => {
952
+ const wrapper = mount(FzInput, {
953
+ props: {
954
+ label: 'Label',
955
+ secondRightIcon: 'info-circle',
956
+ secondRightIconAriaLabel: 'Show information',
957
+ readonly: true,
958
+ },
959
+ slots: {},
960
+ })
961
+
962
+ await wrapper.vm.$nextTick()
963
+
964
+ // Find all FzIcon components and get the one with secondRightIcon
965
+ const iconComponents = wrapper.findAllComponents({ name: 'FzIcon' })
966
+ const secondIconComponent = iconComponents.find((icon) =>
967
+ icon.props('name') === 'info-circle'
968
+ )
969
+
970
+ expect(secondIconComponent?.exists()).toBe(true)
971
+
972
+ // Get the root element (div wrapper)
973
+ const rootElement = secondIconComponent?.element as HTMLElement
974
+
975
+ // Verify attributes are on the root element
976
+ expect(rootElement.getAttribute('role')).toBe('button')
977
+ expect(rootElement.getAttribute('aria-label')).toBe('Show information')
978
+ expect(rootElement.getAttribute('tabindex')).toBeNull() // Removed when readonly
979
+ expect(rootElement.getAttribute('aria-disabled')).toBe('true')
980
+ })
981
+ })
982
+
983
+ describe('Right icons order', () => {
984
+ it('renders valid checkmark as last icon when all icons are present', async () => {
985
+ const wrapper = mount(FzInput, {
986
+ props: {
987
+ label: 'Label',
988
+ valid: true,
989
+ secondRightIcon: 'info-circle',
990
+ rightIcon: 'envelope',
991
+ },
992
+ slots: {},
993
+ })
994
+
995
+ await wrapper.vm.$nextTick()
996
+
997
+ // Find all FzIcon components in the right-icon slot
998
+ const iconComponents = wrapper.findAllComponents({ name: 'FzIcon' })
999
+ const validIcon = iconComponents.find((icon) => icon.props('name') === 'check')
1000
+ const secondIcon = iconComponents.find((icon) => icon.props('name') === 'info-circle')
1001
+ const rightIcon = iconComponents.find((icon) => icon.props('name') === 'envelope')
1002
+
1003
+ expect(validIcon?.exists()).toBe(true)
1004
+ expect(secondIcon?.exists()).toBe(true)
1005
+ expect(rightIcon?.exists()).toBe(true)
1006
+
1007
+ // Get the container div that wraps all right icons
1008
+ const rightIconContainer = wrapper.find('.fz-input > div > div.flex.items-center.gap-4')
1009
+ expect(rightIconContainer.exists()).toBe(true)
1010
+
1011
+ // Get all icon elements in order
1012
+ const icons = rightIconContainer.findAllComponents({ name: 'FzIcon' })
1013
+ expect(icons.length).toBeGreaterThanOrEqual(3)
1014
+
1015
+ // Verify order: secondRightIcon, rightIcon, valid (check)
1016
+ const iconNames = icons.map((icon) => icon.props('name'))
1017
+ const secondIndex = iconNames.indexOf('info-circle')
1018
+ const rightIndex = iconNames.indexOf('envelope')
1019
+ const validIndex = iconNames.indexOf('check')
1020
+
1021
+ expect(secondIndex).toBeLessThan(rightIndex)
1022
+ expect(rightIndex).toBeLessThan(validIndex)
1023
+ })
1024
+ })
1025
+ })
1026
+
1027
+ describe('Edge cases', () => {
1028
+ it(`renders ${NUMBER_OF_INPUTS} input with different ids`, async () => {
1029
+ const wrapperList = Array.from({ length: NUMBER_OF_INPUTS }).map((_, i) =>
1030
+ mount(FzInput, {
1031
+ props: {
1032
+ label: `Label ${i}`,
1033
+ },
1034
+ slots: {},
1035
+ }),
1036
+ )
1037
+
1038
+ await Promise.all(wrapperList.map((w) => w.vm.$nextTick()))
1039
+
1040
+ const ids = wrapperList.map((w) => w.find('input').attributes('id'))
1041
+
1042
+ expect(new Set(ids).size).toBe(NUMBER_OF_INPUTS)
1043
+ })
1044
+ })
1045
+ })
1046
+