@object-ui/fields 3.4.0 → 4.0.3
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 +43 -0
- package/package.json +7 -7
- package/dist/packages/fields/src/__tests__/boolean-checkbox.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/cell-renderers.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/datetime-cell.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/datetime-timezone.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/extreme-inputs.test.d.ts +0 -0
- package/dist/packages/fields/src/__tests__/form-variants.test.d.ts +0 -0
- package/dist/packages/fields/src/lookup-depends-on.test.d.ts +0 -1
- package/dist/packages/fields/src/new-widgets.test.d.ts +0 -8
- package/dist/packages/fields/src/record-picker.test.d.ts +0 -1
- package/dist/packages/fields/src/remaining-widgets.test.d.ts +0 -1
- package/dist/packages/fields/src/text-rich-widgets.test.d.ts +0 -1
- package/dist/packages/fields/src/visual-widgets.test.d.ts +0 -1
- package/src/__tests__/boolean-checkbox.test.tsx +0 -74
- package/src/__tests__/cell-renderers.test.tsx +0 -1193
- package/src/__tests__/datetime-cell.test.tsx +0 -46
- package/src/__tests__/datetime-timezone.test.tsx +0 -770
- package/src/__tests__/extreme-inputs.test.tsx +0 -567
- package/src/__tests__/form-variants.test.tsx +0 -306
- package/src/lookup-depends-on.test.tsx +0 -130
- package/src/new-widgets.test.tsx +0 -302
- package/src/record-picker.test.tsx +0 -1703
- package/src/remaining-widgets.test.tsx +0 -211
- package/src/text-rich-widgets.test.tsx +0 -104
- package/src/visual-widgets.test.tsx +0 -97
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* P3.2 Field Widget Polish - Extreme Inputs
|
|
11
|
-
*
|
|
12
|
-
* Tests field widgets with extreme inputs: very long strings,
|
|
13
|
-
* MAX_SAFE_INTEGER, emoji, RTL text, null/undefined values.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
17
|
-
import React from 'react';
|
|
18
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
19
|
-
import '@testing-library/jest-dom';
|
|
20
|
-
|
|
21
|
-
import { TextField } from '../widgets/TextField';
|
|
22
|
-
import { NumberField } from '../widgets/NumberField';
|
|
23
|
-
import { EmailField } from '../widgets/EmailField';
|
|
24
|
-
import { UrlField } from '../widgets/UrlField';
|
|
25
|
-
import { PhoneField } from '../widgets/PhoneField';
|
|
26
|
-
import { TextAreaField } from '../widgets/TextAreaField';
|
|
27
|
-
import { CurrencyField } from '../widgets/CurrencyField';
|
|
28
|
-
import { BooleanField } from '../widgets/BooleanField';
|
|
29
|
-
import { SelectField } from '../widgets/SelectField';
|
|
30
|
-
|
|
31
|
-
const noop = vi.fn();
|
|
32
|
-
|
|
33
|
-
describe('P3.2 Extreme Inputs', () => {
|
|
34
|
-
// ---------------------------------------------------------------
|
|
35
|
-
// Very long strings (10,000 chars)
|
|
36
|
-
// ---------------------------------------------------------------
|
|
37
|
-
describe('very long strings', () => {
|
|
38
|
-
const longStr = 'x'.repeat(10000);
|
|
39
|
-
|
|
40
|
-
it('TextField handles 10,000-char value', () => {
|
|
41
|
-
render(
|
|
42
|
-
<TextField
|
|
43
|
-
value={longStr}
|
|
44
|
-
onChange={noop}
|
|
45
|
-
field={{ type: 'text' } as any}
|
|
46
|
-
/>
|
|
47
|
-
);
|
|
48
|
-
expect(screen.getByRole('textbox')).toHaveValue(longStr);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('TextField readonly handles 10,000-char value', () => {
|
|
52
|
-
const { container } = render(
|
|
53
|
-
<TextField
|
|
54
|
-
value={longStr}
|
|
55
|
-
onChange={noop}
|
|
56
|
-
field={{ type: 'text' } as any}
|
|
57
|
-
readonly={true}
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
60
|
-
expect(container.textContent).toHaveLength(10000);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('EmailField handles long email value', () => {
|
|
64
|
-
const longEmail = 'a'.repeat(200) + '@example.com';
|
|
65
|
-
render(
|
|
66
|
-
<EmailField
|
|
67
|
-
value={longEmail}
|
|
68
|
-
onChange={noop}
|
|
69
|
-
field={{ type: 'email' } as any}
|
|
70
|
-
/>
|
|
71
|
-
);
|
|
72
|
-
expect(screen.getByDisplayValue(longEmail)).toBeInTheDocument();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('TextAreaField handles 10,000-char value', () => {
|
|
76
|
-
render(
|
|
77
|
-
<TextAreaField
|
|
78
|
-
value={longStr}
|
|
79
|
-
onChange={noop}
|
|
80
|
-
field={{ type: 'textarea' } as any}
|
|
81
|
-
/>
|
|
82
|
-
);
|
|
83
|
-
expect(screen.getByRole('textbox')).toHaveValue(longStr);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('UrlField handles long URL value', () => {
|
|
87
|
-
const longUrl = 'https://example.com/' + 'a'.repeat(5000);
|
|
88
|
-
render(
|
|
89
|
-
<UrlField
|
|
90
|
-
value={longUrl}
|
|
91
|
-
onChange={noop}
|
|
92
|
-
field={{ type: 'url' } as any}
|
|
93
|
-
/>
|
|
94
|
-
);
|
|
95
|
-
expect(screen.getByDisplayValue(longUrl)).toBeInTheDocument();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ---------------------------------------------------------------
|
|
100
|
-
// Numeric extremes
|
|
101
|
-
// ---------------------------------------------------------------
|
|
102
|
-
describe('numeric extremes', () => {
|
|
103
|
-
it('NumberField handles MAX_SAFE_INTEGER', () => {
|
|
104
|
-
render(
|
|
105
|
-
<NumberField
|
|
106
|
-
value={Number.MAX_SAFE_INTEGER}
|
|
107
|
-
onChange={noop}
|
|
108
|
-
field={{ type: 'number' } as any}
|
|
109
|
-
/>
|
|
110
|
-
);
|
|
111
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(Number.MAX_SAFE_INTEGER);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('NumberField handles negative MAX_SAFE_INTEGER', () => {
|
|
115
|
-
render(
|
|
116
|
-
<NumberField
|
|
117
|
-
value={-Number.MAX_SAFE_INTEGER}
|
|
118
|
-
onChange={noop}
|
|
119
|
-
field={{ type: 'number' } as any}
|
|
120
|
-
/>
|
|
121
|
-
);
|
|
122
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(-Number.MAX_SAFE_INTEGER);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('NumberField handles 0', () => {
|
|
126
|
-
render(
|
|
127
|
-
<NumberField
|
|
128
|
-
value={0}
|
|
129
|
-
onChange={noop}
|
|
130
|
-
field={{ type: 'number' } as any}
|
|
131
|
-
/>
|
|
132
|
-
);
|
|
133
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(0);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('NumberField readonly handles MAX_SAFE_INTEGER', () => {
|
|
137
|
-
const { container } = render(
|
|
138
|
-
<NumberField
|
|
139
|
-
value={Number.MAX_SAFE_INTEGER}
|
|
140
|
-
onChange={noop}
|
|
141
|
-
field={{ type: 'number' } as any}
|
|
142
|
-
readonly={true}
|
|
143
|
-
/>
|
|
144
|
-
);
|
|
145
|
-
expect(container.textContent).toBe(String(Number.MAX_SAFE_INTEGER));
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('CurrencyField handles large value', () => {
|
|
149
|
-
render(
|
|
150
|
-
<CurrencyField
|
|
151
|
-
value={9999999.99}
|
|
152
|
-
onChange={noop}
|
|
153
|
-
field={{ type: 'currency', currency: 'USD' } as any}
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(9999999.99);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('CurrencyField handles 0', () => {
|
|
160
|
-
render(
|
|
161
|
-
<CurrencyField
|
|
162
|
-
value={0}
|
|
163
|
-
onChange={noop}
|
|
164
|
-
field={{ type: 'currency', currency: 'USD' } as any}
|
|
165
|
-
/>
|
|
166
|
-
);
|
|
167
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(0);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('NumberField fires onChange with empty string -> null', () => {
|
|
171
|
-
const onChange = vi.fn();
|
|
172
|
-
render(
|
|
173
|
-
<NumberField
|
|
174
|
-
value={42}
|
|
175
|
-
onChange={onChange}
|
|
176
|
-
field={{ type: 'number' } as any}
|
|
177
|
-
/>
|
|
178
|
-
);
|
|
179
|
-
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '' } });
|
|
180
|
-
expect(onChange).toHaveBeenCalledWith(null);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// ---------------------------------------------------------------
|
|
185
|
-
// Emoji and special unicode
|
|
186
|
-
// ---------------------------------------------------------------
|
|
187
|
-
describe('emoji and unicode', () => {
|
|
188
|
-
it('TextField handles emoji input', () => {
|
|
189
|
-
render(
|
|
190
|
-
<TextField
|
|
191
|
-
value="🚀🎉🔥"
|
|
192
|
-
onChange={noop}
|
|
193
|
-
field={{ type: 'text' } as any}
|
|
194
|
-
/>
|
|
195
|
-
);
|
|
196
|
-
expect(screen.getByRole('textbox')).toHaveValue('🚀🎉🔥');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('TextField readonly handles emoji', () => {
|
|
200
|
-
const { container } = render(
|
|
201
|
-
<TextField
|
|
202
|
-
value="Hello 🌍"
|
|
203
|
-
onChange={noop}
|
|
204
|
-
field={{ type: 'text' } as any}
|
|
205
|
-
readonly={true}
|
|
206
|
-
/>
|
|
207
|
-
);
|
|
208
|
-
expect(container.textContent).toBe('Hello 🌍');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('TextAreaField handles emoji', () => {
|
|
212
|
-
render(
|
|
213
|
-
<TextAreaField
|
|
214
|
-
value="Notes: 📝✅❌"
|
|
215
|
-
onChange={noop}
|
|
216
|
-
field={{ type: 'textarea' } as any}
|
|
217
|
-
/>
|
|
218
|
-
);
|
|
219
|
-
expect(screen.getByRole('textbox')).toHaveValue('Notes: 📝✅❌');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('PhoneField handles unicode value', () => {
|
|
223
|
-
render(
|
|
224
|
-
<PhoneField
|
|
225
|
-
value="+1 (555) 123-4567"
|
|
226
|
-
onChange={noop}
|
|
227
|
-
field={{ type: 'phone' } as any}
|
|
228
|
-
/>
|
|
229
|
-
);
|
|
230
|
-
expect(screen.getByDisplayValue('+1 (555) 123-4567')).toBeInTheDocument();
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('TextField handles multi-byte emoji family (ZWJ sequence)', () => {
|
|
234
|
-
render(
|
|
235
|
-
<TextField
|
|
236
|
-
value="👨👩👧👦"
|
|
237
|
-
onChange={noop}
|
|
238
|
-
field={{ type: 'text' } as any}
|
|
239
|
-
/>
|
|
240
|
-
);
|
|
241
|
-
expect(screen.getByRole('textbox')).toHaveValue('👨👩👧👦');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('TextAreaField handles multi-byte emoji family', () => {
|
|
245
|
-
render(
|
|
246
|
-
<TextAreaField
|
|
247
|
-
value="Family: 👨👩👧👦 Flag: 🏳️🌈"
|
|
248
|
-
onChange={noop}
|
|
249
|
-
field={{ type: 'textarea' } as any}
|
|
250
|
-
/>
|
|
251
|
-
);
|
|
252
|
-
expect(screen.getByRole('textbox')).toHaveValue('Family: 👨👩👧👦 Flag: 🏳️🌈');
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// ---------------------------------------------------------------
|
|
257
|
-
// RTL text
|
|
258
|
-
// ---------------------------------------------------------------
|
|
259
|
-
describe('RTL text', () => {
|
|
260
|
-
const arabicText = 'مرحبا بالعالم';
|
|
261
|
-
|
|
262
|
-
it('TextField handles RTL text', () => {
|
|
263
|
-
render(
|
|
264
|
-
<TextField
|
|
265
|
-
value={arabicText}
|
|
266
|
-
onChange={noop}
|
|
267
|
-
field={{ type: 'text' } as any}
|
|
268
|
-
/>
|
|
269
|
-
);
|
|
270
|
-
expect(screen.getByRole('textbox')).toHaveValue(arabicText);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('TextField readonly handles RTL text', () => {
|
|
274
|
-
const { container } = render(
|
|
275
|
-
<TextField
|
|
276
|
-
value={arabicText}
|
|
277
|
-
onChange={noop}
|
|
278
|
-
field={{ type: 'text' } as any}
|
|
279
|
-
readonly={true}
|
|
280
|
-
/>
|
|
281
|
-
);
|
|
282
|
-
expect(container.textContent).toBe(arabicText);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('TextAreaField handles RTL text', () => {
|
|
286
|
-
render(
|
|
287
|
-
<TextAreaField
|
|
288
|
-
value={arabicText}
|
|
289
|
-
onChange={noop}
|
|
290
|
-
field={{ type: 'textarea' } as any}
|
|
291
|
-
/>
|
|
292
|
-
);
|
|
293
|
-
expect(screen.getByRole('textbox')).toHaveValue(arabicText);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('TextField handles Hebrew RTL text', () => {
|
|
297
|
-
const hebrewText = 'שלום עולם';
|
|
298
|
-
render(
|
|
299
|
-
<TextField
|
|
300
|
-
value={hebrewText}
|
|
301
|
-
onChange={noop}
|
|
302
|
-
field={{ type: 'text' } as any}
|
|
303
|
-
/>
|
|
304
|
-
);
|
|
305
|
-
expect(screen.getByRole('textbox')).toHaveValue(hebrewText);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('TextField handles mixed RTL and LTR text', () => {
|
|
309
|
-
const mixedText = 'Hello مرحبا World שלום';
|
|
310
|
-
render(
|
|
311
|
-
<TextField
|
|
312
|
-
value={mixedText}
|
|
313
|
-
onChange={noop}
|
|
314
|
-
field={{ type: 'text' } as any}
|
|
315
|
-
/>
|
|
316
|
-
);
|
|
317
|
-
expect(screen.getByRole('textbox')).toHaveValue(mixedText);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// ---------------------------------------------------------------
|
|
322
|
-
// Null and undefined values
|
|
323
|
-
// ---------------------------------------------------------------
|
|
324
|
-
describe('null and undefined values', () => {
|
|
325
|
-
it('TextField treats null as empty string', () => {
|
|
326
|
-
render(
|
|
327
|
-
<TextField
|
|
328
|
-
value={null as any}
|
|
329
|
-
onChange={noop}
|
|
330
|
-
field={{ type: 'text' } as any}
|
|
331
|
-
/>
|
|
332
|
-
);
|
|
333
|
-
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('NumberField treats null as empty', () => {
|
|
337
|
-
render(
|
|
338
|
-
<NumberField
|
|
339
|
-
value={null as any}
|
|
340
|
-
onChange={noop}
|
|
341
|
-
field={{ type: 'number' } as any}
|
|
342
|
-
/>
|
|
343
|
-
);
|
|
344
|
-
// null ?? '' -> '' which means no value
|
|
345
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(null);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('EmailField treats null as empty', () => {
|
|
349
|
-
render(
|
|
350
|
-
<EmailField
|
|
351
|
-
value={null as any}
|
|
352
|
-
onChange={noop}
|
|
353
|
-
field={{ type: 'email' } as any}
|
|
354
|
-
/>
|
|
355
|
-
);
|
|
356
|
-
const input = screen.getByRole('textbox');
|
|
357
|
-
expect(input).toHaveValue('');
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('CurrencyField treats null as empty', () => {
|
|
361
|
-
render(
|
|
362
|
-
<CurrencyField
|
|
363
|
-
value={null as any}
|
|
364
|
-
onChange={noop}
|
|
365
|
-
field={{ type: 'currency' } as any}
|
|
366
|
-
/>
|
|
367
|
-
);
|
|
368
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(null);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('BooleanField treats null as false', () => {
|
|
372
|
-
render(
|
|
373
|
-
<BooleanField
|
|
374
|
-
value={null as any}
|
|
375
|
-
onChange={noop}
|
|
376
|
-
field={{ type: 'boolean' } as any}
|
|
377
|
-
/>
|
|
378
|
-
);
|
|
379
|
-
expect(screen.getByRole('switch')).not.toBeChecked();
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('SelectField treats null as empty', () => {
|
|
383
|
-
render(
|
|
384
|
-
<SelectField
|
|
385
|
-
value={null as any}
|
|
386
|
-
onChange={noop}
|
|
387
|
-
field={{ type: 'select', options: [{ label: 'A', value: 'a' }] } as any}
|
|
388
|
-
/>
|
|
389
|
-
);
|
|
390
|
-
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('TextField treats undefined as empty string', () => {
|
|
394
|
-
render(
|
|
395
|
-
<TextField
|
|
396
|
-
value={undefined as any}
|
|
397
|
-
onChange={noop}
|
|
398
|
-
field={{ type: 'text' } as any}
|
|
399
|
-
/>
|
|
400
|
-
);
|
|
401
|
-
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('NumberField treats undefined as empty', () => {
|
|
405
|
-
render(
|
|
406
|
-
<NumberField
|
|
407
|
-
value={undefined as any}
|
|
408
|
-
onChange={noop}
|
|
409
|
-
field={{ type: 'number' } as any}
|
|
410
|
-
/>
|
|
411
|
-
);
|
|
412
|
-
expect(screen.getByRole('spinbutton')).toHaveValue(null);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('BooleanField treats undefined as false', () => {
|
|
416
|
-
render(
|
|
417
|
-
<BooleanField
|
|
418
|
-
value={undefined as any}
|
|
419
|
-
onChange={noop}
|
|
420
|
-
field={{ type: 'boolean' } as any}
|
|
421
|
-
/>
|
|
422
|
-
);
|
|
423
|
-
expect(screen.getByRole('switch')).not.toBeChecked();
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('TextField treats empty string as empty', () => {
|
|
427
|
-
render(
|
|
428
|
-
<TextField
|
|
429
|
-
value=""
|
|
430
|
-
onChange={noop}
|
|
431
|
-
field={{ type: 'text' } as any}
|
|
432
|
-
/>
|
|
433
|
-
);
|
|
434
|
-
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
435
|
-
});
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// ---------------------------------------------------------------
|
|
439
|
-
// Special characters and XSS prevention
|
|
440
|
-
// ---------------------------------------------------------------
|
|
441
|
-
describe('special characters and XSS', () => {
|
|
442
|
-
it('TextField renders HTML entity string as literal text', () => {
|
|
443
|
-
const htmlEntities = '<b>bold</b>';
|
|
444
|
-
render(
|
|
445
|
-
<TextField
|
|
446
|
-
value={htmlEntities}
|
|
447
|
-
onChange={noop}
|
|
448
|
-
field={{ type: 'text' } as any}
|
|
449
|
-
/>
|
|
450
|
-
);
|
|
451
|
-
expect(screen.getByRole('textbox')).toHaveValue(htmlEntities);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it('TextField renders script tag as literal text', () => {
|
|
455
|
-
const xssPayload = '<script>alert("xss")</script>';
|
|
456
|
-
render(
|
|
457
|
-
<TextField
|
|
458
|
-
value={xssPayload}
|
|
459
|
-
onChange={noop}
|
|
460
|
-
field={{ type: 'text' } as any}
|
|
461
|
-
/>
|
|
462
|
-
);
|
|
463
|
-
expect(screen.getByRole('textbox')).toHaveValue(xssPayload);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it('TextField readonly renders script tag safely (no script element)', () => {
|
|
467
|
-
const xssPayload = '<script>alert("xss")</script>';
|
|
468
|
-
const { container } = render(
|
|
469
|
-
<TextField
|
|
470
|
-
value={xssPayload}
|
|
471
|
-
onChange={noop}
|
|
472
|
-
field={{ type: 'text' } as any}
|
|
473
|
-
readonly={true}
|
|
474
|
-
/>
|
|
475
|
-
);
|
|
476
|
-
expect(container.querySelector('script')).toBeNull();
|
|
477
|
-
expect(container.textContent).toContain(xssPayload);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('TextAreaField renders script tag as literal text', () => {
|
|
481
|
-
const xssPayload = '<script>alert("xss")</script>';
|
|
482
|
-
render(
|
|
483
|
-
<TextAreaField
|
|
484
|
-
value={xssPayload}
|
|
485
|
-
onChange={noop}
|
|
486
|
-
field={{ type: 'textarea' } as any}
|
|
487
|
-
/>
|
|
488
|
-
);
|
|
489
|
-
expect(screen.getByRole('textbox')).toHaveValue(xssPayload);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it('EmailField renders HTML injection safely', () => {
|
|
493
|
-
const htmlEmail = '"><script>alert(1)</script>@evil.com';
|
|
494
|
-
render(
|
|
495
|
-
<EmailField
|
|
496
|
-
value={htmlEmail}
|
|
497
|
-
onChange={noop}
|
|
498
|
-
field={{ type: 'email' } as any}
|
|
499
|
-
/>
|
|
500
|
-
);
|
|
501
|
-
expect(screen.getByDisplayValue(htmlEmail)).toBeInTheDocument();
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
it('UrlField renders malicious URL safely', () => {
|
|
505
|
-
const maliciousUrl = 'javascript:alert(1)';
|
|
506
|
-
render(
|
|
507
|
-
<UrlField
|
|
508
|
-
value={maliciousUrl}
|
|
509
|
-
onChange={noop}
|
|
510
|
-
field={{ type: 'url' } as any}
|
|
511
|
-
/>
|
|
512
|
-
);
|
|
513
|
-
expect(screen.getByDisplayValue(maliciousUrl)).toBeInTheDocument();
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
it('TextField handles SQL injection string as literal text', () => {
|
|
517
|
-
const sqlInjection = "'; DROP TABLE users; --";
|
|
518
|
-
render(
|
|
519
|
-
<TextField
|
|
520
|
-
value={sqlInjection}
|
|
521
|
-
onChange={noop}
|
|
522
|
-
field={{ type: 'text' } as any}
|
|
523
|
-
/>
|
|
524
|
-
);
|
|
525
|
-
expect(screen.getByRole('textbox')).toHaveValue(sqlInjection);
|
|
526
|
-
});
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// ---------------------------------------------------------------
|
|
530
|
-
// TextAreaField character counter
|
|
531
|
-
// ---------------------------------------------------------------
|
|
532
|
-
describe('TextAreaField character counter', () => {
|
|
533
|
-
it('shows character count when max_length is set', () => {
|
|
534
|
-
render(
|
|
535
|
-
<TextAreaField
|
|
536
|
-
value="Hello"
|
|
537
|
-
onChange={noop}
|
|
538
|
-
field={{ type: 'textarea', max_length: 100 } as any}
|
|
539
|
-
/>
|
|
540
|
-
);
|
|
541
|
-
expect(screen.getByText('5/100')).toBeInTheDocument();
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it('shows count at max_length limit', () => {
|
|
545
|
-
const maxStr = 'a'.repeat(50);
|
|
546
|
-
render(
|
|
547
|
-
<TextAreaField
|
|
548
|
-
value={maxStr}
|
|
549
|
-
onChange={noop}
|
|
550
|
-
field={{ type: 'textarea', max_length: 50 } as any}
|
|
551
|
-
/>
|
|
552
|
-
);
|
|
553
|
-
expect(screen.getByText('50/50')).toBeInTheDocument();
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('does not show count without max_length', () => {
|
|
557
|
-
const { container } = render(
|
|
558
|
-
<TextAreaField
|
|
559
|
-
value="Hello"
|
|
560
|
-
onChange={noop}
|
|
561
|
-
field={{ type: 'textarea' } as any}
|
|
562
|
-
/>
|
|
563
|
-
);
|
|
564
|
-
expect(container.querySelector('[aria-live="polite"]')).toBeNull();
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
});
|