@khanacademy/wonder-blocks-form 4.7.5 → 4.8.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.
- package/CHANGELOG.md +12 -0
- package/dist/components/text-area.d.ts +153 -0
- package/dist/es/index.js +261 -52
- package/dist/index.d.ts +2 -1
- package/dist/index.js +258 -48
- package/package.json +1 -1
- package/src/components/__tests__/field-heading.test.tsx +10 -2
- package/src/components/__tests__/text-area.test.tsx +1286 -0
- package/src/components/text-area.tsx +410 -0
- package/src/index.ts +2 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -0,0 +1,1286 @@
|
|
|
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 aria-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).toHaveAttribute("aria-disabled", "true");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should set the aria-disabled attribute when the disabled prop is false", async () => {
|
|
94
|
+
// Arrange
|
|
95
|
+
render(
|
|
96
|
+
<TextArea disabled={false} value="Text" onChange={() => {}} />,
|
|
97
|
+
defaultOptions,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Act
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
const textArea = await screen.findByRole("textbox");
|
|
104
|
+
expect(textArea).toHaveAttribute("aria-disabled", "false");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should not set the aria-disabled attribute if the disabled prop is not provided", async () => {
|
|
108
|
+
// Arrange
|
|
109
|
+
render(
|
|
110
|
+
<TextArea value="Text" onChange={() => {}} />,
|
|
111
|
+
defaultOptions,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Act
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
const textArea = await screen.findByRole("textbox");
|
|
118
|
+
expect(textArea).not.toHaveAttribute("aria-disabled");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should not set the disabled attribute when the disabled prop is true", async () => {
|
|
122
|
+
// Arrange
|
|
123
|
+
render(
|
|
124
|
+
<TextArea disabled={true} value="Text" onChange={() => {}} />,
|
|
125
|
+
defaultOptions,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
const textArea = await screen.findByRole("textbox");
|
|
132
|
+
expect(textArea).not.toHaveAttribute("disabled");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should set the readonly attribute when the readOnly prop is provided", async () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
render(
|
|
138
|
+
<TextArea value="Text" onChange={() => {}} readOnly={true} />,
|
|
139
|
+
defaultOptions,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
const textArea = await screen.findByRole("textbox");
|
|
146
|
+
expect(textArea).toHaveAttribute("readonly");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should set the readonly attribute if the disabled prop is true", async () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
render(
|
|
152
|
+
<TextArea value="Text" onChange={() => {}} disabled={true} />,
|
|
153
|
+
defaultOptions,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Act
|
|
157
|
+
|
|
158
|
+
// Assert
|
|
159
|
+
const textArea = await screen.findByRole("textbox");
|
|
160
|
+
expect(textArea).toHaveAttribute("readonly");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should set the autocomplete attribute when the autoComplete prop is provided", async () => {
|
|
164
|
+
// Arrange
|
|
165
|
+
render(
|
|
166
|
+
<TextArea value="Text" onChange={() => {}} autoComplete="on" />,
|
|
167
|
+
defaultOptions,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Act
|
|
171
|
+
|
|
172
|
+
// Assert
|
|
173
|
+
const textArea = await screen.findByRole("textbox");
|
|
174
|
+
expect(textArea).toHaveAttribute("autocomplete", "on");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should set the name attribute when the name prop is provided", async () => {
|
|
178
|
+
// Arrange
|
|
179
|
+
const name = "Test name";
|
|
180
|
+
render(
|
|
181
|
+
<TextArea value="Text" onChange={() => {}} name={name} />,
|
|
182
|
+
defaultOptions,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Act
|
|
186
|
+
|
|
187
|
+
// Assert
|
|
188
|
+
const textArea = await screen.findByRole("textbox");
|
|
189
|
+
expect(textArea).toHaveAttribute("name", name);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should set the class when the className prop is provided", async () => {
|
|
193
|
+
// Arrange
|
|
194
|
+
const className = "Test class name";
|
|
195
|
+
render(
|
|
196
|
+
<TextArea
|
|
197
|
+
value="Text"
|
|
198
|
+
onChange={() => {}}
|
|
199
|
+
className={className}
|
|
200
|
+
/>,
|
|
201
|
+
defaultOptions,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Act
|
|
205
|
+
|
|
206
|
+
// Assert
|
|
207
|
+
const textArea = await screen.findByRole("textbox");
|
|
208
|
+
expect(textArea).toHaveClass(className);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should set the rows attribute when the rows prop is provided", async () => {
|
|
212
|
+
// Arrange
|
|
213
|
+
const rows = 10;
|
|
214
|
+
render(
|
|
215
|
+
<TextArea value="Text" onChange={() => {}} rows={rows} />,
|
|
216
|
+
defaultOptions,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Act
|
|
220
|
+
|
|
221
|
+
// Assert
|
|
222
|
+
const textArea = await screen.findByRole("textbox");
|
|
223
|
+
expect(textArea).toHaveAttribute("rows", `${rows}`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should set the spellcheck attribute when spellCheck prop is set to true", async () => {
|
|
227
|
+
// Arrange
|
|
228
|
+
render(
|
|
229
|
+
<TextArea value="Text" onChange={() => {}} spellCheck={true} />,
|
|
230
|
+
defaultOptions,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Act
|
|
234
|
+
|
|
235
|
+
// Assert
|
|
236
|
+
const textArea = await screen.findByRole("textbox");
|
|
237
|
+
expect(textArea).toHaveAttribute("spellcheck", "true");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should set the spellcheck attribute when spellCheck prop is set to false", async () => {
|
|
241
|
+
// Arrange
|
|
242
|
+
render(
|
|
243
|
+
<TextArea
|
|
244
|
+
value="Text"
|
|
245
|
+
onChange={() => {}}
|
|
246
|
+
spellCheck={false}
|
|
247
|
+
/>,
|
|
248
|
+
defaultOptions,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Act
|
|
252
|
+
|
|
253
|
+
// Assert
|
|
254
|
+
const textArea = await screen.findByRole("textbox");
|
|
255
|
+
expect(textArea).toHaveAttribute("spellcheck", "false");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it.each(wrapOptions)(
|
|
259
|
+
"should set the wrap attribute when the spellCheck prop is set to '%s' ",
|
|
260
|
+
async (wrap) => {
|
|
261
|
+
// Arrange
|
|
262
|
+
render(
|
|
263
|
+
<TextArea value="Text" onChange={() => {}} wrap={wrap} />,
|
|
264
|
+
defaultOptions,
|
|
265
|
+
);
|
|
266
|
+
// Act
|
|
267
|
+
|
|
268
|
+
// Assert
|
|
269
|
+
const textArea = await screen.findByRole("textbox");
|
|
270
|
+
expect(textArea).toHaveAttribute("wrap", wrap);
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
it("should set the minlength attribute when the minLength prop is used", async () => {
|
|
275
|
+
// Arrange
|
|
276
|
+
const minLength = 3;
|
|
277
|
+
render(
|
|
278
|
+
<TextArea
|
|
279
|
+
value="Text"
|
|
280
|
+
onChange={() => {}}
|
|
281
|
+
minLength={minLength}
|
|
282
|
+
/>,
|
|
283
|
+
defaultOptions,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Act
|
|
287
|
+
|
|
288
|
+
// Assert
|
|
289
|
+
const textArea = await screen.findByRole("textbox");
|
|
290
|
+
expect(textArea).toHaveAttribute("minlength", `${minLength}`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should set the maxlength attribute when the maxLength prop is used", async () => {
|
|
294
|
+
// Arrange
|
|
295
|
+
const maxLength = 3;
|
|
296
|
+
render(
|
|
297
|
+
<TextArea
|
|
298
|
+
value="Text"
|
|
299
|
+
onChange={() => {}}
|
|
300
|
+
maxLength={maxLength}
|
|
301
|
+
/>,
|
|
302
|
+
defaultOptions,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Act
|
|
306
|
+
|
|
307
|
+
// Assert
|
|
308
|
+
const textArea = await screen.findByRole("textbox");
|
|
309
|
+
expect(textArea).toHaveAttribute("maxlength", `${maxLength}`);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should set the required attribute when the required prop is used", async () => {
|
|
313
|
+
// Arrange
|
|
314
|
+
render(
|
|
315
|
+
<TextArea value="Text" onChange={() => {}} required={true} />,
|
|
316
|
+
defaultOptions,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Act
|
|
320
|
+
|
|
321
|
+
// Assert
|
|
322
|
+
const textArea = await screen.findByRole("textbox");
|
|
323
|
+
expect(textArea).toHaveAttribute("required");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should use the value prop", async () => {
|
|
328
|
+
// Arrange
|
|
329
|
+
const testValue = "test value";
|
|
330
|
+
render(
|
|
331
|
+
<TextArea value={testValue} onChange={() => {}} />,
|
|
332
|
+
defaultOptions,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Act
|
|
336
|
+
|
|
337
|
+
// Assert
|
|
338
|
+
const textArea = await screen.findByRole("textbox");
|
|
339
|
+
expect(textArea).toHaveValue(testValue);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should forward the ref to the textarea element", async () => {
|
|
343
|
+
// Arrange
|
|
344
|
+
const ref = React.createRef<HTMLTextAreaElement>();
|
|
345
|
+
render(
|
|
346
|
+
<TextArea value="Text" onChange={() => {}} ref={ref} />,
|
|
347
|
+
defaultOptions,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Act
|
|
351
|
+
|
|
352
|
+
// Assert
|
|
353
|
+
expect(ref.current).toBeInstanceOf(HTMLTextAreaElement);
|
|
354
|
+
expect(await screen.findByRole("textbox")).toBe(ref.current);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("Event Handlers", () => {
|
|
358
|
+
it("should call the onChange prop when the textarea value changes", async () => {
|
|
359
|
+
// Arrange
|
|
360
|
+
const onChangeMock = jest.fn();
|
|
361
|
+
render(
|
|
362
|
+
<TextArea value="" onChange={onChangeMock} />,
|
|
363
|
+
defaultOptions,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Act
|
|
367
|
+
// Type one letter
|
|
368
|
+
const letterToType = "X";
|
|
369
|
+
await userEvent.type(
|
|
370
|
+
await screen.findByRole("textbox"),
|
|
371
|
+
letterToType,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Assert
|
|
375
|
+
expect(onChangeMock).toHaveBeenCalledExactlyOnceWith(letterToType);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should not call the onChange prop when the textarea value changes and it is disabled", async () => {
|
|
379
|
+
// Arrange
|
|
380
|
+
const onChangeMock = jest.fn();
|
|
381
|
+
render(
|
|
382
|
+
<TextArea value="" onChange={onChangeMock} disabled={true} />,
|
|
383
|
+
defaultOptions,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Act
|
|
387
|
+
// Type one letter
|
|
388
|
+
const letterToType = "X";
|
|
389
|
+
await userEvent.type(
|
|
390
|
+
await screen.findByRole("textbox"),
|
|
391
|
+
letterToType,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Assert
|
|
395
|
+
expect(onChangeMock).not.toHaveBeenCalled();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should call the onClick prop when the textarea is clicked", async () => {
|
|
399
|
+
// Arrange
|
|
400
|
+
const onClickMock = jest.fn();
|
|
401
|
+
render(
|
|
402
|
+
<TextArea value="" onChange={() => {}} onClick={onClickMock} />,
|
|
403
|
+
defaultOptions,
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Act
|
|
407
|
+
await userEvent.click(await screen.findByRole("textbox"));
|
|
408
|
+
|
|
409
|
+
// Assert
|
|
410
|
+
expect(onClickMock).toHaveBeenCalledOnce();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("should not call the onClick prop when the textarea is clicked and it is disabled", async () => {
|
|
414
|
+
// Arrange
|
|
415
|
+
const onClickMock = jest.fn();
|
|
416
|
+
render(
|
|
417
|
+
<TextArea
|
|
418
|
+
value=""
|
|
419
|
+
onChange={() => {}}
|
|
420
|
+
onClick={onClickMock}
|
|
421
|
+
disabled={true}
|
|
422
|
+
/>,
|
|
423
|
+
defaultOptions,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Act
|
|
427
|
+
await userEvent.click(await screen.findByRole("textbox"));
|
|
428
|
+
|
|
429
|
+
// Assert
|
|
430
|
+
expect(onClickMock).not.toHaveBeenCalled();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("should call the onKeyDown prop when a key is typed in the textarea", async () => {
|
|
434
|
+
// Arrange
|
|
435
|
+
const handleOnKeyDown = jest.fn(
|
|
436
|
+
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
437
|
+
return event.key;
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
render(
|
|
442
|
+
<TextArea
|
|
443
|
+
value=""
|
|
444
|
+
onChange={() => {}}
|
|
445
|
+
onKeyDown={handleOnKeyDown}
|
|
446
|
+
/>,
|
|
447
|
+
defaultOptions,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Act
|
|
451
|
+
await userEvent.type(await screen.findByRole("textbox"), "{enter}");
|
|
452
|
+
|
|
453
|
+
// Assert
|
|
454
|
+
expect(handleOnKeyDown).toHaveReturnedWith("Enter");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("should not call the onKeyDown prop when a key is typed in the textarea and it is disabled", async () => {
|
|
458
|
+
// Arrange
|
|
459
|
+
const handleOnKeyDown = jest.fn();
|
|
460
|
+
|
|
461
|
+
render(
|
|
462
|
+
<TextArea
|
|
463
|
+
value=""
|
|
464
|
+
onChange={() => {}}
|
|
465
|
+
onKeyDown={handleOnKeyDown}
|
|
466
|
+
disabled={true}
|
|
467
|
+
/>,
|
|
468
|
+
defaultOptions,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Act
|
|
472
|
+
await userEvent.type(await screen.findByRole("textbox"), "{enter}");
|
|
473
|
+
|
|
474
|
+
// Assert
|
|
475
|
+
expect(handleOnKeyDown).not.toHaveBeenCalled();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("should call the onKeyUp prop when a key is typed in the textarea", async () => {
|
|
479
|
+
// Arrange
|
|
480
|
+
const handleOnKeyUp = jest.fn(
|
|
481
|
+
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
482
|
+
return event.key;
|
|
483
|
+
},
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
render(
|
|
487
|
+
<TextArea
|
|
488
|
+
value=""
|
|
489
|
+
onChange={() => {}}
|
|
490
|
+
onKeyUp={handleOnKeyUp}
|
|
491
|
+
/>,
|
|
492
|
+
defaultOptions,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Act
|
|
496
|
+
await userEvent.type(await screen.findByRole("textbox"), "{enter}");
|
|
497
|
+
|
|
498
|
+
// Assert
|
|
499
|
+
expect(handleOnKeyUp).toHaveReturnedWith("Enter");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("should not call the onKeyUp prop when a key is typed in the textarea and it is disabled", async () => {
|
|
503
|
+
// Arrange
|
|
504
|
+
const handleOnKeyUp = jest.fn();
|
|
505
|
+
|
|
506
|
+
render(
|
|
507
|
+
<TextArea
|
|
508
|
+
value=""
|
|
509
|
+
onChange={() => {}}
|
|
510
|
+
onKeyUp={handleOnKeyUp}
|
|
511
|
+
disabled={true}
|
|
512
|
+
/>,
|
|
513
|
+
defaultOptions,
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// Act
|
|
517
|
+
await userEvent.type(await screen.findByRole("textbox"), "{enter}");
|
|
518
|
+
|
|
519
|
+
// Assert
|
|
520
|
+
expect(handleOnKeyUp).not.toHaveBeenCalled();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("should call the onFocus prop when the textarea is focused", async () => {
|
|
524
|
+
// Arrange
|
|
525
|
+
const handleOnFocus = jest.fn();
|
|
526
|
+
|
|
527
|
+
render(
|
|
528
|
+
<TextArea
|
|
529
|
+
value=""
|
|
530
|
+
onChange={() => {}}
|
|
531
|
+
onFocus={handleOnFocus}
|
|
532
|
+
/>,
|
|
533
|
+
defaultOptions,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// Act
|
|
537
|
+
await userEvent.tab();
|
|
538
|
+
|
|
539
|
+
// Assert
|
|
540
|
+
expect(handleOnFocus).toHaveBeenCalledOnce();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("should continue to call the onFocus prop when the textarea is focused and it is disabled", async () => {
|
|
544
|
+
// Arrange
|
|
545
|
+
const handleOnFocus = jest.fn();
|
|
546
|
+
|
|
547
|
+
render(
|
|
548
|
+
<TextArea
|
|
549
|
+
value=""
|
|
550
|
+
onChange={() => {}}
|
|
551
|
+
onFocus={handleOnFocus}
|
|
552
|
+
disabled={true}
|
|
553
|
+
/>,
|
|
554
|
+
defaultOptions,
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// Act
|
|
558
|
+
await userEvent.tab();
|
|
559
|
+
|
|
560
|
+
// Assert
|
|
561
|
+
expect(handleOnFocus).toHaveBeenCalledOnce();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("should call the onBlur prop when the textarea is blurred", async () => {
|
|
565
|
+
// Arrange
|
|
566
|
+
const handleOnBlur = jest.fn();
|
|
567
|
+
|
|
568
|
+
render(
|
|
569
|
+
<TextArea value="" onChange={() => {}} onBlur={handleOnBlur} />,
|
|
570
|
+
defaultOptions,
|
|
571
|
+
);
|
|
572
|
+
// Tab to focus on textarea
|
|
573
|
+
await userEvent.tab();
|
|
574
|
+
|
|
575
|
+
// Act
|
|
576
|
+
// Tab to move focus away
|
|
577
|
+
await userEvent.tab();
|
|
578
|
+
|
|
579
|
+
// Assert
|
|
580
|
+
expect(handleOnBlur).toHaveBeenCalledOnce();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("should continue to call the onBlur prop when the textarea is blurred and it is disabled", async () => {
|
|
584
|
+
// Arrange
|
|
585
|
+
const handleOnBlur = jest.fn();
|
|
586
|
+
|
|
587
|
+
render(
|
|
588
|
+
<TextArea
|
|
589
|
+
value=""
|
|
590
|
+
onChange={() => {}}
|
|
591
|
+
onBlur={handleOnBlur}
|
|
592
|
+
disabled={true}
|
|
593
|
+
/>,
|
|
594
|
+
defaultOptions,
|
|
595
|
+
);
|
|
596
|
+
// Tab to focus on textarea
|
|
597
|
+
await userEvent.tab();
|
|
598
|
+
|
|
599
|
+
// Act
|
|
600
|
+
// Tab to move focus away
|
|
601
|
+
await userEvent.tab();
|
|
602
|
+
|
|
603
|
+
// Assert
|
|
604
|
+
expect(handleOnBlur).toHaveBeenCalledOnce();
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe("Accessibility", () => {
|
|
609
|
+
describe("Axe", () => {
|
|
610
|
+
test("has no accessibility violations", async () => {
|
|
611
|
+
// Arrange
|
|
612
|
+
// Use with label to demonstrate how it should be used with the
|
|
613
|
+
// TextArea component
|
|
614
|
+
const {container} = render(
|
|
615
|
+
<>
|
|
616
|
+
<label htmlFor="text-area">Test label</label>
|
|
617
|
+
<TextArea
|
|
618
|
+
value="Text"
|
|
619
|
+
onChange={() => {}}
|
|
620
|
+
id="text-area"
|
|
621
|
+
/>
|
|
622
|
+
</>,
|
|
623
|
+
defaultOptions,
|
|
624
|
+
);
|
|
625
|
+
// Act
|
|
626
|
+
|
|
627
|
+
// Assert
|
|
628
|
+
await expect(container).toHaveNoA11yViolations();
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
describe("Focus", () => {
|
|
632
|
+
it("should focus on the textarea by default when the autoFocus prop is provided", async () => {
|
|
633
|
+
// Arrange
|
|
634
|
+
render(
|
|
635
|
+
<TextArea
|
|
636
|
+
value="Text"
|
|
637
|
+
onChange={() => {}}
|
|
638
|
+
autoFocus={true}
|
|
639
|
+
/>,
|
|
640
|
+
defaultOptions,
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
// Act
|
|
644
|
+
|
|
645
|
+
// Assert
|
|
646
|
+
const textArea = await screen.findByRole("textbox");
|
|
647
|
+
expect(textArea).toHaveFocus();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("should be focusable", async () => {
|
|
651
|
+
// Arrange
|
|
652
|
+
render(
|
|
653
|
+
<TextArea value="Text" onChange={() => {}} />,
|
|
654
|
+
defaultOptions,
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
// Act
|
|
658
|
+
await userEvent.tab();
|
|
659
|
+
|
|
660
|
+
// Assert
|
|
661
|
+
const textArea = await screen.findByRole("textbox");
|
|
662
|
+
expect(textArea).toHaveFocus();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("should be focusable if it is disabled", async () => {
|
|
666
|
+
// Arrange
|
|
667
|
+
render(
|
|
668
|
+
<TextArea
|
|
669
|
+
value="Text"
|
|
670
|
+
onChange={() => {}}
|
|
671
|
+
disabled={true}
|
|
672
|
+
/>,
|
|
673
|
+
defaultOptions,
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// Act
|
|
677
|
+
await userEvent.tab();
|
|
678
|
+
|
|
679
|
+
// Assert
|
|
680
|
+
const textArea = await screen.findByRole("textbox");
|
|
681
|
+
expect(textArea).toHaveFocus();
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
describe("ARIA", () => {
|
|
686
|
+
it("should set the aria-label attribute when provided", async () => {
|
|
687
|
+
// Arrange
|
|
688
|
+
const ariaLabel = "Test Aria Label";
|
|
689
|
+
render(
|
|
690
|
+
<TextArea
|
|
691
|
+
value="Text"
|
|
692
|
+
onChange={() => {}}
|
|
693
|
+
aria-label={ariaLabel}
|
|
694
|
+
/>,
|
|
695
|
+
defaultOptions,
|
|
696
|
+
);
|
|
697
|
+
// Act
|
|
698
|
+
|
|
699
|
+
// Assert
|
|
700
|
+
const textArea = await screen.findByRole("textbox");
|
|
701
|
+
expect(textArea).toHaveAttribute("aria-label", ariaLabel);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("should set the aria-labelledby attribute when provided", async () => {
|
|
705
|
+
// Arrange
|
|
706
|
+
const ariaLabelledBy = "test-label-id";
|
|
707
|
+
render(
|
|
708
|
+
<TextArea
|
|
709
|
+
value="Text"
|
|
710
|
+
onChange={() => {}}
|
|
711
|
+
aria-labelledby={ariaLabelledBy}
|
|
712
|
+
/>,
|
|
713
|
+
defaultOptions,
|
|
714
|
+
);
|
|
715
|
+
// Act
|
|
716
|
+
|
|
717
|
+
// Assert
|
|
718
|
+
const textArea = await screen.findByRole("textbox");
|
|
719
|
+
expect(textArea).toHaveAttribute(
|
|
720
|
+
"aria-labelledby",
|
|
721
|
+
ariaLabelledBy,
|
|
722
|
+
);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("should set the aria-describedby attribute when provided", async () => {
|
|
726
|
+
// Arrange
|
|
727
|
+
const ariaDescribedBy = "test-label-id";
|
|
728
|
+
render(
|
|
729
|
+
<TextArea
|
|
730
|
+
value="Text"
|
|
731
|
+
onChange={() => {}}
|
|
732
|
+
aria-describedby={ariaDescribedBy}
|
|
733
|
+
/>,
|
|
734
|
+
defaultOptions,
|
|
735
|
+
);
|
|
736
|
+
// Act
|
|
737
|
+
|
|
738
|
+
// Assert
|
|
739
|
+
const textArea = await screen.findByRole("textbox");
|
|
740
|
+
expect(textArea).toHaveAttribute(
|
|
741
|
+
"aria-describedby",
|
|
742
|
+
ariaDescribedBy,
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("should set the aria-details attribute when provided", async () => {
|
|
747
|
+
// Arrange
|
|
748
|
+
const ariaDetails = "details-id";
|
|
749
|
+
render(
|
|
750
|
+
<TextArea
|
|
751
|
+
value="Text"
|
|
752
|
+
onChange={() => {}}
|
|
753
|
+
aria-details={ariaDetails}
|
|
754
|
+
/>,
|
|
755
|
+
defaultOptions,
|
|
756
|
+
);
|
|
757
|
+
// Act
|
|
758
|
+
|
|
759
|
+
// Assert
|
|
760
|
+
const textArea = await screen.findByRole("textbox");
|
|
761
|
+
expect(textArea).toHaveAttribute(
|
|
762
|
+
"aria-details",
|
|
763
|
+
`${ariaDetails}`,
|
|
764
|
+
);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it("should set aria-invalid to true if the validate prop returns an error message", async () => {
|
|
768
|
+
// Arrange
|
|
769
|
+
render(
|
|
770
|
+
<TextArea
|
|
771
|
+
value="text"
|
|
772
|
+
onChange={() => {}}
|
|
773
|
+
// If the validate function returns a string or true,
|
|
774
|
+
// then the text area is in an error state. For this
|
|
775
|
+
// test, we always return a string upon validation
|
|
776
|
+
// to trigger the error state. Since the textarea is
|
|
777
|
+
// being mounted with a non-empty value, it is validated
|
|
778
|
+
// on initial render. Because the text area is in
|
|
779
|
+
// an error state, it will have aria-invalid=true
|
|
780
|
+
validate={() => "Error"}
|
|
781
|
+
/>,
|
|
782
|
+
defaultOptions,
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
// Act
|
|
786
|
+
|
|
787
|
+
// Assert
|
|
788
|
+
const textArea = await screen.findByRole("textbox");
|
|
789
|
+
expect(textArea).toHaveAttribute("aria-invalid", "true");
|
|
790
|
+
});
|
|
791
|
+
it("should set aria-invalid to true if the validate prop returns an error message", async () => {
|
|
792
|
+
// Arrange
|
|
793
|
+
render(
|
|
794
|
+
<TextArea
|
|
795
|
+
value="text"
|
|
796
|
+
onChange={() => {}}
|
|
797
|
+
validate={() => null}
|
|
798
|
+
/>,
|
|
799
|
+
defaultOptions,
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
// Act
|
|
803
|
+
|
|
804
|
+
// Assert
|
|
805
|
+
const textArea = await screen.findByRole("textbox");
|
|
806
|
+
expect(textArea).toHaveAttribute("aria-invalid", "false");
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
describe("Validation", () => {
|
|
812
|
+
describe("validate prop", () => {
|
|
813
|
+
it("should be in an error state if the initial value is not empty and not valid", async () => {
|
|
814
|
+
// Arrange
|
|
815
|
+
render(
|
|
816
|
+
<TextArea
|
|
817
|
+
value="tooShort"
|
|
818
|
+
onChange={() => {}}
|
|
819
|
+
validate={(value) => {
|
|
820
|
+
if (value.length < 10) {
|
|
821
|
+
return "Error: value should be >= 10";
|
|
822
|
+
}
|
|
823
|
+
}}
|
|
824
|
+
/>,
|
|
825
|
+
defaultOptions,
|
|
826
|
+
);
|
|
827
|
+
// Act
|
|
828
|
+
|
|
829
|
+
// Assert
|
|
830
|
+
const textArea = await screen.findByRole("textbox");
|
|
831
|
+
expect(textArea).toHaveAttribute("aria-invalid", "true");
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("should not be in an error state if the initial value is empty and not valid", async () => {
|
|
835
|
+
// Arrange
|
|
836
|
+
render(
|
|
837
|
+
<TextArea
|
|
838
|
+
value=""
|
|
839
|
+
onChange={() => {}}
|
|
840
|
+
validate={(value) => {
|
|
841
|
+
if (value.length < 10) {
|
|
842
|
+
return "Error: value should be >= 10";
|
|
843
|
+
}
|
|
844
|
+
}}
|
|
845
|
+
/>,
|
|
846
|
+
defaultOptions,
|
|
847
|
+
);
|
|
848
|
+
// Act
|
|
849
|
+
|
|
850
|
+
// Assert
|
|
851
|
+
const textArea = await screen.findByRole("textbox");
|
|
852
|
+
expect(textArea).toHaveAttribute("aria-invalid", "false");
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("should not be in an error state if the initial value is valid", async () => {
|
|
856
|
+
// Arrange
|
|
857
|
+
render(
|
|
858
|
+
<TextArea
|
|
859
|
+
value="LongerThan10"
|
|
860
|
+
onChange={() => {}}
|
|
861
|
+
validate={(value) => {
|
|
862
|
+
if (value.length < 10) {
|
|
863
|
+
return "Error: value should be >= 10";
|
|
864
|
+
}
|
|
865
|
+
}}
|
|
866
|
+
/>,
|
|
867
|
+
defaultOptions,
|
|
868
|
+
);
|
|
869
|
+
// Act
|
|
870
|
+
|
|
871
|
+
// Assert
|
|
872
|
+
const textArea = await screen.findByRole("textbox");
|
|
873
|
+
expect(textArea).toHaveAttribute("aria-invalid", "false");
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it("should be able to change from a valid state to an error state", async () => {
|
|
877
|
+
// Arrange
|
|
878
|
+
const Controlled = () => {
|
|
879
|
+
const [value, setValue] = React.useState("text");
|
|
880
|
+
return (
|
|
881
|
+
<TextArea
|
|
882
|
+
value={value}
|
|
883
|
+
onChange={setValue}
|
|
884
|
+
validate={(value) => {
|
|
885
|
+
if (value.length > 4) {
|
|
886
|
+
return "Error";
|
|
887
|
+
}
|
|
888
|
+
}}
|
|
889
|
+
/>
|
|
890
|
+
);
|
|
891
|
+
};
|
|
892
|
+
render(<Controlled />, defaultOptions);
|
|
893
|
+
|
|
894
|
+
// Act
|
|
895
|
+
// Add a character to make it longer than the validation limit
|
|
896
|
+
await userEvent.type(await screen.findByRole("textbox"), "s");
|
|
897
|
+
|
|
898
|
+
// Assert
|
|
899
|
+
const textArea = await screen.findByRole("textbox");
|
|
900
|
+
expect(textArea).toHaveAttribute("aria-invalid", "true");
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("should be able to change from an error state to a valid state", async () => {
|
|
904
|
+
// Arrange
|
|
905
|
+
const Controlled = () => {
|
|
906
|
+
const [value, setValue] = React.useState("texts");
|
|
907
|
+
return (
|
|
908
|
+
<TextArea
|
|
909
|
+
value={value}
|
|
910
|
+
onChange={setValue}
|
|
911
|
+
validate={(value) => {
|
|
912
|
+
if (value.length > 4) {
|
|
913
|
+
return "Error";
|
|
914
|
+
}
|
|
915
|
+
}}
|
|
916
|
+
/>
|
|
917
|
+
);
|
|
918
|
+
};
|
|
919
|
+
render(<Controlled />, defaultOptions);
|
|
920
|
+
|
|
921
|
+
// Act
|
|
922
|
+
// Remove a character to make it within the validation limit
|
|
923
|
+
await userEvent.type(
|
|
924
|
+
await screen.findByRole("textbox"),
|
|
925
|
+
"{backspace}",
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
// Assert
|
|
929
|
+
const textArea = await screen.findByRole("textbox");
|
|
930
|
+
expect(textArea).toHaveAttribute("aria-invalid", "false");
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it("should call the validate function when it is first rendered", async () => {
|
|
934
|
+
// Arrange
|
|
935
|
+
const validate = jest.fn();
|
|
936
|
+
render(
|
|
937
|
+
<TextArea
|
|
938
|
+
value="text"
|
|
939
|
+
onChange={() => {}}
|
|
940
|
+
validate={validate}
|
|
941
|
+
/>,
|
|
942
|
+
defaultOptions,
|
|
943
|
+
);
|
|
944
|
+
// Act
|
|
945
|
+
|
|
946
|
+
// Assert
|
|
947
|
+
expect(validate).toHaveBeenCalledExactlyOnceWith("text");
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it("should not call the validate function when it is first rendered if the value is empty", async () => {
|
|
951
|
+
// Arrange
|
|
952
|
+
const validate = jest.fn();
|
|
953
|
+
render(
|
|
954
|
+
<TextArea
|
|
955
|
+
value=""
|
|
956
|
+
onChange={() => {}}
|
|
957
|
+
validate={validate}
|
|
958
|
+
/>,
|
|
959
|
+
defaultOptions,
|
|
960
|
+
);
|
|
961
|
+
// Act
|
|
962
|
+
|
|
963
|
+
// Assert
|
|
964
|
+
expect(validate).not.toHaveBeenCalled();
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it("should call the validate function when the value is updated", async () => {
|
|
968
|
+
// Arrange
|
|
969
|
+
const validate = jest.fn();
|
|
970
|
+
const Controlled = () => {
|
|
971
|
+
const [value, setValue] = React.useState("text");
|
|
972
|
+
return (
|
|
973
|
+
<TextArea
|
|
974
|
+
value={value}
|
|
975
|
+
onChange={setValue}
|
|
976
|
+
validate={validate}
|
|
977
|
+
/>
|
|
978
|
+
);
|
|
979
|
+
};
|
|
980
|
+
render(<Controlled />, defaultOptions);
|
|
981
|
+
// Reset mock after initial render
|
|
982
|
+
validate.mockReset();
|
|
983
|
+
|
|
984
|
+
// Act
|
|
985
|
+
// Update value
|
|
986
|
+
await userEvent.type(await screen.findByRole("textbox"), "s");
|
|
987
|
+
|
|
988
|
+
// Assert
|
|
989
|
+
expect(validate).toHaveBeenCalledExactlyOnceWith("texts");
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it("should call the validate function when the value is updated to an empty string", async () => {
|
|
993
|
+
// Arrange
|
|
994
|
+
const validate = jest.fn();
|
|
995
|
+
const Controlled = () => {
|
|
996
|
+
const [value, setValue] = React.useState("t");
|
|
997
|
+
return (
|
|
998
|
+
<TextArea
|
|
999
|
+
value={value}
|
|
1000
|
+
onChange={setValue}
|
|
1001
|
+
validate={validate}
|
|
1002
|
+
/>
|
|
1003
|
+
);
|
|
1004
|
+
};
|
|
1005
|
+
render(<Controlled />, defaultOptions);
|
|
1006
|
+
// Reset mock after initial render
|
|
1007
|
+
validate.mockReset();
|
|
1008
|
+
|
|
1009
|
+
// Act
|
|
1010
|
+
// Erase value
|
|
1011
|
+
await userEvent.type(
|
|
1012
|
+
await screen.findByRole("textbox"),
|
|
1013
|
+
"{backspace}",
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
// Assert
|
|
1017
|
+
expect(validate).toHaveBeenCalledExactlyOnceWith("");
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
describe("onValidate prop", () => {
|
|
1021
|
+
it("should call the onValidate prop with the error message when the textarea is validated", () => {
|
|
1022
|
+
// Arrange
|
|
1023
|
+
const handleValidate = jest.fn();
|
|
1024
|
+
const errorMsg = "error message";
|
|
1025
|
+
render(
|
|
1026
|
+
<TextArea
|
|
1027
|
+
value="text"
|
|
1028
|
+
onChange={() => {}}
|
|
1029
|
+
validate={() => errorMsg}
|
|
1030
|
+
onValidate={handleValidate}
|
|
1031
|
+
/>,
|
|
1032
|
+
defaultOptions,
|
|
1033
|
+
);
|
|
1034
|
+
// Act
|
|
1035
|
+
|
|
1036
|
+
// Assert
|
|
1037
|
+
expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
|
|
1038
|
+
errorMsg,
|
|
1039
|
+
);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it("should call the onValidate prop with null if the validate prop returns null", () => {
|
|
1043
|
+
// Arrange
|
|
1044
|
+
const handleValidate = jest.fn();
|
|
1045
|
+
render(
|
|
1046
|
+
<TextArea
|
|
1047
|
+
value="text"
|
|
1048
|
+
onChange={() => {}}
|
|
1049
|
+
validate={() => null}
|
|
1050
|
+
onValidate={handleValidate}
|
|
1051
|
+
/>,
|
|
1052
|
+
defaultOptions,
|
|
1053
|
+
);
|
|
1054
|
+
// Act
|
|
1055
|
+
|
|
1056
|
+
// Assert
|
|
1057
|
+
expect(handleValidate).toHaveBeenCalledExactlyOnceWith(null);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it("should call the onValidate prop with null if the validate prop is a void function", () => {
|
|
1061
|
+
// Arrange
|
|
1062
|
+
const handleValidate = jest.fn();
|
|
1063
|
+
render(
|
|
1064
|
+
<TextArea
|
|
1065
|
+
value="text"
|
|
1066
|
+
onChange={() => {}}
|
|
1067
|
+
validate={() => {}}
|
|
1068
|
+
onValidate={handleValidate}
|
|
1069
|
+
/>,
|
|
1070
|
+
defaultOptions,
|
|
1071
|
+
);
|
|
1072
|
+
// Act
|
|
1073
|
+
|
|
1074
|
+
// Assert
|
|
1075
|
+
expect(handleValidate).toHaveBeenCalledExactlyOnceWith(null);
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
describe("required prop", () => {
|
|
1080
|
+
it("should initially render with no error if it is required and the value is empty", async () => {
|
|
1081
|
+
// Arrange
|
|
1082
|
+
render(
|
|
1083
|
+
<TextArea
|
|
1084
|
+
value=""
|
|
1085
|
+
onChange={() => {}}
|
|
1086
|
+
required="Required"
|
|
1087
|
+
/>,
|
|
1088
|
+
defaultOptions,
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
// Act
|
|
1092
|
+
|
|
1093
|
+
// Assert
|
|
1094
|
+
const textArea = await screen.findByRole("textbox");
|
|
1095
|
+
expect(textArea).toHaveAttribute("aria-invalid", "false");
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it("should initially render with no error if it is required and the value is not empty", async () => {
|
|
1099
|
+
// Arrange
|
|
1100
|
+
render(
|
|
1101
|
+
<TextArea
|
|
1102
|
+
value="Text"
|
|
1103
|
+
onChange={() => {}}
|
|
1104
|
+
required="Required"
|
|
1105
|
+
/>,
|
|
1106
|
+
defaultOptions,
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
// Act
|
|
1110
|
+
|
|
1111
|
+
// Assert
|
|
1112
|
+
const textArea = await screen.findByRole("textbox");
|
|
1113
|
+
expect(textArea).toHaveAttribute("aria-invalid", "false");
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it("shound update with error if it is required and the value changes to an empty string", async () => {
|
|
1117
|
+
// Arrange
|
|
1118
|
+
render(
|
|
1119
|
+
<TextArea
|
|
1120
|
+
value="T"
|
|
1121
|
+
onChange={() => {}}
|
|
1122
|
+
required="Required"
|
|
1123
|
+
/>,
|
|
1124
|
+
defaultOptions,
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
// Act
|
|
1128
|
+
await userEvent.type(
|
|
1129
|
+
await screen.findByRole("textbox"),
|
|
1130
|
+
"{backspace}",
|
|
1131
|
+
);
|
|
1132
|
+
// Assert
|
|
1133
|
+
const textArea = await screen.findByRole("textbox");
|
|
1134
|
+
expect(textArea).toHaveAttribute("aria-invalid", "true");
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it("should not call onValidate on first render if the value is empty and required prop is used", async () => {
|
|
1138
|
+
// Arrange
|
|
1139
|
+
const handleValidate = jest.fn();
|
|
1140
|
+
render(
|
|
1141
|
+
<TextArea
|
|
1142
|
+
value=""
|
|
1143
|
+
onChange={() => {}}
|
|
1144
|
+
required="Required"
|
|
1145
|
+
onValidate={handleValidate}
|
|
1146
|
+
/>,
|
|
1147
|
+
defaultOptions,
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
// Act
|
|
1151
|
+
|
|
1152
|
+
// Assert
|
|
1153
|
+
expect(handleValidate).not.toHaveBeenCalled();
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("should call onValidate with no error message on first render if the value is not empty and required prop is used", async () => {
|
|
1157
|
+
// Arrange
|
|
1158
|
+
const handleValidate = jest.fn();
|
|
1159
|
+
render(
|
|
1160
|
+
<TextArea
|
|
1161
|
+
value="Text"
|
|
1162
|
+
onChange={() => {}}
|
|
1163
|
+
required="Required"
|
|
1164
|
+
onValidate={handleValidate}
|
|
1165
|
+
/>,
|
|
1166
|
+
defaultOptions,
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
// Act
|
|
1170
|
+
|
|
1171
|
+
// Assert
|
|
1172
|
+
expect(handleValidate).toHaveBeenCalledExactlyOnceWith(null);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it("should call onValidate when the value is cleared", async () => {
|
|
1176
|
+
// Arrange
|
|
1177
|
+
const handleValidate = jest.fn();
|
|
1178
|
+
render(
|
|
1179
|
+
<TextArea
|
|
1180
|
+
value="T"
|
|
1181
|
+
onChange={() => {}}
|
|
1182
|
+
required="Required"
|
|
1183
|
+
onValidate={handleValidate}
|
|
1184
|
+
/>,
|
|
1185
|
+
defaultOptions,
|
|
1186
|
+
);
|
|
1187
|
+
// Reset mock after initial render
|
|
1188
|
+
handleValidate.mockReset();
|
|
1189
|
+
|
|
1190
|
+
// Act
|
|
1191
|
+
await userEvent.type(
|
|
1192
|
+
await screen.findByRole("textbox"),
|
|
1193
|
+
"{backspace}",
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
// Assert
|
|
1197
|
+
expect(handleValidate).toHaveBeenCalledOnce();
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
it("should call onValidate with the custom error message from the required prop when it is a string", async () => {
|
|
1201
|
+
// Arrange
|
|
1202
|
+
const requiredErrorMsg = "Custom required error message";
|
|
1203
|
+
const handleValidate = jest.fn();
|
|
1204
|
+
render(
|
|
1205
|
+
<TextArea
|
|
1206
|
+
value="T"
|
|
1207
|
+
onChange={() => {}}
|
|
1208
|
+
required={requiredErrorMsg}
|
|
1209
|
+
onValidate={handleValidate}
|
|
1210
|
+
/>,
|
|
1211
|
+
defaultOptions,
|
|
1212
|
+
);
|
|
1213
|
+
// Reset mock after initial render
|
|
1214
|
+
handleValidate.mockReset();
|
|
1215
|
+
|
|
1216
|
+
// Act
|
|
1217
|
+
await userEvent.type(
|
|
1218
|
+
await screen.findByRole("textbox"),
|
|
1219
|
+
"{backspace}",
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
// Assert
|
|
1223
|
+
expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
|
|
1224
|
+
requiredErrorMsg,
|
|
1225
|
+
);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it("should call onValidate with a default error message if required is not a string", async () => {
|
|
1229
|
+
// Arrange
|
|
1230
|
+
const handleValidate = jest.fn();
|
|
1231
|
+
render(
|
|
1232
|
+
<TextArea
|
|
1233
|
+
value="T"
|
|
1234
|
+
onChange={() => {}}
|
|
1235
|
+
required={true}
|
|
1236
|
+
onValidate={handleValidate}
|
|
1237
|
+
/>,
|
|
1238
|
+
defaultOptions,
|
|
1239
|
+
);
|
|
1240
|
+
// Reset mock after initial render
|
|
1241
|
+
handleValidate.mockReset();
|
|
1242
|
+
|
|
1243
|
+
// Act
|
|
1244
|
+
await userEvent.type(
|
|
1245
|
+
await screen.findByRole("textbox"),
|
|
1246
|
+
"{backspace}",
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
// Assert
|
|
1250
|
+
expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
|
|
1251
|
+
"This field is required.",
|
|
1252
|
+
);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it("should prioritize validate prop over required prop if both are provided", async () => {
|
|
1256
|
+
// Arrange
|
|
1257
|
+
const handleValidate = jest.fn();
|
|
1258
|
+
const requiredErrorMessage = "Error because it is required";
|
|
1259
|
+
const validateErrorMessage = "Error because of validation";
|
|
1260
|
+
render(
|
|
1261
|
+
<TextArea
|
|
1262
|
+
value="T"
|
|
1263
|
+
onChange={() => {}}
|
|
1264
|
+
required={requiredErrorMessage}
|
|
1265
|
+
onValidate={handleValidate}
|
|
1266
|
+
validate={() => validateErrorMessage}
|
|
1267
|
+
/>,
|
|
1268
|
+
defaultOptions,
|
|
1269
|
+
);
|
|
1270
|
+
// Reset mock after initial render
|
|
1271
|
+
handleValidate.mockReset();
|
|
1272
|
+
|
|
1273
|
+
// Act
|
|
1274
|
+
await userEvent.type(
|
|
1275
|
+
await screen.findByRole("textbox"),
|
|
1276
|
+
"{backspace}",
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
// Assert
|
|
1280
|
+
expect(handleValidate).toHaveBeenCalledExactlyOnceWith(
|
|
1281
|
+
validateErrorMessage,
|
|
1282
|
+
);
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
});
|