@object-ui/fields 4.0.1 → 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.
Files changed (26) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +7 -7
  3. package/dist/packages/fields/src/__tests__/boolean-checkbox.test.d.ts +0 -0
  4. package/dist/packages/fields/src/__tests__/cell-renderers.test.d.ts +0 -0
  5. package/dist/packages/fields/src/__tests__/datetime-cell.test.d.ts +0 -0
  6. package/dist/packages/fields/src/__tests__/datetime-timezone.test.d.ts +0 -0
  7. package/dist/packages/fields/src/__tests__/extreme-inputs.test.d.ts +0 -0
  8. package/dist/packages/fields/src/__tests__/form-variants.test.d.ts +0 -0
  9. package/dist/packages/fields/src/lookup-depends-on.test.d.ts +0 -1
  10. package/dist/packages/fields/src/new-widgets.test.d.ts +0 -8
  11. package/dist/packages/fields/src/record-picker.test.d.ts +0 -1
  12. package/dist/packages/fields/src/remaining-widgets.test.d.ts +0 -1
  13. package/dist/packages/fields/src/text-rich-widgets.test.d.ts +0 -1
  14. package/dist/packages/fields/src/visual-widgets.test.d.ts +0 -1
  15. package/src/__tests__/boolean-checkbox.test.tsx +0 -74
  16. package/src/__tests__/cell-renderers.test.tsx +0 -1193
  17. package/src/__tests__/datetime-cell.test.tsx +0 -46
  18. package/src/__tests__/datetime-timezone.test.tsx +0 -770
  19. package/src/__tests__/extreme-inputs.test.tsx +0 -567
  20. package/src/__tests__/form-variants.test.tsx +0 -306
  21. package/src/lookup-depends-on.test.tsx +0 -130
  22. package/src/new-widgets.test.tsx +0 -302
  23. package/src/record-picker.test.tsx +0 -1703
  24. package/src/remaining-widgets.test.tsx +0 -211
  25. package/src/text-rich-widgets.test.tsx +0 -104
  26. 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
- });