@object-ui/plugin-form 3.3.0 → 3.3.2

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.
@@ -1,684 +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
- import { describe, it, expect, vi } from 'vitest';
10
- import { render, screen, waitFor } from '@testing-library/react';
11
- import React from 'react';
12
- import { ObjectForm } from '../ObjectForm';
13
- import { SplitForm } from '../SplitForm';
14
- import { DrawerForm } from '../DrawerForm';
15
- import { ModalForm } from '../ModalForm';
16
-
17
- // Mock dataSource used across tests
18
- const mockObjectSchema = {
19
- name: 'contacts',
20
- fields: {
21
- firstName: { label: 'First Name', type: 'text', required: true },
22
- lastName: { label: 'Last Name', type: 'text', required: false },
23
- email: { label: 'Email', type: 'email', required: true },
24
- phone: { label: 'Phone', type: 'phone', required: false },
25
- street: { label: 'Street', type: 'text', required: false },
26
- city: { label: 'City', type: 'text', required: false },
27
- },
28
- };
29
-
30
- const createMockDataSource = () => ({
31
- getObjectSchema: vi.fn().mockResolvedValue(mockObjectSchema),
32
- findOne: vi.fn().mockResolvedValue({ firstName: 'John', lastName: 'Doe' }),
33
- find: vi.fn().mockResolvedValue([]),
34
- create: vi.fn().mockResolvedValue({ id: '1' }),
35
- update: vi.fn().mockResolvedValue({ id: '1' }),
36
- delete: vi.fn().mockResolvedValue(true),
37
- });
38
-
39
- // ─── SplitForm Tests ────────────────────────────────────────────────────
40
-
41
- describe('SplitForm', () => {
42
- it('renders with split panel layout', async () => {
43
- const mockDataSource = createMockDataSource();
44
-
45
- render(
46
- <SplitForm
47
- schema={{
48
- type: 'object-form',
49
- formType: 'split',
50
- objectName: 'contacts',
51
- mode: 'create',
52
- sections: [
53
- {
54
- label: 'Personal',
55
- fields: ['firstName', 'lastName'],
56
- },
57
- {
58
- label: 'Contact',
59
- fields: ['email', 'phone'],
60
- },
61
- ],
62
- }}
63
- dataSource={mockDataSource as any}
64
- />
65
- );
66
-
67
- // Wait for schema to load
68
- await waitFor(() => {
69
- expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
70
- });
71
-
72
- // Should show section labels
73
- await waitFor(() => {
74
- expect(screen.getByText('Personal')).toBeInTheDocument();
75
- });
76
- expect(screen.getByText('Contact')).toBeInTheDocument();
77
- });
78
-
79
- it('renders loading state initially', () => {
80
- const mockDataSource = createMockDataSource();
81
- // Make getObjectSchema hang (never resolve)
82
- mockDataSource.getObjectSchema.mockReturnValue(new Promise(() => {}));
83
-
84
- render(
85
- <SplitForm
86
- schema={{
87
- type: 'object-form',
88
- formType: 'split',
89
- objectName: 'contacts',
90
- mode: 'create',
91
- sections: [
92
- { label: 'Section 1', fields: ['firstName'] },
93
- { label: 'Section 2', fields: ['email'] },
94
- ],
95
- }}
96
- dataSource={mockDataSource as any}
97
- />
98
- );
99
-
100
- expect(screen.getByText('Loading form...')).toBeInTheDocument();
101
- });
102
-
103
- it('shows error state on fetch failure', async () => {
104
- const mockDataSource = createMockDataSource();
105
- mockDataSource.getObjectSchema.mockRejectedValue(new Error('Network error'));
106
-
107
- render(
108
- <SplitForm
109
- schema={{
110
- type: 'object-form',
111
- formType: 'split',
112
- objectName: 'contacts',
113
- mode: 'create',
114
- sections: [
115
- { label: 'Section 1', fields: ['firstName'] },
116
- { label: 'Section 2', fields: ['email'] },
117
- ],
118
- }}
119
- dataSource={mockDataSource as any}
120
- />
121
- );
122
-
123
- await waitFor(() => {
124
- expect(screen.getByText('Error loading form')).toBeInTheDocument();
125
- });
126
- expect(screen.getByText('Network error')).toBeInTheDocument();
127
- });
128
- });
129
-
130
- // ─── DrawerForm Tests ───────────────────────────────────────────────────
131
-
132
- describe('DrawerForm', () => {
133
- it('renders inside a sheet with title and description', async () => {
134
- const mockDataSource = createMockDataSource();
135
-
136
- render(
137
- <DrawerForm
138
- schema={{
139
- type: 'object-form',
140
- formType: 'drawer',
141
- objectName: 'contacts',
142
- mode: 'create',
143
- title: 'Create Contact',
144
- description: 'Fill in the contact details',
145
- open: true,
146
- fields: ['firstName', 'lastName', 'email'],
147
- }}
148
- dataSource={mockDataSource as any}
149
- />
150
- );
151
-
152
- // Wait for schema and rendering
153
- await waitFor(() => {
154
- expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
155
- });
156
-
157
- // Title and description should be visible
158
- await waitFor(() => {
159
- expect(screen.getByText('Create Contact')).toBeInTheDocument();
160
- });
161
- expect(screen.getByText('Fill in the contact details')).toBeInTheDocument();
162
- });
163
-
164
- it('calls onOpenChange when closing', async () => {
165
- const onOpenChange = vi.fn();
166
- const mockDataSource = createMockDataSource();
167
-
168
- render(
169
- <DrawerForm
170
- schema={{
171
- type: 'object-form',
172
- formType: 'drawer',
173
- objectName: 'contacts',
174
- mode: 'create',
175
- title: 'Create Contact',
176
- open: true,
177
- onOpenChange,
178
- fields: ['firstName'],
179
- }}
180
- dataSource={mockDataSource as any}
181
- />
182
- );
183
-
184
- // The close button (X) in the Sheet should exist
185
- await waitFor(() => {
186
- const closeButton = screen.getByRole('button', { name: /close/i });
187
- expect(closeButton).toBeInTheDocument();
188
- });
189
- });
190
-
191
- it('renders with sections layout inside drawer', async () => {
192
- const mockDataSource = createMockDataSource();
193
-
194
- render(
195
- <DrawerForm
196
- schema={{
197
- type: 'object-form',
198
- formType: 'drawer',
199
- objectName: 'contacts',
200
- mode: 'create',
201
- title: 'New Contact',
202
- open: true,
203
- sections: [
204
- {
205
- label: 'Personal Info',
206
- fields: ['firstName', 'lastName'],
207
- columns: 2,
208
- },
209
- {
210
- label: 'Contact Details',
211
- fields: ['email', 'phone'],
212
- },
213
- ],
214
- }}
215
- dataSource={mockDataSource as any}
216
- />
217
- );
218
-
219
- await waitFor(() => {
220
- expect(screen.getByText('New Contact')).toBeInTheDocument();
221
- });
222
-
223
- await waitFor(() => {
224
- expect(screen.getByText('Personal Info')).toBeInTheDocument();
225
- });
226
- expect(screen.getByText('Contact Details')).toBeInTheDocument();
227
- });
228
-
229
- it('renders when open is false (hidden)', () => {
230
- const mockDataSource = createMockDataSource();
231
-
232
- const { container } = render(
233
- <DrawerForm
234
- schema={{
235
- type: 'object-form',
236
- formType: 'drawer',
237
- objectName: 'contacts',
238
- mode: 'create',
239
- open: false,
240
- fields: ['firstName'],
241
- }}
242
- dataSource={mockDataSource as any}
243
- />
244
- );
245
-
246
- // When open=false, the sheet should not render content
247
- expect(screen.queryByText('Create Contact')).not.toBeInTheDocument();
248
- });
249
- });
250
-
251
- // ─── ModalForm Tests ────────────────────────────────────────────────────
252
-
253
- describe('ModalForm', () => {
254
- it('renders inside a dialog with title and description', async () => {
255
- const mockDataSource = createMockDataSource();
256
-
257
- render(
258
- <ModalForm
259
- schema={{
260
- type: 'object-form',
261
- formType: 'modal',
262
- objectName: 'contacts',
263
- mode: 'create',
264
- title: 'Create Contact',
265
- description: 'Enter contact information',
266
- open: true,
267
- fields: ['firstName', 'lastName', 'email'],
268
- }}
269
- dataSource={mockDataSource as any}
270
- />
271
- );
272
-
273
- await waitFor(() => {
274
- expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
275
- });
276
-
277
- await waitFor(() => {
278
- expect(screen.getByText('Create Contact')).toBeInTheDocument();
279
- });
280
- expect(screen.getByText('Enter contact information')).toBeInTheDocument();
281
- });
282
-
283
- it('renders with sections layout inside modal', async () => {
284
- const mockDataSource = createMockDataSource();
285
-
286
- render(
287
- <ModalForm
288
- schema={{
289
- type: 'object-form',
290
- formType: 'modal',
291
- objectName: 'contacts',
292
- mode: 'create',
293
- title: 'New Contact',
294
- open: true,
295
- sections: [
296
- {
297
- label: 'Basic Info',
298
- fields: ['firstName', 'lastName'],
299
- columns: 2,
300
- },
301
- {
302
- label: 'Contact Details',
303
- fields: ['email', 'phone'],
304
- },
305
- ],
306
- }}
307
- dataSource={mockDataSource as any}
308
- />
309
- );
310
-
311
- await waitFor(() => {
312
- expect(screen.getByText('New Contact')).toBeInTheDocument();
313
- });
314
-
315
- await waitFor(() => {
316
- expect(screen.getByText('Basic Info')).toBeInTheDocument();
317
- });
318
- expect(screen.getByText('Contact Details')).toBeInTheDocument();
319
- });
320
-
321
- it('does not render content when open is false', () => {
322
- const mockDataSource = createMockDataSource();
323
-
324
- render(
325
- <ModalForm
326
- schema={{
327
- type: 'object-form',
328
- formType: 'modal',
329
- objectName: 'contacts',
330
- mode: 'create',
331
- title: 'Hidden Modal',
332
- open: false,
333
- fields: ['firstName'],
334
- }}
335
- dataSource={mockDataSource as any}
336
- />
337
- );
338
-
339
- expect(screen.queryByText('Hidden Modal')).not.toBeInTheDocument();
340
- });
341
-
342
- it('applies size class for large modal', async () => {
343
- const mockDataSource = createMockDataSource();
344
-
345
- render(
346
- <ModalForm
347
- schema={{
348
- type: 'object-form',
349
- formType: 'modal',
350
- objectName: 'contacts',
351
- mode: 'create',
352
- title: 'Large Modal',
353
- open: true,
354
- modalSize: 'xl',
355
- fields: ['firstName'],
356
- }}
357
- dataSource={mockDataSource as any}
358
- />
359
- );
360
-
361
- await waitFor(() => {
362
- expect(screen.getByText('Large Modal')).toBeInTheDocument();
363
- });
364
- });
365
-
366
- it('shows error state on data fetch failure', async () => {
367
- const mockDataSource = createMockDataSource();
368
- mockDataSource.getObjectSchema.mockRejectedValue(new Error('Server error'));
369
-
370
- render(
371
- <ModalForm
372
- schema={{
373
- type: 'object-form',
374
- formType: 'modal',
375
- objectName: 'contacts',
376
- mode: 'create',
377
- open: true,
378
- fields: ['firstName'],
379
- }}
380
- dataSource={mockDataSource as any}
381
- />
382
- );
383
-
384
- await waitFor(() => {
385
- expect(screen.getByText('Error loading form')).toBeInTheDocument();
386
- });
387
- expect(screen.getByText('Server error')).toBeInTheDocument();
388
- });
389
- });
390
-
391
- // ─── ObjectForm Routing Tests ───────────────────────────────────────────
392
-
393
- describe('ObjectForm routing to new variants', () => {
394
- it('routes formType=split to SplitForm', async () => {
395
- const mockDataSource = createMockDataSource();
396
-
397
- render(
398
- <ObjectForm
399
- schema={{
400
- type: 'object-form',
401
- objectName: 'contacts',
402
- mode: 'create',
403
- formType: 'split',
404
- sections: [
405
- { label: 'Left Panel', fields: ['firstName', 'lastName'] },
406
- { label: 'Right Panel', fields: ['email', 'phone'] },
407
- ],
408
- }}
409
- dataSource={mockDataSource as any}
410
- />
411
- );
412
-
413
- // Should load via SplitForm
414
- await waitFor(() => {
415
- expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
416
- });
417
-
418
- await waitFor(() => {
419
- expect(screen.getByText('Left Panel')).toBeInTheDocument();
420
- });
421
- expect(screen.getByText('Right Panel')).toBeInTheDocument();
422
- });
423
-
424
- it('routes formType=drawer to DrawerForm', async () => {
425
- const mockDataSource = createMockDataSource();
426
-
427
- render(
428
- <ObjectForm
429
- schema={{
430
- type: 'object-form',
431
- objectName: 'contacts',
432
- mode: 'create',
433
- formType: 'drawer',
434
- title: 'Drawer Form Title',
435
- open: true,
436
- fields: ['firstName', 'lastName'],
437
- }}
438
- dataSource={mockDataSource as any}
439
- />
440
- );
441
-
442
- await waitFor(() => {
443
- expect(screen.getByText('Drawer Form Title')).toBeInTheDocument();
444
- });
445
- });
446
-
447
- it('routes formType=modal to ModalForm', async () => {
448
- const mockDataSource = createMockDataSource();
449
-
450
- render(
451
- <ObjectForm
452
- schema={{
453
- type: 'object-form',
454
- objectName: 'contacts',
455
- mode: 'create',
456
- formType: 'modal',
457
- title: 'Modal Form Title',
458
- open: true,
459
- fields: ['firstName', 'lastName'],
460
- }}
461
- dataSource={mockDataSource as any}
462
- />
463
- );
464
-
465
- await waitFor(() => {
466
- expect(screen.getByText('Modal Form Title')).toBeInTheDocument();
467
- });
468
- });
469
- });
470
-
471
- // ─── Auto-Layout Integration Tests ──────────────────────────────────────
472
-
473
- /**
474
- * These tests verify that ModalForm and DrawerForm apply applyAutoLayout
475
- * when rendering flat fields (no sections), consistent with SimpleObjectForm.
476
- */
477
- describe('ModalForm auto-layout integration', () => {
478
- // Schema with 6 fields (>3) should trigger 2-column inference
479
- const manyFieldsSchema = {
480
- name: 'contacts',
481
- fields: {
482
- firstName: { label: 'First Name', type: 'text', required: true },
483
- lastName: { label: 'Last Name', type: 'text', required: false },
484
- email: { label: 'Email', type: 'email', required: true },
485
- phone: { label: 'Phone', type: 'phone', required: false },
486
- street: { label: 'Street', type: 'text', required: false },
487
- city: { label: 'City', type: 'text', required: false },
488
- },
489
- };
490
-
491
- it('applies auto-layout with 2 columns for 6 flat fields', async () => {
492
- const mockDataSource = createMockDataSource();
493
- mockDataSource.getObjectSchema.mockResolvedValue(manyFieldsSchema);
494
-
495
- render(
496
- <ModalForm
497
- schema={{
498
- type: 'object-form',
499
- formType: 'modal',
500
- objectName: 'contacts',
501
- mode: 'create',
502
- title: 'Auto Layout Modal',
503
- open: true,
504
- }}
505
- dataSource={mockDataSource as any}
506
- />
507
- );
508
-
509
- await waitFor(() => {
510
- expect(screen.getByText('Auto Layout Modal')).toBeInTheDocument();
511
- });
512
-
513
- // With 6 fields (>3), auto-layout should infer columns=2
514
- // The form element receives columns="2" as an attribute
515
- await waitFor(() => {
516
- const formEl = document.querySelector('form[columns="2"]');
517
- expect(formEl).not.toBeNull();
518
- });
519
- });
520
-
521
- it('does not apply multi-column for 3 or fewer fields', async () => {
522
- const fewFieldsSchema = {
523
- name: 'simple',
524
- fields: {
525
- firstName: { label: 'First Name', type: 'text', required: true },
526
- lastName: { label: 'Last Name', type: 'text', required: false },
527
- },
528
- };
529
- const mockDataSource = createMockDataSource();
530
- mockDataSource.getObjectSchema.mockResolvedValue(fewFieldsSchema);
531
-
532
- render(
533
- <ModalForm
534
- schema={{
535
- type: 'object-form',
536
- formType: 'modal',
537
- objectName: 'simple',
538
- mode: 'create',
539
- title: 'Few Fields Modal',
540
- open: true,
541
- }}
542
- dataSource={mockDataSource as any}
543
- />
544
- );
545
-
546
- await waitFor(() => {
547
- expect(screen.getByText('Few Fields Modal')).toBeInTheDocument();
548
- });
549
-
550
- // With 2 fields (≤3), auto-layout should infer columns=1
551
- await waitFor(() => {
552
- const formEl = document.querySelector('form[columns="2"]');
553
- expect(formEl).toBeNull();
554
- });
555
- });
556
-
557
- it('respects explicit columns override', async () => {
558
- const mockDataSource = createMockDataSource();
559
- mockDataSource.getObjectSchema.mockResolvedValue(manyFieldsSchema);
560
-
561
- render(
562
- <ModalForm
563
- schema={{
564
- type: 'object-form',
565
- formType: 'modal',
566
- objectName: 'contacts',
567
- mode: 'create',
568
- title: 'Explicit Columns Modal',
569
- open: true,
570
- columns: 3,
571
- }}
572
- dataSource={mockDataSource as any}
573
- />
574
- );
575
-
576
- await waitFor(() => {
577
- expect(screen.getByText('Explicit Columns Modal')).toBeInTheDocument();
578
- });
579
-
580
- // User specified columns=3
581
- await waitFor(() => {
582
- const formEl = document.querySelector('form[columns="3"]');
583
- expect(formEl).not.toBeNull();
584
- });
585
- });
586
- });
587
-
588
- describe('DrawerForm auto-layout integration', () => {
589
- const manyFieldsSchema = {
590
- name: 'contacts',
591
- fields: {
592
- firstName: { label: 'First Name', type: 'text', required: true },
593
- lastName: { label: 'Last Name', type: 'text', required: false },
594
- email: { label: 'Email', type: 'email', required: true },
595
- phone: { label: 'Phone', type: 'phone', required: false },
596
- street: { label: 'Street', type: 'text', required: false },
597
- city: { label: 'City', type: 'text', required: false },
598
- },
599
- };
600
-
601
- it('applies auto-layout with 2 columns for 6 flat fields', async () => {
602
- const mockDataSource = createMockDataSource();
603
- mockDataSource.getObjectSchema.mockResolvedValue(manyFieldsSchema);
604
-
605
- render(
606
- <DrawerForm
607
- schema={{
608
- type: 'object-form',
609
- formType: 'drawer',
610
- objectName: 'contacts',
611
- mode: 'create',
612
- title: 'Auto Layout Drawer',
613
- open: true,
614
- }}
615
- dataSource={mockDataSource as any}
616
- />
617
- );
618
-
619
- await waitFor(() => {
620
- expect(screen.getByText('Auto Layout Drawer')).toBeInTheDocument();
621
- });
622
-
623
- // With 6 fields (>3), auto-layout should infer columns=2
624
- await waitFor(() => {
625
- const formEl = document.querySelector('form[columns="2"]');
626
- expect(formEl).not.toBeNull();
627
- });
628
- });
629
-
630
- it('does not apply multi-column for 3 or fewer fields', async () => {
631
- const fewFieldsSchema = {
632
- name: 'simple',
633
- fields: {
634
- firstName: { label: 'First Name', type: 'text', required: true },
635
- lastName: { label: 'Last Name', type: 'text', required: false },
636
- },
637
- };
638
- const mockDataSource = createMockDataSource();
639
- mockDataSource.getObjectSchema.mockResolvedValue(fewFieldsSchema);
640
-
641
- render(
642
- <DrawerForm
643
- schema={{
644
- type: 'object-form',
645
- formType: 'drawer',
646
- objectName: 'simple',
647
- mode: 'create',
648
- title: 'Few Fields Drawer',
649
- open: true,
650
- }}
651
- dataSource={mockDataSource as any}
652
- />
653
- );
654
-
655
- await waitFor(() => {
656
- expect(screen.getByText('Few Fields Drawer')).toBeInTheDocument();
657
- });
658
-
659
- // With 2 fields (≤3), should remain 1 column
660
- await waitFor(() => {
661
- const formEl = document.querySelector('form[columns="2"]');
662
- expect(formEl).toBeNull();
663
- });
664
- });
665
- });
666
-
667
- // ─── Export Tests ────────────────────────────────────────────────────────
668
-
669
- describe('New variant exports', () => {
670
- it('exports SplitForm', () => {
671
- expect(SplitForm).toBeDefined();
672
- expect(typeof SplitForm).toBe('function');
673
- });
674
-
675
- it('exports DrawerForm', () => {
676
- expect(DrawerForm).toBeDefined();
677
- expect(typeof DrawerForm).toBe('function');
678
- });
679
-
680
- it('exports ModalForm', () => {
681
- expect(ModalForm).toBeDefined();
682
- expect(typeof ModalForm).toBe('function');
683
- });
684
- });