@finsys/core 1.1.0 → 1.2.0
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/dist/index.cjs +9101 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +375 -0
- package/dist/index.d.ts +374 -18
- package/dist/index.js +9038 -31
- package/dist/index.js.map +1 -0
- package/package.json +12 -10
- package/dist/catalogs.d.ts +0 -13
- package/dist/catalogs.js +0 -35
- package/dist/catalogs.test.d.ts +0 -1
- package/dist/catalogs.test.js +0 -207
- package/dist/form-field-category.d.ts +0 -12
- package/dist/form-field-category.js +0 -21
- package/dist/form-field-validator.d.ts +0 -18
- package/dist/form-field-validator.js +0 -41
- package/dist/form-field.d.ts +0 -81
- package/dist/form-field.js +0 -197
- package/dist/form-field.test.d.ts +0 -1
- package/dist/form-field.test.js +0 -482
- package/dist/form-spec.d.ts +0 -39
- package/dist/form-spec.js +0 -278
- package/dist/form-spec.test.d.ts +0 -1
- package/dist/form-spec.test.js +0 -322
- package/dist/rhf-generator.d.ts +0 -30
- package/dist/rhf-generator.js +0 -267
- package/dist/rhf-generator.test.d.ts +0 -1
- package/dist/rhf-generator.test.js +0 -583
- package/dist/survey-generator.d.ts +0 -153
- package/dist/survey-generator.js +0 -194
- package/dist/survey-generator.test.d.ts +0 -1
- package/dist/survey-generator.test.js +0 -332
- package/dist/utils.d.ts +0 -13
- package/dist/utils.js +0 -56
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -131
- package/dist/validator.d.ts +0 -14
- package/dist/validator.js +0 -49
- package/dist/validator.test.d.ts +0 -1
- package/dist/validator.test.js +0 -142
|
@@ -1,583 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2025 Sisters Inspire Sdn Bhd
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { describe, it, expect } from 'vitest';
|
|
17
|
-
import { generateRHFSchema, getStepSchema, getStepDefaultValues } from './rhf-generator.js';
|
|
18
|
-
describe('rhf-generator', () => {
|
|
19
|
-
const mockConfig = {
|
|
20
|
-
displayName: 'Test Loan',
|
|
21
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
22
|
-
fields: {
|
|
23
|
-
carPrice: {
|
|
24
|
-
type: 'text',
|
|
25
|
-
inputType: 'number',
|
|
26
|
-
displayName: 'Car Price',
|
|
27
|
-
category: 'general',
|
|
28
|
-
min: 1000,
|
|
29
|
-
max: 500000
|
|
30
|
-
},
|
|
31
|
-
fullName: {
|
|
32
|
-
type: 'text',
|
|
33
|
-
displayName: 'Full Name',
|
|
34
|
-
category: 'general',
|
|
35
|
-
required: true,
|
|
36
|
-
validators: [
|
|
37
|
-
{ type: 'regex', regex: '^[A-Za-z ]+$', text: 'Alpha only' }
|
|
38
|
-
]
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
pages: [
|
|
42
|
-
{
|
|
43
|
-
id: 'step1',
|
|
44
|
-
title: 'Step 1',
|
|
45
|
-
fields: ['carPrice', 'fullName']
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
};
|
|
49
|
-
it('should generate required schemas by default', () => {
|
|
50
|
-
const { baseSchema } = generateRHFSchema(mockConfig);
|
|
51
|
-
const shape = baseSchema.shape;
|
|
52
|
-
// carPrice is required by default (even if not explicitly required: true in config)
|
|
53
|
-
const result = shape.carPrice.safeParse(undefined);
|
|
54
|
-
expect(result.success).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
it('should generate required schemas when required is true', () => {
|
|
57
|
-
const { baseSchema } = generateRHFSchema(mockConfig);
|
|
58
|
-
const shape = baseSchema.shape;
|
|
59
|
-
// fullName is required
|
|
60
|
-
const result = shape.fullName.safeParse('');
|
|
61
|
-
expect(result.success).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
it('should apply numeric constraints (min/max) even to fields', () => {
|
|
64
|
-
const { baseSchema } = generateRHFSchema(mockConfig);
|
|
65
|
-
const shape = baseSchema.shape;
|
|
66
|
-
// Below min (1000) should fail if provided
|
|
67
|
-
const lowPrice = shape.carPrice.safeParse(500);
|
|
68
|
-
expect(lowPrice.success).toBe(false);
|
|
69
|
-
// Valid price should pass
|
|
70
|
-
const validPrice = shape.carPrice.safeParse(5000);
|
|
71
|
-
expect(validPrice.success).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
it('should apply regex validation correctly', () => {
|
|
74
|
-
const { baseSchema } = generateRHFSchema(mockConfig);
|
|
75
|
-
const shape = baseSchema.shape;
|
|
76
|
-
// Invalid format (numbers)
|
|
77
|
-
const invalid = shape.fullName.safeParse('John123');
|
|
78
|
-
expect(invalid.success).toBe(false);
|
|
79
|
-
// Valid format
|
|
80
|
-
const valid = shape.fullName.safeParse('John Doe');
|
|
81
|
-
expect(valid.success).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
it('should correctly build multi-step wizard categories', () => {
|
|
84
|
-
const multiPageConfig = {
|
|
85
|
-
displayName: 'Multi Page Test',
|
|
86
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
87
|
-
fields: {
|
|
88
|
-
carPrice: {
|
|
89
|
-
type: 'text',
|
|
90
|
-
inputType: 'number',
|
|
91
|
-
displayName: 'Car Price',
|
|
92
|
-
category: 'general'
|
|
93
|
-
},
|
|
94
|
-
fullName: {
|
|
95
|
-
type: 'text',
|
|
96
|
-
displayName: 'Full Name',
|
|
97
|
-
category: 'general',
|
|
98
|
-
required: true
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
pages: [
|
|
102
|
-
{ id: 'page1', title: 'First', fields: ['carPrice'] },
|
|
103
|
-
{ id: 'page2', title: 'Second', fields: ['fullName'] }
|
|
104
|
-
]
|
|
105
|
-
};
|
|
106
|
-
const { steps } = generateRHFSchema(multiPageConfig);
|
|
107
|
-
expect(steps).toHaveLength(2);
|
|
108
|
-
expect(steps[0].title).toBe('First');
|
|
109
|
-
expect(steps[0].fields[0].name).toBe('carPrice');
|
|
110
|
-
expect(steps[1].title).toBe('Second');
|
|
111
|
-
expect(steps[1].fields[0].name).toBe('fullName');
|
|
112
|
-
});
|
|
113
|
-
it('should handle field references with overrides', () => {
|
|
114
|
-
const configWithOverrides = {
|
|
115
|
-
displayName: 'Override Test',
|
|
116
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
117
|
-
fields: {
|
|
118
|
-
carPrice: {
|
|
119
|
-
type: 'text',
|
|
120
|
-
inputType: 'number',
|
|
121
|
-
displayName: 'Car Price',
|
|
122
|
-
category: 'general',
|
|
123
|
-
min: 1000
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
pages: [
|
|
127
|
-
{
|
|
128
|
-
id: 'page1',
|
|
129
|
-
title: 'Test',
|
|
130
|
-
fields: [
|
|
131
|
-
{ ref: 'carPrice', displayName: 'Vehicle Price (Override)', min: 5000 }
|
|
132
|
-
]
|
|
133
|
-
}
|
|
134
|
-
]
|
|
135
|
-
};
|
|
136
|
-
const { fields } = generateRHFSchema(configWithOverrides);
|
|
137
|
-
expect(fields[0].displayName).toBe('Vehicle Price (Override)');
|
|
138
|
-
expect(fields[0].min).toBe(5000);
|
|
139
|
-
});
|
|
140
|
-
it('should normalize required and isRequired properties', () => {
|
|
141
|
-
const { fields } = generateRHFSchema(mockConfig);
|
|
142
|
-
// fullName has required: true, should also have isRequired: true
|
|
143
|
-
const fullNameField = fields.find((f) => f.name === 'fullName');
|
|
144
|
-
expect(fullNameField?.required).toBe(true);
|
|
145
|
-
expect(fullNameField?.isRequired).toBe(true);
|
|
146
|
-
// carPrice is also required by default in the new logic
|
|
147
|
-
const carPriceField = fields.find((f) => f.name === 'carPrice');
|
|
148
|
-
expect(carPriceField?.required).toBe(true);
|
|
149
|
-
expect(carPriceField?.isRequired).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
describe('Visibility-Aware Validation (superRefine)', () => {
|
|
152
|
-
it('should NOT require a field if it is hidden via visibleIf', () => {
|
|
153
|
-
const config = {
|
|
154
|
-
displayName: 'Conditional Test',
|
|
155
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
156
|
-
fields: {
|
|
157
|
-
showField: { type: 'boolean', displayName: 'Show Secret', category: 'general' },
|
|
158
|
-
secretField: {
|
|
159
|
-
type: 'text',
|
|
160
|
-
displayName: 'Secret',
|
|
161
|
-
category: 'general',
|
|
162
|
-
required: true,
|
|
163
|
-
visibleIf: '{showField} = true'
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['showField', 'secretField'] }]
|
|
167
|
-
};
|
|
168
|
-
const { zodSchema } = generateRHFSchema(config);
|
|
169
|
-
// Scenario A: showField is false, secretField is hidden. Validation should PASS even if empty.
|
|
170
|
-
const passResult = zodSchema.safeParse({ showField: false, secretField: '' });
|
|
171
|
-
expect(passResult.success).toBe(true);
|
|
172
|
-
// Scenario B: showField is true, secretField is visible. Validation should FAIL if empty.
|
|
173
|
-
const failResult = zodSchema.safeParse({ showField: true, secretField: '' });
|
|
174
|
-
expect(failResult.success).toBe(false);
|
|
175
|
-
if (!failResult.success) {
|
|
176
|
-
expect(failResult.error.issues[0].message).toBe('This field is required');
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
it('should NOT require a field if explicitly visible is false', () => {
|
|
180
|
-
const config = {
|
|
181
|
-
displayName: 'Hidden Test',
|
|
182
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
183
|
-
fields: {
|
|
184
|
-
hiddenField: { type: 'text', displayName: 'Hidden', category: 'general', required: true, visible: false }
|
|
185
|
-
},
|
|
186
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['hiddenField'] }]
|
|
187
|
-
};
|
|
188
|
-
const { zodSchema } = generateRHFSchema(config);
|
|
189
|
-
// hiddenField is required but visible: false. Should pass.
|
|
190
|
-
const result = zodSchema.safeParse({ hiddenField: '' });
|
|
191
|
-
expect(result.success).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
describe('Validation Message Precedence', () => {
|
|
195
|
-
it('should skip regex/email validation for empty strings to favor "required" message', () => {
|
|
196
|
-
const config = {
|
|
197
|
-
displayName: 'Precedence Test',
|
|
198
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
199
|
-
fields: {
|
|
200
|
-
email: {
|
|
201
|
-
type: 'text',
|
|
202
|
-
name: 'email',
|
|
203
|
-
displayName: 'Email',
|
|
204
|
-
category: 'general',
|
|
205
|
-
required: true,
|
|
206
|
-
validators: [{ type: 'email', text: 'Bad email' }]
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['email'] }]
|
|
210
|
-
};
|
|
211
|
-
const { zodSchema } = generateRHFSchema(config);
|
|
212
|
-
// 1. Empty string -> should give "This field is required" (from baseSchema.min(1) or superRefine)
|
|
213
|
-
// Actually, since it's required, we expect an error.
|
|
214
|
-
const emptyResult = zodSchema.safeParse({ email: '' });
|
|
215
|
-
expect(emptyResult.success).toBe(false);
|
|
216
|
-
if (!emptyResult.success) {
|
|
217
|
-
// ZodError structure check
|
|
218
|
-
const issues = emptyResult.error.issues;
|
|
219
|
-
expect(issues[0].message).toBe('This field is required');
|
|
220
|
-
}
|
|
221
|
-
// 2. Invalid format but not empty -> should give "Bad email"
|
|
222
|
-
const invalidResult = zodSchema.safeParse({ email: 'not-an-email' });
|
|
223
|
-
expect(invalidResult.success).toBe(false);
|
|
224
|
-
if (!invalidResult.success) {
|
|
225
|
-
const issues = invalidResult.error.issues;
|
|
226
|
-
expect(issues[0].message).toBe('Bad email');
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
describe('Field type handling', () => {
|
|
231
|
-
it('should handle file fields with array schema and empty array default', () => {
|
|
232
|
-
const config = {
|
|
233
|
-
displayName: 'File Test',
|
|
234
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
235
|
-
fields: {
|
|
236
|
-
document: { type: 'file', displayName: 'Upload Document', category: 'general' }
|
|
237
|
-
},
|
|
238
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['document'] }]
|
|
239
|
-
};
|
|
240
|
-
const { defaultValues, baseSchema } = generateRHFSchema(config);
|
|
241
|
-
expect(defaultValues.document).toEqual([]);
|
|
242
|
-
// File fields are required by default, so empty array fails base schema
|
|
243
|
-
const emptyResult = baseSchema.shape.document.safeParse([]);
|
|
244
|
-
expect(emptyResult.success).toBe(false);
|
|
245
|
-
// With content, it passes
|
|
246
|
-
const withFile = baseSchema.shape.document.safeParse([{ name: 'file.pdf' }]);
|
|
247
|
-
expect(withFile.success).toBe(true);
|
|
248
|
-
});
|
|
249
|
-
it('should handle boolean fields with false default', () => {
|
|
250
|
-
const config = {
|
|
251
|
-
displayName: 'Boolean Test',
|
|
252
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
253
|
-
fields: {
|
|
254
|
-
agree: { type: 'boolean', displayName: 'Agree', category: 'general', required: false }
|
|
255
|
-
},
|
|
256
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['agree'] }]
|
|
257
|
-
};
|
|
258
|
-
const { defaultValues } = generateRHFSchema(config);
|
|
259
|
-
expect(defaultValues.agree).toBe(false);
|
|
260
|
-
});
|
|
261
|
-
it('should handle boolean fields with custom default', () => {
|
|
262
|
-
const config = {
|
|
263
|
-
displayName: 'Boolean Default Test',
|
|
264
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
265
|
-
fields: {
|
|
266
|
-
agree: { type: 'boolean', displayName: 'Agree', category: 'general', defaultValue: true }
|
|
267
|
-
},
|
|
268
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['agree'] }]
|
|
269
|
-
};
|
|
270
|
-
const { defaultValues } = generateRHFSchema(config);
|
|
271
|
-
expect(defaultValues.agree).toBe(true);
|
|
272
|
-
});
|
|
273
|
-
it('should handle checkbox fields with array schema and empty array default', () => {
|
|
274
|
-
const config = {
|
|
275
|
-
displayName: 'Checkbox Test',
|
|
276
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
277
|
-
fields: {
|
|
278
|
-
interests: {
|
|
279
|
-
type: 'checkbox',
|
|
280
|
-
displayName: 'Interests',
|
|
281
|
-
category: 'general',
|
|
282
|
-
choices: [
|
|
283
|
-
{ value: 'a', text: 'A' },
|
|
284
|
-
{ value: 'b', text: 'B' }
|
|
285
|
-
]
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['interests'] }]
|
|
289
|
-
};
|
|
290
|
-
const { defaultValues, baseSchema } = generateRHFSchema(config);
|
|
291
|
-
expect(defaultValues.interests).toEqual([]);
|
|
292
|
-
const result = baseSchema.shape.interests.safeParse(['a']);
|
|
293
|
-
expect(result.success).toBe(true);
|
|
294
|
-
});
|
|
295
|
-
it('should handle html display fields as optional', () => {
|
|
296
|
-
const config = {
|
|
297
|
-
displayName: 'HTML Test',
|
|
298
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
299
|
-
fields: {
|
|
300
|
-
notice: {
|
|
301
|
-
type: 'html',
|
|
302
|
-
displayName: 'Notice',
|
|
303
|
-
category: 'general',
|
|
304
|
-
html: '<p>Important notice</p>',
|
|
305
|
-
required: true // even if required is true, html should be optional
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['notice'] }]
|
|
309
|
-
};
|
|
310
|
-
const { defaultValues, zodSchema } = generateRHFSchema(config);
|
|
311
|
-
expect(defaultValues.notice).toBe('');
|
|
312
|
-
// html fields should not cause validation failure
|
|
313
|
-
const result = zodSchema.safeParse({ notice: '' });
|
|
314
|
-
expect(result.success).toBe(true);
|
|
315
|
-
});
|
|
316
|
-
it('should set numeric field default to empty string when no defaultValue specified', () => {
|
|
317
|
-
const config = {
|
|
318
|
-
displayName: 'Numeric Default Test',
|
|
319
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
320
|
-
fields: {
|
|
321
|
-
amount: { type: 'text', inputType: 'number', displayName: 'Amount', category: 'general' }
|
|
322
|
-
},
|
|
323
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['amount'] }]
|
|
324
|
-
};
|
|
325
|
-
const { defaultValues } = generateRHFSchema(config);
|
|
326
|
-
// HTML number inputs return "" when empty; z.coerce.number() handles this correctly
|
|
327
|
-
expect(defaultValues.amount).toBe("");
|
|
328
|
-
});
|
|
329
|
-
it('should set numeric field default to the provided value', () => {
|
|
330
|
-
const config = {
|
|
331
|
-
displayName: 'Numeric Default Test',
|
|
332
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
333
|
-
fields: {
|
|
334
|
-
amount: {
|
|
335
|
-
type: 'text', inputType: 'number', displayName: 'Amount',
|
|
336
|
-
category: 'general', defaultValue: 5000
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['amount'] }]
|
|
340
|
-
};
|
|
341
|
-
const { defaultValues } = generateRHFSchema(config);
|
|
342
|
-
expect(defaultValues.amount).toBe(5000);
|
|
343
|
-
});
|
|
344
|
-
it('should handle explicitly optional fields', () => {
|
|
345
|
-
const config = {
|
|
346
|
-
displayName: 'Optional Test',
|
|
347
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
348
|
-
fields: {
|
|
349
|
-
nickname: { type: 'text', displayName: 'Nickname', category: 'general', required: false }
|
|
350
|
-
},
|
|
351
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['nickname'] }]
|
|
352
|
-
};
|
|
353
|
-
const { baseSchema } = generateRHFSchema(config);
|
|
354
|
-
// Optional text field should accept empty string
|
|
355
|
-
const result = baseSchema.shape.nickname.safeParse('');
|
|
356
|
-
expect(result.success).toBe(true);
|
|
357
|
-
});
|
|
358
|
-
it('should use first element when defaultValue is an array for text fields', () => {
|
|
359
|
-
const config = {
|
|
360
|
-
displayName: 'Array Default Test',
|
|
361
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
362
|
-
fields: {
|
|
363
|
-
country: {
|
|
364
|
-
type: 'dropdown',
|
|
365
|
-
displayName: 'Country',
|
|
366
|
-
category: 'general',
|
|
367
|
-
defaultValue: ['US', 'UK']
|
|
368
|
-
}
|
|
369
|
-
},
|
|
370
|
-
pages: [{ id: 'p1', title: 'P1', fields: ['country'] }]
|
|
371
|
-
};
|
|
372
|
-
const { defaultValues } = generateRHFSchema(config);
|
|
373
|
-
expect(defaultValues.country).toBe('US');
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
describe('Validators on conditional fields', () => {
|
|
377
|
-
it('should apply regex validators to fields with visibleIf', () => {
|
|
378
|
-
const config = {
|
|
379
|
-
displayName: 'IC Validation Test',
|
|
380
|
-
categories: [{ id: 'customer', name: 'Customer' }],
|
|
381
|
-
fields: {
|
|
382
|
-
idType: {
|
|
383
|
-
type: 'dropdown',
|
|
384
|
-
displayName: 'ID Type',
|
|
385
|
-
category: 'customer',
|
|
386
|
-
choices: [
|
|
387
|
-
{ value: 'MK', text: 'MyKad' },
|
|
388
|
-
{ value: 'MI', text: 'Military' }
|
|
389
|
-
]
|
|
390
|
-
},
|
|
391
|
-
idNumber_mk: {
|
|
392
|
-
type: 'text',
|
|
393
|
-
displayName: 'IC Number',
|
|
394
|
-
category: 'customer',
|
|
395
|
-
required: true,
|
|
396
|
-
visibleIf: "{idType} == 'MK'",
|
|
397
|
-
maxLength: 12,
|
|
398
|
-
validators: [
|
|
399
|
-
{ type: 'regex', regex: '^\\d{12}$', text: 'Please enter exactly 12 digit numbers' }
|
|
400
|
-
]
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
pages: [{ id: 'p1', title: 'Info', fields: ['idType', 'idNumber_mk'] }]
|
|
404
|
-
};
|
|
405
|
-
const { zodSchema } = generateRHFSchema(config);
|
|
406
|
-
// When visible (idType=MK) and valid IC number -> pass
|
|
407
|
-
const validResult = zodSchema.safeParse({ idType: 'MK', idNumber_mk: '700514065413' });
|
|
408
|
-
expect(validResult.success).toBe(true);
|
|
409
|
-
// When visible (idType=MK) and invalid IC number -> fail with regex message
|
|
410
|
-
const invalidResult = zodSchema.safeParse({ idType: 'MK', idNumber_mk: 'abc123' });
|
|
411
|
-
expect(invalidResult.success).toBe(false);
|
|
412
|
-
if (!invalidResult.success) {
|
|
413
|
-
const icIssue = invalidResult.error.issues.find(i => i.path.includes('idNumber_mk'));
|
|
414
|
-
expect(icIssue?.message).toBe('Please enter exactly 12 digit numbers');
|
|
415
|
-
}
|
|
416
|
-
// When hidden (idType=MI) and empty -> pass (not validated)
|
|
417
|
-
const hiddenResult = zodSchema.safeParse({ idType: 'MI', idNumber_mk: '' });
|
|
418
|
-
expect(hiddenResult.success).toBe(true);
|
|
419
|
-
});
|
|
420
|
-
it('should apply email validators to fields with visibleIf', () => {
|
|
421
|
-
const config = {
|
|
422
|
-
displayName: 'Conditional Email Test',
|
|
423
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
424
|
-
fields: {
|
|
425
|
-
showEmail: { type: 'boolean', displayName: 'Show Email', category: 'general' },
|
|
426
|
-
contactEmail: {
|
|
427
|
-
type: 'text',
|
|
428
|
-
inputType: 'email',
|
|
429
|
-
displayName: 'Contact Email',
|
|
430
|
-
category: 'general',
|
|
431
|
-
required: true,
|
|
432
|
-
visibleIf: '{showEmail} = true',
|
|
433
|
-
validators: [{ type: 'email', text: 'Invalid email format' }]
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
pages: [{ id: 'p1', title: 'Test', fields: ['showEmail', 'contactEmail'] }]
|
|
437
|
-
};
|
|
438
|
-
const { zodSchema } = generateRHFSchema(config);
|
|
439
|
-
// Visible + invalid email -> fail
|
|
440
|
-
const invalidResult = zodSchema.safeParse({ showEmail: true, contactEmail: 'not-email' });
|
|
441
|
-
expect(invalidResult.success).toBe(false);
|
|
442
|
-
if (!invalidResult.success) {
|
|
443
|
-
const emailIssue = invalidResult.error.issues.find(i => i.path.includes('contactEmail'));
|
|
444
|
-
expect(emailIssue?.message).toBe('Invalid email format');
|
|
445
|
-
}
|
|
446
|
-
// Visible + valid email -> pass
|
|
447
|
-
const validResult = zodSchema.safeParse({ showEmail: true, contactEmail: 'test@example.com' });
|
|
448
|
-
expect(validResult.success).toBe(true);
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
describe('Error handling', () => {
|
|
452
|
-
it('should throw when config has no pages', () => {
|
|
453
|
-
const config = {
|
|
454
|
-
displayName: 'No Pages',
|
|
455
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
456
|
-
fields: { name: { type: 'text' } },
|
|
457
|
-
pages: []
|
|
458
|
-
};
|
|
459
|
-
expect(() => generateRHFSchema(config)).toThrow("v2.0.0 templates must include a 'pages' configuration.");
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
describe('getStepSchema', () => {
|
|
463
|
-
it('should extract schema for a specific step from ZodEffects', () => {
|
|
464
|
-
const multiPageConfig = {
|
|
465
|
-
displayName: 'Step Schema Test',
|
|
466
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
467
|
-
fields: {
|
|
468
|
-
name: { type: 'text', displayName: 'Name', category: 'general' },
|
|
469
|
-
age: { type: 'text', inputType: 'number', displayName: 'Age', category: 'general', min: 0 }
|
|
470
|
-
},
|
|
471
|
-
pages: [
|
|
472
|
-
{ id: 'p1', title: 'Personal', fields: ['name'] },
|
|
473
|
-
{ id: 'p2', title: 'Details', fields: ['age'] }
|
|
474
|
-
]
|
|
475
|
-
};
|
|
476
|
-
const result = generateRHFSchema(multiPageConfig);
|
|
477
|
-
const step1Schema = getStepSchema(result.steps[0], result.zodSchema);
|
|
478
|
-
// Step 1 only has "name"
|
|
479
|
-
const step1Pass = step1Schema.safeParse({ name: 'John' });
|
|
480
|
-
expect(step1Pass.success).toBe(true);
|
|
481
|
-
// Step 1 should not care about "age"
|
|
482
|
-
const step1NoAge = step1Schema.safeParse({ name: 'John' });
|
|
483
|
-
expect(step1NoAge.success).toBe(true);
|
|
484
|
-
});
|
|
485
|
-
it('should accept baseSchema directly', () => {
|
|
486
|
-
const config = {
|
|
487
|
-
displayName: 'Step Schema Direct',
|
|
488
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
489
|
-
fields: {
|
|
490
|
-
name: { type: 'text', displayName: 'Name', category: 'general' }
|
|
491
|
-
},
|
|
492
|
-
pages: [{ id: 'p1', title: 'Test', fields: ['name'] }]
|
|
493
|
-
};
|
|
494
|
-
const result = generateRHFSchema(config);
|
|
495
|
-
const stepSchema = getStepSchema(result.steps[0], result.baseSchema);
|
|
496
|
-
const parsed = stepSchema.safeParse({ name: 'Alice' });
|
|
497
|
-
expect(parsed.success).toBe(true);
|
|
498
|
-
});
|
|
499
|
-
it('should accept { baseSchema } wrapper object', () => {
|
|
500
|
-
const config = {
|
|
501
|
-
displayName: 'Step Schema Wrapper',
|
|
502
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
503
|
-
fields: {
|
|
504
|
-
name: { type: 'text', displayName: 'Name', category: 'general' }
|
|
505
|
-
},
|
|
506
|
-
pages: [{ id: 'p1', title: 'Test', fields: ['name'] }]
|
|
507
|
-
};
|
|
508
|
-
const result = generateRHFSchema(config);
|
|
509
|
-
const stepSchema = getStepSchema(result.steps[0], { baseSchema: result.baseSchema });
|
|
510
|
-
const parsed = stepSchema.safeParse({ name: 'Bob' });
|
|
511
|
-
expect(parsed.success).toBe(true);
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
describe('getStepDefaultValues', () => {
|
|
515
|
-
it('should extract defaults for fields in a specific step', () => {
|
|
516
|
-
const config = {
|
|
517
|
-
displayName: 'Step Defaults Test',
|
|
518
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
519
|
-
fields: {
|
|
520
|
-
name: { type: 'text', displayName: 'Name', category: 'general', defaultValue: 'John' },
|
|
521
|
-
age: { type: 'text', inputType: 'number', displayName: 'Age', category: 'general', defaultValue: 25 }
|
|
522
|
-
},
|
|
523
|
-
pages: [
|
|
524
|
-
{ id: 'p1', title: 'Personal', fields: ['name'] },
|
|
525
|
-
{ id: 'p2', title: 'Details', fields: ['age'] }
|
|
526
|
-
]
|
|
527
|
-
};
|
|
528
|
-
const result = generateRHFSchema(config);
|
|
529
|
-
const step1Defaults = getStepDefaultValues(result.steps[0], result.defaultValues);
|
|
530
|
-
expect(step1Defaults).toEqual({ name: 'John' });
|
|
531
|
-
expect(step1Defaults).not.toHaveProperty('age');
|
|
532
|
-
const step2Defaults = getStepDefaultValues(result.steps[1], result.defaultValues);
|
|
533
|
-
expect(step2Defaults).toEqual({ age: 25 });
|
|
534
|
-
expect(step2Defaults).not.toHaveProperty('name');
|
|
535
|
-
});
|
|
536
|
-
it('should return empty object for step with no matching defaults', () => {
|
|
537
|
-
const config = {
|
|
538
|
-
displayName: 'Empty Defaults Test',
|
|
539
|
-
categories: [{ id: 'general', name: 'General' }],
|
|
540
|
-
fields: {
|
|
541
|
-
name: { type: 'text', displayName: 'Name', category: 'general' }
|
|
542
|
-
},
|
|
543
|
-
pages: [{ id: 'p1', title: 'Test', fields: ['name'] }]
|
|
544
|
-
};
|
|
545
|
-
const result = generateRHFSchema(config);
|
|
546
|
-
// Pass empty allDefaults
|
|
547
|
-
const stepDefaults = getStepDefaultValues(result.steps[0], {});
|
|
548
|
-
expect(stepDefaults).toEqual({});
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
describe('Dynamic titles', () => {
|
|
552
|
-
it('should apply dynamic bank statement titles', () => {
|
|
553
|
-
const config = {
|
|
554
|
-
displayName: 'Bank Statement Test',
|
|
555
|
-
categories: [{ id: 'docs', name: 'Documents' }],
|
|
556
|
-
fields: {
|
|
557
|
-
bank_statement_t1: { type: 'file', displayName: 'Statement', category: 'docs' },
|
|
558
|
-
bank_statement_t2: { type: 'file', displayName: 'Statement', category: 'docs' }
|
|
559
|
-
},
|
|
560
|
-
pages: [{ id: 'p1', title: 'Docs', fields: ['bank_statement_t1', 'bank_statement_t2'] }]
|
|
561
|
-
};
|
|
562
|
-
const { fields, steps } = generateRHFSchema(config);
|
|
563
|
-
// Both mergedFields and step fields should have dynamic titles
|
|
564
|
-
expect(fields[0].displayName).toMatch(/^Bank Statement \(\w+\)$/);
|
|
565
|
-
expect(fields[1].displayName).toMatch(/^Bank Statement \(\w+\)$/);
|
|
566
|
-
expect(fields[0].displayName).not.toBe(fields[1].displayName);
|
|
567
|
-
expect(steps[0].fields[0].displayName).toMatch(/^Bank Statement \(\w+\)$/);
|
|
568
|
-
});
|
|
569
|
-
it('should apply dynamic financials title', () => {
|
|
570
|
-
const config = {
|
|
571
|
-
displayName: 'Financials Test',
|
|
572
|
-
categories: [{ id: 'docs', name: 'Documents' }],
|
|
573
|
-
fields: {
|
|
574
|
-
financials: { type: 'file', displayName: 'Financial Statement', category: 'docs' }
|
|
575
|
-
},
|
|
576
|
-
pages: [{ id: 'p1', title: 'Docs', fields: ['financials'] }]
|
|
577
|
-
};
|
|
578
|
-
const { fields } = generateRHFSchema(config);
|
|
579
|
-
const expectedYear = (new Date().getFullYear() - 1).toString();
|
|
580
|
-
expect(fields[0].displayName).toBe(`Audited Financial Statement (${expectedYear})`);
|
|
581
|
-
});
|
|
582
|
-
});
|
|
583
|
-
});
|