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