@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.
- package/CHANGELOG.md +21 -0
- package/README.md +21 -1
- package/dist/index.js +109 -66
- package/dist/index.umd.cjs +2 -2
- package/dist/packages/plugin-form/src/DrawerForm.d.ts +2 -0
- package/dist/packages/plugin-form/src/autoLayout.d.ts +11 -4
- package/package.json +42 -10
- package/.turbo/turbo-build.log +0 -32
- package/src/DrawerForm.tsx +0 -410
- package/src/EmbeddableForm.tsx +0 -240
- package/src/FormAnalytics.tsx +0 -209
- package/src/FormSection.tsx +0 -152
- package/src/FormVariants.test.tsx +0 -219
- package/src/ModalForm.tsx +0 -485
- package/src/ObjectForm.msw.test.tsx +0 -156
- package/src/ObjectForm.stories.tsx +0 -85
- package/src/ObjectForm.test.tsx +0 -61
- package/src/ObjectForm.tsx +0 -609
- package/src/SplitForm.tsx +0 -300
- package/src/TabbedForm.tsx +0 -395
- package/src/WizardForm.tsx +0 -502
- package/src/__tests__/EmbeddableFormPrefill.test.tsx +0 -186
- package/src/__tests__/MobileUX.test.tsx +0 -433
- package/src/__tests__/NewVariants.test.tsx +0 -684
- package/src/__tests__/autoLayout.test.ts +0 -339
- package/src/__tests__/form-validation-submit.test.tsx +0 -286
- package/src/autoLayout.ts +0 -166
- package/src/index.tsx +0 -134
- package/tsconfig.json +0 -9
- package/vite.config.ts +0 -58
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
isWideFieldType,
|
|
4
|
-
isAutoGeneratedFieldType,
|
|
5
|
-
inferColumns,
|
|
6
|
-
inferModalSize,
|
|
7
|
-
applyAutoColSpan,
|
|
8
|
-
filterCreateModeFields,
|
|
9
|
-
applyAutoLayout,
|
|
10
|
-
} from '../autoLayout';
|
|
11
|
-
import type { FormField } from '@object-ui/types';
|
|
12
|
-
|
|
13
|
-
describe('autoLayout', () => {
|
|
14
|
-
describe('isWideFieldType', () => {
|
|
15
|
-
it('returns true for wide form field types', () => {
|
|
16
|
-
expect(isWideFieldType('field:textarea')).toBe(true);
|
|
17
|
-
expect(isWideFieldType('field:markdown')).toBe(true);
|
|
18
|
-
expect(isWideFieldType('field:html')).toBe(true);
|
|
19
|
-
expect(isWideFieldType('field:grid')).toBe(true);
|
|
20
|
-
expect(isWideFieldType('field:rich-text')).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('returns true for raw wide field types', () => {
|
|
24
|
-
expect(isWideFieldType('textarea')).toBe(true);
|
|
25
|
-
expect(isWideFieldType('markdown')).toBe(true);
|
|
26
|
-
expect(isWideFieldType('html')).toBe(true);
|
|
27
|
-
expect(isWideFieldType('grid')).toBe(true);
|
|
28
|
-
expect(isWideFieldType('rich-text')).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns false for narrow field types', () => {
|
|
32
|
-
expect(isWideFieldType('field:text')).toBe(false);
|
|
33
|
-
expect(isWideFieldType('field:number')).toBe(false);
|
|
34
|
-
expect(isWideFieldType('field:select')).toBe(false);
|
|
35
|
-
expect(isWideFieldType('text')).toBe(false);
|
|
36
|
-
expect(isWideFieldType('boolean')).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('isAutoGeneratedFieldType', () => {
|
|
41
|
-
it('returns true for auto-generated types', () => {
|
|
42
|
-
expect(isAutoGeneratedFieldType('formula')).toBe(true);
|
|
43
|
-
expect(isAutoGeneratedFieldType('summary')).toBe(true);
|
|
44
|
-
expect(isAutoGeneratedFieldType('auto_number')).toBe(true);
|
|
45
|
-
expect(isAutoGeneratedFieldType('autonumber')).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('returns false for user-editable types', () => {
|
|
49
|
-
expect(isAutoGeneratedFieldType('text')).toBe(false);
|
|
50
|
-
expect(isAutoGeneratedFieldType('number')).toBe(false);
|
|
51
|
-
expect(isAutoGeneratedFieldType('select')).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('inferColumns', () => {
|
|
56
|
-
it('returns 1 column for 0 fields', () => {
|
|
57
|
-
expect(inferColumns(0)).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('returns 1 column for 1-3 fields', () => {
|
|
61
|
-
expect(inferColumns(1)).toBe(1);
|
|
62
|
-
expect(inferColumns(2)).toBe(1);
|
|
63
|
-
expect(inferColumns(3)).toBe(1);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns 2 columns for 4+ fields', () => {
|
|
67
|
-
expect(inferColumns(4)).toBe(2);
|
|
68
|
-
expect(inferColumns(8)).toBe(2);
|
|
69
|
-
expect(inferColumns(20)).toBe(2);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('inferModalSize', () => {
|
|
74
|
-
it('returns default for 0 or 1 column', () => {
|
|
75
|
-
expect(inferModalSize(0)).toBe('default');
|
|
76
|
-
expect(inferModalSize(1)).toBe('default');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns xl for 2 columns', () => {
|
|
80
|
-
expect(inferModalSize(2)).toBe('xl');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('returns full for 3+ columns', () => {
|
|
84
|
-
expect(inferModalSize(3)).toBe('full');
|
|
85
|
-
expect(inferModalSize(4)).toBe('full');
|
|
86
|
-
expect(inferModalSize(5)).toBe('full');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe('applyAutoColSpan', () => {
|
|
91
|
-
it('returns fields unchanged when columns is 1', () => {
|
|
92
|
-
const fields: FormField[] = [
|
|
93
|
-
{ name: 'a', label: 'A', type: 'field:textarea' },
|
|
94
|
-
];
|
|
95
|
-
const result = applyAutoColSpan(fields, 1);
|
|
96
|
-
expect(result).toEqual(fields);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('sets colSpan for wide fields in multi-column layout', () => {
|
|
100
|
-
const fields: FormField[] = [
|
|
101
|
-
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
102
|
-
{ name: 'desc', label: 'Description', type: 'field:textarea' },
|
|
103
|
-
{ name: 'notes', label: 'Notes', type: 'field:markdown' },
|
|
104
|
-
];
|
|
105
|
-
const result = applyAutoColSpan(fields, 2);
|
|
106
|
-
|
|
107
|
-
expect(result[0].colSpan).toBeUndefined();
|
|
108
|
-
expect(result[1].colSpan).toBe(2);
|
|
109
|
-
expect(result[2].colSpan).toBe(2);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('does not override user-defined colSpan', () => {
|
|
113
|
-
const fields: FormField[] = [
|
|
114
|
-
{ name: 'desc', label: 'Description', type: 'field:textarea', colSpan: 1 },
|
|
115
|
-
];
|
|
116
|
-
const result = applyAutoColSpan(fields, 2);
|
|
117
|
-
expect(result[0].colSpan).toBe(1);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('does not mutate original fields', () => {
|
|
121
|
-
const fields: FormField[] = [
|
|
122
|
-
{ name: 'desc', label: 'Description', type: 'field:textarea' },
|
|
123
|
-
];
|
|
124
|
-
const result = applyAutoColSpan(fields, 2);
|
|
125
|
-
expect(fields[0].colSpan).toBeUndefined();
|
|
126
|
-
expect(result[0].colSpan).toBe(2);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('filterCreateModeFields', () => {
|
|
131
|
-
const objectSchema = {
|
|
132
|
-
name: 'test',
|
|
133
|
-
fields: {
|
|
134
|
-
name: { type: 'text', label: 'Name' },
|
|
135
|
-
total: { type: 'formula', label: 'Total' },
|
|
136
|
-
count: { type: 'summary', label: 'Count' },
|
|
137
|
-
record_no: { type: 'auto_number', label: 'Record #' },
|
|
138
|
-
email: { type: 'email', label: 'Email' },
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
it('filters out formula, summary, and auto_number fields', () => {
|
|
143
|
-
const fields: FormField[] = [
|
|
144
|
-
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
145
|
-
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
146
|
-
{ name: 'count', label: 'Count', type: 'field:text' },
|
|
147
|
-
{ name: 'record_no', label: 'Record #', type: 'field:text' },
|
|
148
|
-
{ name: 'email', label: 'Email', type: 'field:text' },
|
|
149
|
-
];
|
|
150
|
-
|
|
151
|
-
const result = filterCreateModeFields(fields, objectSchema);
|
|
152
|
-
|
|
153
|
-
expect(result).toHaveLength(2);
|
|
154
|
-
expect(result.map(f => f.name)).toEqual(['name', 'email']);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('keeps all fields when objectSchema has no fields metadata', () => {
|
|
158
|
-
const fields: FormField[] = [
|
|
159
|
-
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
160
|
-
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
161
|
-
];
|
|
162
|
-
|
|
163
|
-
const result = filterCreateModeFields(fields, { name: 'test' });
|
|
164
|
-
expect(result).toHaveLength(2);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('keeps custom fields not in object schema', () => {
|
|
168
|
-
const fields: FormField[] = [
|
|
169
|
-
{ name: 'custom_field', label: 'Custom', type: 'field:text' },
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
const result = filterCreateModeFields(fields, objectSchema);
|
|
173
|
-
expect(result).toHaveLength(1);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('applyAutoLayout', () => {
|
|
178
|
-
it('infers 1 column for 3 fields', () => {
|
|
179
|
-
const fields: FormField[] = [
|
|
180
|
-
{ name: 'a', label: 'A', type: 'field:text' },
|
|
181
|
-
{ name: 'b', label: 'B', type: 'field:number' },
|
|
182
|
-
{ name: 'c', label: 'C', type: 'field:select' },
|
|
183
|
-
];
|
|
184
|
-
|
|
185
|
-
const result = applyAutoLayout(fields, null, undefined, 'create');
|
|
186
|
-
expect(result.columns).toBe(1);
|
|
187
|
-
expect(result.fields).toHaveLength(3);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('infers 2 columns for 5 fields', () => {
|
|
191
|
-
const fields: FormField[] = [
|
|
192
|
-
{ name: 'a', label: 'A', type: 'field:text' },
|
|
193
|
-
{ name: 'b', label: 'B', type: 'field:text' },
|
|
194
|
-
{ name: 'c', label: 'C', type: 'field:text' },
|
|
195
|
-
{ name: 'd', label: 'D', type: 'field:text' },
|
|
196
|
-
{ name: 'e', label: 'E', type: 'field:text' },
|
|
197
|
-
];
|
|
198
|
-
|
|
199
|
-
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
200
|
-
expect(result.columns).toBe(2);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('applies colSpan to wide fields when columns > 1', () => {
|
|
204
|
-
const fields: FormField[] = [
|
|
205
|
-
{ name: 'a', label: 'A', type: 'field:text' },
|
|
206
|
-
{ name: 'b', label: 'B', type: 'field:text' },
|
|
207
|
-
{ name: 'c', label: 'C', type: 'field:text' },
|
|
208
|
-
{ name: 'd', label: 'D', type: 'field:textarea' },
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
212
|
-
expect(result.columns).toBe(2);
|
|
213
|
-
expect(result.fields[3].colSpan).toBe(2);
|
|
214
|
-
// Regular fields should not have colSpan
|
|
215
|
-
expect(result.fields[0].colSpan).toBeUndefined();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('filters auto-generated fields in create mode', () => {
|
|
219
|
-
const fields: FormField[] = [
|
|
220
|
-
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
221
|
-
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
222
|
-
{ name: 'email', label: 'Email', type: 'field:text' },
|
|
223
|
-
{ name: 'count', label: 'Count', type: 'field:text' },
|
|
224
|
-
];
|
|
225
|
-
|
|
226
|
-
const objectSchema = {
|
|
227
|
-
name: 'test',
|
|
228
|
-
fields: {
|
|
229
|
-
name: { type: 'text' },
|
|
230
|
-
total: { type: 'formula' },
|
|
231
|
-
email: { type: 'email' },
|
|
232
|
-
count: { type: 'summary' },
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const result = applyAutoLayout(fields, objectSchema, undefined, 'create');
|
|
237
|
-
expect(result.fields.map(f => f.name)).toEqual(['name', 'email']);
|
|
238
|
-
expect(result.columns).toBe(1); // Only 2 fields after filtering → 1 column
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('does not filter auto-generated fields in edit mode', () => {
|
|
242
|
-
const fields: FormField[] = [
|
|
243
|
-
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
244
|
-
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
245
|
-
];
|
|
246
|
-
|
|
247
|
-
const objectSchema = {
|
|
248
|
-
name: 'test',
|
|
249
|
-
fields: {
|
|
250
|
-
name: { type: 'text' },
|
|
251
|
-
total: { type: 'formula' },
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const result = applyAutoLayout(fields, objectSchema, undefined, 'edit');
|
|
256
|
-
expect(result.fields).toHaveLength(2);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('respects user-provided columns', () => {
|
|
260
|
-
const fields: FormField[] = [
|
|
261
|
-
{ name: 'a', label: 'A', type: 'field:text' },
|
|
262
|
-
{ name: 'b', label: 'B', type: 'field:text' },
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
const result = applyAutoLayout(fields, null, 3, 'edit');
|
|
266
|
-
expect(result.columns).toBe(3);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('applies auto colSpan even with user-provided columns', () => {
|
|
270
|
-
const fields: FormField[] = [
|
|
271
|
-
{ name: 'a', label: 'A', type: 'field:text' },
|
|
272
|
-
{ name: 'b', label: 'B', type: 'field:textarea' },
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
const result = applyAutoLayout(fields, null, 3, 'edit');
|
|
276
|
-
expect(result.columns).toBe(3);
|
|
277
|
-
expect(result.fields[1].colSpan).toBe(3);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('does not override user-defined colSpan on fields', () => {
|
|
281
|
-
const fields: FormField[] = [
|
|
282
|
-
{ name: 'a', label: 'A', type: 'field:text' },
|
|
283
|
-
{ name: 'b', label: 'B', type: 'field:textarea', colSpan: 1 },
|
|
284
|
-
{ name: 'c', label: 'C', type: 'field:text' },
|
|
285
|
-
{ name: 'd', label: 'D', type: 'field:text' },
|
|
286
|
-
];
|
|
287
|
-
|
|
288
|
-
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
289
|
-
expect(result.columns).toBe(2);
|
|
290
|
-
expect(result.fields[1].colSpan).toBe(1); // User override preserved
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('does not mutate original fields array', () => {
|
|
294
|
-
const fields: FormField[] = [
|
|
295
|
-
{ name: 'a', label: 'A', type: 'field:text' },
|
|
296
|
-
{ name: 'b', label: 'B', type: 'field:textarea' },
|
|
297
|
-
{ name: 'c', label: 'C', type: 'field:text' },
|
|
298
|
-
{ name: 'd', label: 'D', type: 'field:text' },
|
|
299
|
-
];
|
|
300
|
-
|
|
301
|
-
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
302
|
-
expect(fields[1].colSpan).toBeUndefined();
|
|
303
|
-
expect(result.fields[1].colSpan).toBe(2);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('handles empty fields array', () => {
|
|
307
|
-
const result = applyAutoLayout([], null, undefined, 'create');
|
|
308
|
-
expect(result.fields).toEqual([]);
|
|
309
|
-
expect(result.columns).toBe(1);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('infers columns based on field count after create-mode filtering', () => {
|
|
313
|
-
// Start with 5 fields, but 3 are auto-generated → 2 remain → 1 column
|
|
314
|
-
const fields: FormField[] = [
|
|
315
|
-
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
316
|
-
{ name: 'f1', label: 'F1', type: 'field:text' },
|
|
317
|
-
{ name: 'f2', label: 'F2', type: 'field:text' },
|
|
318
|
-
{ name: 'f3', label: 'F3', type: 'field:text' },
|
|
319
|
-
{ name: 'f4', label: 'F4', type: 'field:text' },
|
|
320
|
-
];
|
|
321
|
-
|
|
322
|
-
const objectSchema = {
|
|
323
|
-
name: 'test',
|
|
324
|
-
fields: {
|
|
325
|
-
name: { type: 'text' },
|
|
326
|
-
f1: { type: 'formula' },
|
|
327
|
-
f2: { type: 'summary' },
|
|
328
|
-
f3: { type: 'auto_number' },
|
|
329
|
-
f4: { type: 'text' },
|
|
330
|
-
},
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
const result = applyAutoLayout(fields, objectSchema, undefined, 'create');
|
|
334
|
-
// Only 'name' and 'f4' remain after filtering
|
|
335
|
-
expect(result.fields).toHaveLength(2);
|
|
336
|
-
expect(result.columns).toBe(1);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
});
|
|
@@ -1,286 +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.4 Integration Test — Form Validation + Submit Workflow
|
|
11
|
-
*
|
|
12
|
-
* Tests the full lifecycle: required-field validation → inline errors →
|
|
13
|
-
* successful submit → form reset.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
17
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
18
|
-
import userEvent from '@testing-library/user-event';
|
|
19
|
-
import React from 'react';
|
|
20
|
-
import { ObjectForm } from '../ObjectForm';
|
|
21
|
-
|
|
22
|
-
// ─── Shared Fixtures ────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
const mockObjectSchema = {
|
|
25
|
-
name: 'contacts',
|
|
26
|
-
fields: {
|
|
27
|
-
firstName: { label: 'First Name', type: 'text', required: true },
|
|
28
|
-
lastName: { label: 'Last Name', type: 'text', required: false },
|
|
29
|
-
email: { label: 'Email', type: 'email', required: true },
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const createMockDataSource = () => ({
|
|
34
|
-
getObjectSchema: vi.fn().mockResolvedValue(mockObjectSchema),
|
|
35
|
-
findOne: vi.fn().mockResolvedValue({}),
|
|
36
|
-
find: vi.fn().mockResolvedValue([]),
|
|
37
|
-
create: vi.fn().mockResolvedValue({ id: '1' }),
|
|
38
|
-
update: vi.fn().mockResolvedValue({ id: '1' }),
|
|
39
|
-
delete: vi.fn().mockResolvedValue(true),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// ─── Tests ──────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
describe('P3.4 Form Validation + Submit Workflow', () => {
|
|
45
|
-
let mockDataSource: ReturnType<typeof createMockDataSource>;
|
|
46
|
-
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
mockDataSource = createMockDataSource();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// --- Required field validation before submit ---
|
|
52
|
-
|
|
53
|
-
it('validates required fields and prevents submission when empty', async () => {
|
|
54
|
-
const user = userEvent.setup();
|
|
55
|
-
const onSuccess = vi.fn();
|
|
56
|
-
|
|
57
|
-
render(
|
|
58
|
-
<ObjectForm
|
|
59
|
-
schema={{
|
|
60
|
-
type: 'object-form',
|
|
61
|
-
objectName: 'contacts',
|
|
62
|
-
mode: 'create',
|
|
63
|
-
fields: ['firstName', 'email'],
|
|
64
|
-
onSuccess,
|
|
65
|
-
}}
|
|
66
|
-
dataSource={mockDataSource as any}
|
|
67
|
-
/>,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
// Wait for the form to load
|
|
71
|
-
await waitFor(() => {
|
|
72
|
-
expect(screen.getByText('First Name')).toBeInTheDocument();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Click submit without filling in required fields
|
|
76
|
-
const submitButton = screen.getByRole('button', { name: /create/i });
|
|
77
|
-
await user.click(submitButton);
|
|
78
|
-
|
|
79
|
-
// The data source should NOT have been called
|
|
80
|
-
await waitFor(() => {
|
|
81
|
-
expect(mockDataSource.create).not.toHaveBeenCalled();
|
|
82
|
-
});
|
|
83
|
-
expect(onSuccess).not.toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// --- Inline validation errors ---
|
|
87
|
-
|
|
88
|
-
it('shows inline validation error messages for required fields', async () => {
|
|
89
|
-
const user = userEvent.setup();
|
|
90
|
-
|
|
91
|
-
render(
|
|
92
|
-
<ObjectForm
|
|
93
|
-
schema={{
|
|
94
|
-
type: 'object-form',
|
|
95
|
-
objectName: 'contacts',
|
|
96
|
-
mode: 'create',
|
|
97
|
-
fields: ['firstName', 'email'],
|
|
98
|
-
}}
|
|
99
|
-
dataSource={mockDataSource as any}
|
|
100
|
-
/>,
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
await waitFor(() => {
|
|
104
|
-
expect(screen.getByText('First Name')).toBeInTheDocument();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Submit with empty required fields
|
|
108
|
-
const submitButton = screen.getByRole('button', { name: /create/i });
|
|
109
|
-
await user.click(submitButton);
|
|
110
|
-
|
|
111
|
-
// Validation messages should appear
|
|
112
|
-
await waitFor(() => {
|
|
113
|
-
expect(screen.getByText(/first name is required/i)).toBeInTheDocument();
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// --- Successful submit calls handler ---
|
|
118
|
-
|
|
119
|
-
it('calls onSuccess handler after successful submission', async () => {
|
|
120
|
-
const user = userEvent.setup();
|
|
121
|
-
const onSuccess = vi.fn();
|
|
122
|
-
|
|
123
|
-
render(
|
|
124
|
-
<ObjectForm
|
|
125
|
-
schema={{
|
|
126
|
-
type: 'object-form',
|
|
127
|
-
objectName: 'contacts',
|
|
128
|
-
mode: 'create',
|
|
129
|
-
fields: ['firstName', 'email'],
|
|
130
|
-
onSuccess,
|
|
131
|
-
}}
|
|
132
|
-
dataSource={mockDataSource as any}
|
|
133
|
-
/>,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Wait for the form to load
|
|
137
|
-
await waitFor(() => {
|
|
138
|
-
expect(screen.getByText('First Name')).toBeInTheDocument();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Fill in the required fields
|
|
142
|
-
const inputs = screen.getAllByRole('textbox');
|
|
143
|
-
// First Name input
|
|
144
|
-
await user.type(inputs[0], 'John');
|
|
145
|
-
// Email input
|
|
146
|
-
await user.type(inputs[1], 'john@example.com');
|
|
147
|
-
|
|
148
|
-
// Submit
|
|
149
|
-
const submitButton = screen.getByRole('button', { name: /create/i });
|
|
150
|
-
await user.click(submitButton);
|
|
151
|
-
|
|
152
|
-
// dataSource.create should have been called
|
|
153
|
-
await waitFor(() => {
|
|
154
|
-
expect(mockDataSource.create).toHaveBeenCalledWith(
|
|
155
|
-
'contacts',
|
|
156
|
-
expect.objectContaining({
|
|
157
|
-
firstName: 'John',
|
|
158
|
-
email: 'john@example.com',
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// onSuccess should have been called with result
|
|
164
|
-
await waitFor(() => {
|
|
165
|
-
expect(onSuccess).toHaveBeenCalledWith({ id: '1' });
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// --- Form reset after submit ---
|
|
170
|
-
|
|
171
|
-
it('resets form fields after successful submission when showReset is true', async () => {
|
|
172
|
-
const user = userEvent.setup();
|
|
173
|
-
|
|
174
|
-
render(
|
|
175
|
-
<ObjectForm
|
|
176
|
-
schema={{
|
|
177
|
-
type: 'object-form',
|
|
178
|
-
objectName: 'contacts',
|
|
179
|
-
mode: 'create',
|
|
180
|
-
fields: ['firstName', 'email'],
|
|
181
|
-
showReset: true,
|
|
182
|
-
onSuccess: vi.fn(),
|
|
183
|
-
}}
|
|
184
|
-
dataSource={mockDataSource as any}
|
|
185
|
-
/>,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
await waitFor(() => {
|
|
189
|
-
expect(screen.getByText('First Name')).toBeInTheDocument();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const inputs = screen.getAllByRole('textbox');
|
|
193
|
-
await user.type(inputs[0], 'Jane');
|
|
194
|
-
await user.type(inputs[1], 'jane@example.com');
|
|
195
|
-
|
|
196
|
-
// Submit
|
|
197
|
-
const submitButton = screen.getByRole('button', { name: /create/i });
|
|
198
|
-
await user.click(submitButton);
|
|
199
|
-
|
|
200
|
-
// After successful submit the inputs should be cleared
|
|
201
|
-
await waitFor(() => {
|
|
202
|
-
expect(mockDataSource.create).toHaveBeenCalled();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
await waitFor(() => {
|
|
206
|
-
expect(inputs[0]).toHaveValue('');
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// --- Inline-field form (no dataSource) ---
|
|
211
|
-
|
|
212
|
-
it('supports inline customFields without a dataSource', async () => {
|
|
213
|
-
const user = userEvent.setup();
|
|
214
|
-
const onSuccess = vi.fn();
|
|
215
|
-
|
|
216
|
-
render(
|
|
217
|
-
<ObjectForm
|
|
218
|
-
schema={{
|
|
219
|
-
type: 'object-form',
|
|
220
|
-
objectName: 'inline_form',
|
|
221
|
-
mode: 'create',
|
|
222
|
-
customFields: [
|
|
223
|
-
{ name: 'username', label: 'Username', type: 'input', required: true },
|
|
224
|
-
],
|
|
225
|
-
onSuccess,
|
|
226
|
-
}}
|
|
227
|
-
/>,
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
await waitFor(() => {
|
|
231
|
-
expect(screen.getByText('Username')).toBeInTheDocument();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Submit empty — should not succeed
|
|
235
|
-
const submitButton = screen.getByRole('button', { name: /create/i });
|
|
236
|
-
await user.click(submitButton);
|
|
237
|
-
expect(onSuccess).not.toHaveBeenCalled();
|
|
238
|
-
|
|
239
|
-
// Fill in and resubmit
|
|
240
|
-
const input = screen.getByRole('textbox');
|
|
241
|
-
await user.type(input, 'admin');
|
|
242
|
-
await user.click(submitButton);
|
|
243
|
-
|
|
244
|
-
await waitFor(() => {
|
|
245
|
-
expect(onSuccess).toHaveBeenCalledWith(
|
|
246
|
-
expect.objectContaining({ username: 'admin' }),
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
// --- Error callback on failed submission ---
|
|
252
|
-
|
|
253
|
-
it('calls onError when dataSource.create rejects', async () => {
|
|
254
|
-
const user = userEvent.setup();
|
|
255
|
-
const onError = vi.fn();
|
|
256
|
-
mockDataSource.create.mockRejectedValueOnce(new Error('Network failure'));
|
|
257
|
-
|
|
258
|
-
render(
|
|
259
|
-
<ObjectForm
|
|
260
|
-
schema={{
|
|
261
|
-
type: 'object-form',
|
|
262
|
-
objectName: 'contacts',
|
|
263
|
-
mode: 'create',
|
|
264
|
-
fields: ['firstName', 'email'],
|
|
265
|
-
onError,
|
|
266
|
-
}}
|
|
267
|
-
dataSource={mockDataSource as any}
|
|
268
|
-
/>,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
await waitFor(() => {
|
|
272
|
-
expect(screen.getByText('First Name')).toBeInTheDocument();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const inputs = screen.getAllByRole('textbox');
|
|
276
|
-
await user.type(inputs[0], 'John');
|
|
277
|
-
await user.type(inputs[1], 'john@example.com');
|
|
278
|
-
|
|
279
|
-
const submitButton = screen.getByRole('button', { name: /create/i });
|
|
280
|
-
await user.click(submitButton);
|
|
281
|
-
|
|
282
|
-
await waitFor(() => {
|
|
283
|
-
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
});
|