@khanacademy/wonder-blocks-form 4.7.5 → 4.8.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,1084 @@
1
+ /* eslint-disable max-lines */
2
+ import * as React from "react";
3
+ import {render, screen} from "@testing-library/react";
4
+
5
+ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
6
+ import {userEvent} from "@testing-library/user-event";
7
+ import TextArea from "../text-area";
8
+
9
+ const defaultOptions = {
10
+ wrapper: RenderStateRoot,
11
+ };
12
+
13
+ const wrapOptions: Array<"soft" | "hard" | "off"> = ["soft", "hard", "off"];
14
+
15
+ describe("TextArea", () => {
16
+ describe("Attributes", () => {
17
+ it("should use the id prop for the textarea", async () => {
18
+ // Arrange
19
+ const testId = "test-id";
20
+ // Act
21
+ render(
22
+ <TextArea id={testId} value="" onChange={() => {}} />,
23
+ defaultOptions,
24
+ );
25
+
26
+ // Assert
27
+ const textArea = await screen.findByRole("textbox");
28
+ expect(textArea).toHaveAttribute("id", testId);
29
+ });
30
+
31
+ it("should use an auto-generated id for the textarea when id prop is not set", async () => {
32
+ // Arrange
33
+
34
+ // Act
35
+ render(<TextArea value="" onChange={() => {}} />, defaultOptions);
36
+
37
+ // Assert
38
+ // Since the generated id is unique, we cannot know what it will be. We
39
+ // only test if the id attribute starts with "uid-", then followed by
40
+ // "text-field-" as the scope assigned to IDProvider.
41
+ const textArea = await screen.findByRole("textbox");
42
+ expect(textArea.getAttribute("id")).toMatch(/uid-text-area-.*$/);
43
+ });
44
+
45
+ it("should use the testId prop for the textarea element", async () => {
46
+ // Arrange
47
+ const testId = "test-id";
48
+ render(
49
+ <TextArea value="Text" onChange={() => {}} testId={testId} />,
50
+ defaultOptions,
51
+ );
52
+
53
+ // Act
54
+
55
+ // Assert
56
+ const textArea = await screen.findByRole("textbox");
57
+ expect(textArea).toHaveAttribute("data-testid", testId);
58
+ });
59
+
60
+ it("should set the placeholder when the prop when provided", async () => {
61
+ // Arrange
62
+ const placeholder = "Test placeholder";
63
+ render(
64
+ <TextArea
65
+ placeholder={placeholder}
66
+ value="Text"
67
+ onChange={() => {}}
68
+ />,
69
+ defaultOptions,
70
+ );
71
+
72
+ // Act
73
+
74
+ // Assert
75
+ const textArea = await screen.findByRole("textbox");
76
+ expect(textArea).toHaveAttribute("placeholder", placeholder);
77
+ });
78
+
79
+ it("should set the disabled attribute when the disabled prop is true", async () => {
80
+ // Arrange
81
+ render(
82
+ <TextArea disabled={true} value="Text" onChange={() => {}} />,
83
+ defaultOptions,
84
+ );
85
+
86
+ // Act
87
+
88
+ // Assert
89
+ const textArea = await screen.findByRole("textbox");
90
+ expect(textArea).toBeDisabled();
91
+ });
92
+
93
+ it("should set the readonly attribute when the readOnly prop is provided", async () => {
94
+ // Arrange
95
+ render(
96
+ <TextArea value="Text" onChange={() => {}} readOnly={true} />,
97
+ defaultOptions,
98
+ );
99
+
100
+ // Act
101
+
102
+ // Assert
103
+ const textArea = await screen.findByRole("textbox");
104
+ expect(textArea).toHaveAttribute("readonly");
105
+ });
106
+
107
+ it("should set the autocomplete attribute when the autoComplete prop is provided", async () => {
108
+ // Arrange
109
+ render(
110
+ <TextArea value="Text" onChange={() => {}} autoComplete="on" />,
111
+ defaultOptions,
112
+ );
113
+
114
+ // Act
115
+
116
+ // Assert
117
+ const textArea = await screen.findByRole("textbox");
118
+ expect(textArea).toHaveAttribute("autocomplete", "on");
119
+ });
120
+
121
+ it("should set the name attribute when the name prop is provided", async () => {
122
+ // Arrange
123
+ const name = "Test name";
124
+ render(
125
+ <TextArea value="Text" onChange={() => {}} name={name} />,
126
+ defaultOptions,
127
+ );
128
+
129
+ // Act
130
+
131
+ // Assert
132
+ const textArea = await screen.findByRole("textbox");
133
+ expect(textArea).toHaveAttribute("name", name);
134
+ });
135
+
136
+ it("should set the class when the className prop is provided", async () => {
137
+ // Arrange
138
+ const className = "Test class name";
139
+ render(
140
+ <TextArea
141
+ value="Text"
142
+ onChange={() => {}}
143
+ className={className}
144
+ />,
145
+ defaultOptions,
146
+ );
147
+
148
+ // Act
149
+
150
+ // Assert
151
+ const textArea = await screen.findByRole("textbox");
152
+ expect(textArea).toHaveClass(className);
153
+ });
154
+
155
+ it("should set the rows attribute when the rows prop is provided", async () => {
156
+ // Arrange
157
+ const rows = 10;
158
+ render(
159
+ <TextArea value="Text" onChange={() => {}} rows={rows} />,
160
+ defaultOptions,
161
+ );
162
+
163
+ // Act
164
+
165
+ // Assert
166
+ const textArea = await screen.findByRole("textbox");
167
+ expect(textArea).toHaveAttribute("rows", `${rows}`);
168
+ });
169
+
170
+ it("should set the spellcheck attribute when spellCheck prop is set to true", async () => {
171
+ // Arrange
172
+ render(
173
+ <TextArea value="Text" onChange={() => {}} spellCheck={true} />,
174
+ defaultOptions,
175
+ );
176
+
177
+ // Act
178
+
179
+ // Assert
180
+ const textArea = await screen.findByRole("textbox");
181
+ expect(textArea).toHaveAttribute("spellcheck", "true");
182
+ });
183
+
184
+ it("should set the spellcheck attribute when spellCheck prop is set to false", async () => {
185
+ // Arrange
186
+ render(
187
+ <TextArea
188
+ value="Text"
189
+ onChange={() => {}}
190
+ spellCheck={false}
191
+ />,
192
+ defaultOptions,
193
+ );
194
+
195
+ // Act
196
+
197
+ // Assert
198
+ const textArea = await screen.findByRole("textbox");
199
+ expect(textArea).toHaveAttribute("spellcheck", "false");
200
+ });
201
+
202
+ it.each(wrapOptions)(
203
+ "should set the wrap attribute when the spellCheck prop is set to '%s' ",
204
+ async (wrap) => {
205
+ // Arrange
206
+ render(
207
+ <TextArea value="Text" onChange={() => {}} wrap={wrap} />,
208
+ defaultOptions,
209
+ );
210
+ // Act
211
+
212
+ // Assert
213
+ const textArea = await screen.findByRole("textbox");
214
+ expect(textArea).toHaveAttribute("wrap", wrap);
215
+ },
216
+ );
217
+
218
+ it("should set the minlength attribute when the minLength prop is used", async () => {
219
+ // Arrange
220
+ const minLength = 3;
221
+ render(
222
+ <TextArea
223
+ value="Text"
224
+ onChange={() => {}}
225
+ minLength={minLength}
226
+ />,
227
+ defaultOptions,
228
+ );
229
+
230
+ // Act
231
+
232
+ // Assert
233
+ const textArea = await screen.findByRole("textbox");
234
+ expect(textArea).toHaveAttribute("minlength", `${minLength}`);
235
+ });
236
+
237
+ it("should set the maxlength attribute when the maxLength prop is used", async () => {
238
+ // Arrange
239
+ const maxLength = 3;
240
+ render(
241
+ <TextArea
242
+ value="Text"
243
+ onChange={() => {}}
244
+ maxLength={maxLength}
245
+ />,
246
+ defaultOptions,
247
+ );
248
+
249
+ // Act
250
+
251
+ // Assert
252
+ const textArea = await screen.findByRole("textbox");
253
+ expect(textArea).toHaveAttribute("maxlength", `${maxLength}`);
254
+ });
255
+
256
+ it("should set the required attribute when the required prop is used", async () => {
257
+ // Arrange
258
+ render(
259
+ <TextArea value="Text" onChange={() => {}} required={true} />,
260
+ defaultOptions,
261
+ );
262
+
263
+ // Act
264
+
265
+ // Assert
266
+ const textArea = await screen.findByRole("textbox");
267
+ expect(textArea).toHaveAttribute("required");
268
+ });
269
+ });
270
+
271
+ it("should use the value prop", async () => {
272
+ // Arrange
273
+ const testValue = "test value";
274
+ render(
275
+ <TextArea value={testValue} onChange={() => {}} />,
276
+ defaultOptions,
277
+ );
278
+
279
+ // Act
280
+
281
+ // Assert
282
+ const textArea = await screen.findByRole("textbox");
283
+ expect(textArea).toHaveValue(testValue);
284
+ });
285
+
286
+ it("should forward the ref to the textarea element", async () => {
287
+ // Arrange
288
+ const ref = React.createRef<HTMLTextAreaElement>();
289
+ render(
290
+ <TextArea value="Text" onChange={() => {}} ref={ref} />,
291
+ defaultOptions,
292
+ );
293
+
294
+ // Act
295
+
296
+ // Assert
297
+ expect(ref.current).toBeInstanceOf(HTMLTextAreaElement);
298
+ expect(await screen.findByRole("textbox")).toBe(ref.current);
299
+ });
300
+
301
+ describe("Event Handlers", () => {
302
+ it("should call the onChange prop when the textarea value changes", async () => {
303
+ // Arrange
304
+ const onChangeMock = jest.fn();
305
+ render(
306
+ <TextArea value="" onChange={onChangeMock} />,
307
+ defaultOptions,
308
+ );
309
+
310
+ // Act
311
+ // Type one letter
312
+ const letterToType = "X";
313
+ await userEvent.type(
314
+ await screen.findByRole("textbox"),
315
+ letterToType,
316
+ );
317
+
318
+ // Assert
319
+ expect(onChangeMock).toHaveBeenCalledExactlyOnceWith(letterToType);
320
+ });
321
+
322
+ it("should call the onClick prop when the textarea is clicked", async () => {
323
+ // Arrange
324
+ const onClickMock = jest.fn();
325
+ render(
326
+ <TextArea value="" onChange={() => {}} onClick={onClickMock} />,
327
+ defaultOptions,
328
+ );
329
+
330
+ // Act
331
+ await userEvent.click(await screen.findByRole("textbox"));
332
+
333
+ // Assert
334
+ expect(onClickMock).toHaveBeenCalledOnce();
335
+ });
336
+
337
+ it("should call the onKeyDown prop when a key is typed in the textarea", async () => {
338
+ // Arrange
339
+ const handleOnKeyDown = jest.fn(
340
+ (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
341
+ return event.key;
342
+ },
343
+ );
344
+
345
+ render(
346
+ <TextArea
347
+ value=""
348
+ onChange={() => {}}
349
+ onKeyDown={handleOnKeyDown}
350
+ />,
351
+ defaultOptions,
352
+ );
353
+
354
+ // Act
355
+ await userEvent.type(await screen.findByRole("textbox"), "{enter}");
356
+
357
+ // Assert
358
+ expect(handleOnKeyDown).toHaveReturnedWith("Enter");
359
+ });
360
+
361
+ it("should call the onKeyUp prop when a key is typed in the textarea", async () => {
362
+ // Arrange
363
+ const handleOnKeyUp = jest.fn(
364
+ (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
365
+ return event.key;
366
+ },
367
+ );
368
+
369
+ render(
370
+ <TextArea
371
+ value=""
372
+ onChange={() => {}}
373
+ onKeyUp={handleOnKeyUp}
374
+ />,
375
+ defaultOptions,
376
+ );
377
+
378
+ // Act
379
+ await userEvent.type(await screen.findByRole("textbox"), "{enter}");
380
+
381
+ // Assert
382
+ expect(handleOnKeyUp).toHaveReturnedWith("Enter");
383
+ });
384
+
385
+ it("should call the onFocus prop when the textarea is focused", async () => {
386
+ // Arrange
387
+ const handleOnFocus = jest.fn();
388
+
389
+ render(
390
+ <TextArea
391
+ value=""
392
+ onChange={() => {}}
393
+ onFocus={handleOnFocus}
394
+ />,
395
+ defaultOptions,
396
+ );
397
+
398
+ // Act
399
+ await userEvent.tab();
400
+
401
+ // Assert
402
+ expect(handleOnFocus).toHaveBeenCalledOnce();
403
+ });
404
+
405
+ it("should call the onBlur prop when the textarea is blurred", async () => {
406
+ // Arrange
407
+ const handleOnBlur = jest.fn();
408
+
409
+ render(
410
+ <TextArea value="" onChange={() => {}} onBlur={handleOnBlur} />,
411
+ defaultOptions,
412
+ );
413
+ // Tab to focus on textarea
414
+ await userEvent.tab();
415
+
416
+ // Act
417
+ // Tab to move focus away
418
+ await userEvent.tab();
419
+
420
+ // Assert
421
+ expect(handleOnBlur).toHaveBeenCalledOnce();
422
+ });
423
+ });
424
+
425
+ describe("Accessibility", () => {
426
+ describe("Axe", () => {
427
+ test("has no accessibility violations", async () => {
428
+ // Arrange
429
+ // Use with label to demonstrate how it should be used with the
430
+ // TextArea component
431
+ const {container} = render(
432
+ <>
433
+ <label htmlFor="text-area">Test label</label>
434
+ <TextArea
435
+ value="Text"
436
+ onChange={() => {}}
437
+ id="text-area"
438
+ />
439
+ </>,
440
+ defaultOptions,
441
+ );
442
+ // Act
443
+
444
+ // Assert
445
+ await expect(container).toHaveNoA11yViolations();
446
+ });
447
+ });
448
+ describe("Focus", () => {
449
+ it("should focus on the textarea by default when the autoFocus prop is provided", async () => {
450
+ // Arrange
451
+ render(
452
+ <TextArea
453
+ value="Text"
454
+ onChange={() => {}}
455
+ autoFocus={true}
456
+ />,
457
+ defaultOptions,
458
+ );
459
+
460
+ // Act
461
+
462
+ // Assert
463
+ const textArea = await screen.findByRole("textbox");
464
+ expect(textArea).toHaveFocus();
465
+ });
466
+
467
+ it("should be focusable", async () => {
468
+ // Arrange
469
+ render(
470
+ <TextArea value="Text" onChange={() => {}} />,
471
+ defaultOptions,
472
+ );
473
+
474
+ // Act
475
+ await userEvent.tab();
476
+
477
+ // Assert
478
+ const textArea = await screen.findByRole("textbox");
479
+ expect(textArea).toHaveFocus();
480
+ });
481
+ });
482
+
483
+ describe("ARIA", () => {
484
+ it("should set the aria-label attribute when provided", async () => {
485
+ // Arrange
486
+ const ariaLabel = "Test Aria Label";
487
+ render(
488
+ <TextArea
489
+ value="Text"
490
+ onChange={() => {}}
491
+ aria-label={ariaLabel}
492
+ />,
493
+ defaultOptions,
494
+ );
495
+ // Act
496
+
497
+ // Assert
498
+ const textArea = await screen.findByRole("textbox");
499
+ expect(textArea).toHaveAttribute("aria-label", ariaLabel);
500
+ });
501
+
502
+ it("should set the aria-labelledby attribute when provided", async () => {
503
+ // Arrange
504
+ const ariaLabelledBy = "test-label-id";
505
+ render(
506
+ <TextArea
507
+ value="Text"
508
+ onChange={() => {}}
509
+ aria-labelledby={ariaLabelledBy}
510
+ />,
511
+ defaultOptions,
512
+ );
513
+ // Act
514
+
515
+ // Assert
516
+ const textArea = await screen.findByRole("textbox");
517
+ expect(textArea).toHaveAttribute(
518
+ "aria-labelledby",
519
+ ariaLabelledBy,
520
+ );
521
+ });
522
+
523
+ it("should set the aria-describedby attribute when provided", async () => {
524
+ // Arrange
525
+ const ariaDescribedBy = "test-label-id";
526
+ render(
527
+ <TextArea
528
+ value="Text"
529
+ onChange={() => {}}
530
+ aria-describedby={ariaDescribedBy}
531
+ />,
532
+ defaultOptions,
533
+ );
534
+ // Act
535
+
536
+ // Assert
537
+ const textArea = await screen.findByRole("textbox");
538
+ expect(textArea).toHaveAttribute(
539
+ "aria-describedby",
540
+ ariaDescribedBy,
541
+ );
542
+ });
543
+
544
+ it("should set the aria-details attribute when provided", async () => {
545
+ // Arrange
546
+ const ariaDetails = "details-id";
547
+ render(
548
+ <TextArea
549
+ value="Text"
550
+ onChange={() => {}}
551
+ aria-details={ariaDetails}
552
+ />,
553
+ defaultOptions,
554
+ );
555
+ // Act
556
+
557
+ // Assert
558
+ const textArea = await screen.findByRole("textbox");
559
+ expect(textArea).toHaveAttribute(
560
+ "aria-details",
561
+ `${ariaDetails}`,
562
+ );
563
+ });
564
+
565
+ it("should set aria-invalid to true if the validate prop returns an error message", async () => {
566
+ // Arrange
567
+ render(
568
+ <TextArea
569
+ value="text"
570
+ onChange={() => {}}
571
+ // If the validate function returns a string or true,
572
+ // then the text area is in an error state. For this
573
+ // test, we always return a string upon validation
574
+ // to trigger the error state. Since the textarea is
575
+ // being mounted with a non-empty value, it is validated
576
+ // on initial render. Because the text area is in
577
+ // an error state, it will have aria-invalid=true
578
+ validate={() => "Error"}
579
+ />,
580
+ defaultOptions,
581
+ );
582
+
583
+ // Act
584
+
585
+ // Assert
586
+ const textArea = await screen.findByRole("textbox");
587
+ expect(textArea).toHaveAttribute("aria-invalid", "true");
588
+ });
589
+ it("should set aria-invalid to true if the validate prop returns an error message", async () => {
590
+ // Arrange
591
+ render(
592
+ <TextArea
593
+ value="text"
594
+ onChange={() => {}}
595
+ validate={() => null}
596
+ />,
597
+ defaultOptions,
598
+ );
599
+
600
+ // Act
601
+
602
+ // Assert
603
+ const textArea = await screen.findByRole("textbox");
604
+ expect(textArea).toHaveAttribute("aria-invalid", "false");
605
+ });
606
+ });
607
+ });
608
+
609
+ describe("Validation", () => {
610
+ describe("validate prop", () => {
611
+ it("should be in an error state if the initial value is not empty and not valid", async () => {
612
+ // Arrange
613
+ render(
614
+ <TextArea
615
+ value="tooShort"
616
+ onChange={() => {}}
617
+ validate={(value) => {
618
+ if (value.length < 10) {
619
+ return "Error: value should be >= 10";
620
+ }
621
+ }}
622
+ />,
623
+ defaultOptions,
624
+ );
625
+ // Act
626
+
627
+ // Assert
628
+ const textArea = await screen.findByRole("textbox");
629
+ expect(textArea).toHaveAttribute("aria-invalid", "true");
630
+ });
631
+
632
+ it("should not be in an error state if the initial value is empty and not valid", async () => {
633
+ // Arrange
634
+ render(
635
+ <TextArea
636
+ value=""
637
+ onChange={() => {}}
638
+ validate={(value) => {
639
+ if (value.length < 10) {
640
+ return "Error: value should be >= 10";
641
+ }
642
+ }}
643
+ />,
644
+ defaultOptions,
645
+ );
646
+ // Act
647
+
648
+ // Assert
649
+ const textArea = await screen.findByRole("textbox");
650
+ expect(textArea).toHaveAttribute("aria-invalid", "false");
651
+ });
652
+
653
+ it("should not be in an error state if the initial value is valid", async () => {
654
+ // Arrange
655
+ render(
656
+ <TextArea
657
+ value="LongerThan10"
658
+ onChange={() => {}}
659
+ validate={(value) => {
660
+ if (value.length < 10) {
661
+ return "Error: value should be >= 10";
662
+ }
663
+ }}
664
+ />,
665
+ defaultOptions,
666
+ );
667
+ // Act
668
+
669
+ // Assert
670
+ const textArea = await screen.findByRole("textbox");
671
+ expect(textArea).toHaveAttribute("aria-invalid", "false");
672
+ });
673
+
674
+ it("should be able to change from a valid state to an error state", async () => {
675
+ // Arrange
676
+ const Controlled = () => {
677
+ const [value, setValue] = React.useState("text");
678
+ return (
679
+ <TextArea
680
+ value={value}
681
+ onChange={setValue}
682
+ validate={(value) => {
683
+ if (value.length > 4) {
684
+ return "Error";
685
+ }
686
+ }}
687
+ />
688
+ );
689
+ };
690
+ render(<Controlled />, defaultOptions);
691
+
692
+ // Act
693
+ // Add a character to make it longer than the validation limit
694
+ await userEvent.type(await screen.findByRole("textbox"), "s");
695
+
696
+ // Assert
697
+ const textArea = await screen.findByRole("textbox");
698
+ expect(textArea).toHaveAttribute("aria-invalid", "true");
699
+ });
700
+
701
+ it("should be able to change from an error state to a valid state", async () => {
702
+ // Arrange
703
+ const Controlled = () => {
704
+ const [value, setValue] = React.useState("texts");
705
+ return (
706
+ <TextArea
707
+ value={value}
708
+ onChange={setValue}
709
+ validate={(value) => {
710
+ if (value.length > 4) {
711
+ return "Error";
712
+ }
713
+ }}
714
+ />
715
+ );
716
+ };
717
+ render(<Controlled />, defaultOptions);
718
+
719
+ // Act
720
+ // Remove a character to make it within the validation limit
721
+ await userEvent.type(
722
+ await screen.findByRole("textbox"),
723
+ "{backspace}",
724
+ );
725
+
726
+ // Assert
727
+ const textArea = await screen.findByRole("textbox");
728
+ expect(textArea).toHaveAttribute("aria-invalid", "false");
729
+ });
730
+
731
+ it("should call the validate function when it is first rendered", async () => {
732
+ // Arrange
733
+ const validate = jest.fn();
734
+ render(
735
+ <TextArea
736
+ value="text"
737
+ onChange={() => {}}
738
+ validate={validate}
739
+ />,
740
+ defaultOptions,
741
+ );
742
+ // Act
743
+
744
+ // Assert
745
+ expect(validate).toHaveBeenCalledExactlyOnceWith("text");
746
+ });
747
+
748
+ it("should not call the validate function when it is first rendered if the value is empty", async () => {
749
+ // Arrange
750
+ const validate = jest.fn();
751
+ render(
752
+ <TextArea
753
+ value=""
754
+ onChange={() => {}}
755
+ validate={validate}
756
+ />,
757
+ defaultOptions,
758
+ );
759
+ // Act
760
+
761
+ // Assert
762
+ expect(validate).not.toHaveBeenCalled();
763
+ });
764
+
765
+ it("should call the validate function when the value is updated", async () => {
766
+ // Arrange
767
+ const validate = jest.fn();
768
+ const Controlled = () => {
769
+ const [value, setValue] = React.useState("text");
770
+ return (
771
+ <TextArea
772
+ value={value}
773
+ onChange={setValue}
774
+ validate={validate}
775
+ />
776
+ );
777
+ };
778
+ render(<Controlled />, defaultOptions);
779
+ // Reset mock after initial render
780
+ validate.mockReset();
781
+
782
+ // Act
783
+ // Update value
784
+ await userEvent.type(await screen.findByRole("textbox"), "s");
785
+
786
+ // Assert
787
+ expect(validate).toHaveBeenCalledExactlyOnceWith("texts");
788
+ });
789
+
790
+ it("should call the validate function when the value is updated to an empty string", async () => {
791
+ // Arrange
792
+ const validate = jest.fn();
793
+ const Controlled = () => {
794
+ const [value, setValue] = React.useState("t");
795
+ return (
796
+ <TextArea
797
+ value={value}
798
+ onChange={setValue}
799
+ validate={validate}
800
+ />
801
+ );
802
+ };
803
+ render(<Controlled />, defaultOptions);
804
+ // Reset mock after initial render
805
+ validate.mockReset();
806
+
807
+ // Act
808
+ // Erase value
809
+ await userEvent.type(
810
+ await screen.findByRole("textbox"),
811
+ "{backspace}",
812
+ );
813
+
814
+ // Assert
815
+ expect(validate).toHaveBeenCalledExactlyOnceWith("");
816
+ });
817
+ });
818
+ describe("onValidate prop", () => {
819
+ it("should call the onValidate prop with the error message when the textarea is validated", () => {
820
+ // Arrange
821
+ const handleValidate = jest.fn();
822
+ const errorMsg = "error message";
823
+ render(
824
+ <TextArea
825
+ value="text"
826
+ onChange={() => {}}
827
+ validate={() => errorMsg}
828
+ onValidate={handleValidate}
829
+ />,
830
+ defaultOptions,
831
+ );
832
+ // Act
833
+
834
+ // Assert
835
+ expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
836
+ errorMsg,
837
+ );
838
+ });
839
+
840
+ it("should call the onValidate prop with null if the validate prop returns null", () => {
841
+ // Arrange
842
+ const handleValidate = jest.fn();
843
+ render(
844
+ <TextArea
845
+ value="text"
846
+ onChange={() => {}}
847
+ validate={() => null}
848
+ onValidate={handleValidate}
849
+ />,
850
+ defaultOptions,
851
+ );
852
+ // Act
853
+
854
+ // Assert
855
+ expect(handleValidate).toHaveBeenCalledExactlyOnceWith(null);
856
+ });
857
+
858
+ it("should call the onValidate prop with null if the validate prop is a void function", () => {
859
+ // Arrange
860
+ const handleValidate = jest.fn();
861
+ render(
862
+ <TextArea
863
+ value="text"
864
+ onChange={() => {}}
865
+ validate={() => {}}
866
+ onValidate={handleValidate}
867
+ />,
868
+ defaultOptions,
869
+ );
870
+ // Act
871
+
872
+ // Assert
873
+ expect(handleValidate).toHaveBeenCalledExactlyOnceWith(null);
874
+ });
875
+ });
876
+
877
+ describe("required prop", () => {
878
+ it("should initially render with no error if it is required and the value is empty", async () => {
879
+ // Arrange
880
+ render(
881
+ <TextArea
882
+ value=""
883
+ onChange={() => {}}
884
+ required="Required"
885
+ />,
886
+ defaultOptions,
887
+ );
888
+
889
+ // Act
890
+
891
+ // Assert
892
+ const textArea = await screen.findByRole("textbox");
893
+ expect(textArea).toHaveAttribute("aria-invalid", "false");
894
+ });
895
+
896
+ it("should initially render with no error if it is required and the value is not empty", async () => {
897
+ // Arrange
898
+ render(
899
+ <TextArea
900
+ value="Text"
901
+ onChange={() => {}}
902
+ required="Required"
903
+ />,
904
+ defaultOptions,
905
+ );
906
+
907
+ // Act
908
+
909
+ // Assert
910
+ const textArea = await screen.findByRole("textbox");
911
+ expect(textArea).toHaveAttribute("aria-invalid", "false");
912
+ });
913
+
914
+ it("shound update with error if it is required and the value changes to an empty string", async () => {
915
+ // Arrange
916
+ render(
917
+ <TextArea
918
+ value="T"
919
+ onChange={() => {}}
920
+ required="Required"
921
+ />,
922
+ defaultOptions,
923
+ );
924
+
925
+ // Act
926
+ await userEvent.type(
927
+ await screen.findByRole("textbox"),
928
+ "{backspace}",
929
+ );
930
+ // Assert
931
+ const textArea = await screen.findByRole("textbox");
932
+ expect(textArea).toHaveAttribute("aria-invalid", "true");
933
+ });
934
+
935
+ it("should not call onValidate on first render if the value is empty and required prop is used", async () => {
936
+ // Arrange
937
+ const handleValidate = jest.fn();
938
+ render(
939
+ <TextArea
940
+ value=""
941
+ onChange={() => {}}
942
+ required="Required"
943
+ onValidate={handleValidate}
944
+ />,
945
+ defaultOptions,
946
+ );
947
+
948
+ // Act
949
+
950
+ // Assert
951
+ expect(handleValidate).not.toHaveBeenCalled();
952
+ });
953
+
954
+ it("should call onValidate with no error message on first render if the value is not empty and required prop is used", async () => {
955
+ // Arrange
956
+ const handleValidate = jest.fn();
957
+ render(
958
+ <TextArea
959
+ value="Text"
960
+ onChange={() => {}}
961
+ required="Required"
962
+ onValidate={handleValidate}
963
+ />,
964
+ defaultOptions,
965
+ );
966
+
967
+ // Act
968
+
969
+ // Assert
970
+ expect(handleValidate).toHaveBeenCalledExactlyOnceWith(null);
971
+ });
972
+
973
+ it("should call onValidate when the value is cleared", async () => {
974
+ // Arrange
975
+ const handleValidate = jest.fn();
976
+ render(
977
+ <TextArea
978
+ value="T"
979
+ onChange={() => {}}
980
+ required="Required"
981
+ onValidate={handleValidate}
982
+ />,
983
+ defaultOptions,
984
+ );
985
+ // Reset mock after initial render
986
+ handleValidate.mockReset();
987
+
988
+ // Act
989
+ await userEvent.type(
990
+ await screen.findByRole("textbox"),
991
+ "{backspace}",
992
+ );
993
+
994
+ // Assert
995
+ expect(handleValidate).toHaveBeenCalledOnce();
996
+ });
997
+
998
+ it("should call onValidate with the custom error message from the required prop when it is a string", async () => {
999
+ // Arrange
1000
+ const requiredErrorMsg = "Custom required error message";
1001
+ const handleValidate = jest.fn();
1002
+ render(
1003
+ <TextArea
1004
+ value="T"
1005
+ onChange={() => {}}
1006
+ required={requiredErrorMsg}
1007
+ onValidate={handleValidate}
1008
+ />,
1009
+ defaultOptions,
1010
+ );
1011
+ // Reset mock after initial render
1012
+ handleValidate.mockReset();
1013
+
1014
+ // Act
1015
+ await userEvent.type(
1016
+ await screen.findByRole("textbox"),
1017
+ "{backspace}",
1018
+ );
1019
+
1020
+ // Assert
1021
+ expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
1022
+ requiredErrorMsg,
1023
+ );
1024
+ });
1025
+
1026
+ it("should call onValidate with a default error message if required is not a string", async () => {
1027
+ // Arrange
1028
+ const handleValidate = jest.fn();
1029
+ render(
1030
+ <TextArea
1031
+ value="T"
1032
+ onChange={() => {}}
1033
+ required={true}
1034
+ onValidate={handleValidate}
1035
+ />,
1036
+ defaultOptions,
1037
+ );
1038
+ // Reset mock after initial render
1039
+ handleValidate.mockReset();
1040
+
1041
+ // Act
1042
+ await userEvent.type(
1043
+ await screen.findByRole("textbox"),
1044
+ "{backspace}",
1045
+ );
1046
+
1047
+ // Assert
1048
+ expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
1049
+ "This field is required.",
1050
+ );
1051
+ });
1052
+
1053
+ it("should prioritize validate prop over required prop if both are provided", async () => {
1054
+ // Arrange
1055
+ const handleValidate = jest.fn();
1056
+ const requiredErrorMessage = "Error because it is required";
1057
+ const validateErrorMessage = "Error because of validation";
1058
+ render(
1059
+ <TextArea
1060
+ value="T"
1061
+ onChange={() => {}}
1062
+ required={requiredErrorMessage}
1063
+ onValidate={handleValidate}
1064
+ validate={() => validateErrorMessage}
1065
+ />,
1066
+ defaultOptions,
1067
+ );
1068
+ // Reset mock after initial render
1069
+ handleValidate.mockReset();
1070
+
1071
+ // Act
1072
+ await userEvent.type(
1073
+ await screen.findByRole("textbox"),
1074
+ "{backspace}",
1075
+ );
1076
+
1077
+ // Assert
1078
+ expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
1079
+ validateErrorMessage,
1080
+ );
1081
+ });
1082
+ });
1083
+ });
1084
+ });