@fogpipe/forma-react 0.12.0-alpha.2 → 0.13.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.
@@ -26,7 +26,10 @@ describe("canProceed", () => {
26
26
  });
27
27
 
28
28
  const { result } = renderHook(() =>
29
- useForma({ spec, initialData: { name: "John", email: "john@example.com" } })
29
+ useForma({
30
+ spec,
31
+ initialData: { name: "John", email: "john@example.com" },
32
+ }),
30
33
  );
31
34
 
32
35
  expect(result.current.wizard?.canProceed).toBe(true);
@@ -38,13 +41,11 @@ describe("canProceed", () => {
38
41
  name: { type: "text", label: "Name", required: true },
39
42
  email: { type: "email", label: "Email", required: true },
40
43
  },
41
- pages: [
42
- { id: "page1", title: "Page 1", fields: ["name", "email"] },
43
- ],
44
+ pages: [{ id: "page1", title: "Page 1", fields: ["name", "email"] }],
44
45
  });
45
46
 
46
- const { result } = renderHook(() =>
47
- useForma({ spec, initialData: { name: "John" } }) // email is missing
47
+ const { result } = renderHook(
48
+ () => useForma({ spec, initialData: { name: "John" } }), // email is missing
48
49
  );
49
50
 
50
51
  expect(result.current.wizard?.canProceed).toBe(false);
@@ -63,8 +64,8 @@ describe("canProceed", () => {
63
64
  ],
64
65
  });
65
66
 
66
- const { result } = renderHook(() =>
67
- useForma({ spec, initialData: { name: "John" } }) // page 2 fields empty
67
+ const { result } = renderHook(
68
+ () => useForma({ spec, initialData: { name: "John" } }), // page 2 fields empty
68
69
  );
69
70
 
70
71
  // On page 1, only name is checked (which is filled)
@@ -84,7 +85,7 @@ describe("canProceed", () => {
84
85
  });
85
86
 
86
87
  const { result } = renderHook(() =>
87
- useForma({ spec, initialData: { name: "John" } })
88
+ useForma({ spec, initialData: { name: "John" } }),
88
89
  );
89
90
 
90
91
  // Page 1 is valid
@@ -112,13 +113,11 @@ describe("canProceed", () => {
112
113
  },
113
114
  },
114
115
  },
115
- pages: [
116
- { id: "page1", title: "Page 1", fields: ["items"] },
117
- ],
116
+ pages: [{ id: "page1", title: "Page 1", fields: ["items"] }],
118
117
  });
119
118
 
120
119
  const { result } = renderHook(() =>
121
- useForma({ spec, initialData: { items: [] } })
120
+ useForma({ spec, initialData: { items: [] } }),
122
121
  );
123
122
 
124
123
  // Array is empty but minItems requires at least 1
@@ -139,15 +138,24 @@ describe("canProceed", () => {
139
138
  fields: {
140
139
  showEmail: { type: "boolean", label: "Show Email" },
141
140
  name: { type: "text", label: "Name", required: true },
142
- email: { type: "email", label: "Email", required: true, visibleWhen: "showEmail = true" },
141
+ email: {
142
+ type: "email",
143
+ label: "Email",
144
+ required: true,
145
+ visibleWhen: "showEmail = true",
146
+ },
143
147
  },
144
148
  pages: [
145
- { id: "page1", title: "Page 1", fields: ["showEmail", "name", "email"] },
149
+ {
150
+ id: "page1",
151
+ title: "Page 1",
152
+ fields: ["showEmail", "name", "email"],
153
+ },
146
154
  ],
147
155
  });
148
156
 
149
157
  const { result } = renderHook(() =>
150
- useForma({ spec, initialData: { showEmail: false, name: "John" } })
158
+ useForma({ spec, initialData: { showEmail: false, name: "John" } }),
151
159
  );
152
160
 
153
161
  // Email is required but hidden, so it shouldn't block progression
@@ -159,15 +167,24 @@ describe("canProceed", () => {
159
167
  fields: {
160
168
  showEmail: { type: "boolean", label: "Show Email" },
161
169
  name: { type: "text", label: "Name", required: true },
162
- email: { type: "email", label: "Email", required: true, visibleWhen: "showEmail = true" },
170
+ email: {
171
+ type: "email",
172
+ label: "Email",
173
+ required: true,
174
+ visibleWhen: "showEmail = true",
175
+ },
163
176
  },
164
177
  pages: [
165
- { id: "page1", title: "Page 1", fields: ["showEmail", "name", "email"] },
178
+ {
179
+ id: "page1",
180
+ title: "Page 1",
181
+ fields: ["showEmail", "name", "email"],
182
+ },
166
183
  ],
167
184
  });
168
185
 
169
186
  const { result } = renderHook(() =>
170
- useForma({ spec, initialData: { showEmail: false, name: "John" } })
187
+ useForma({ spec, initialData: { showEmail: false, name: "John" } }),
171
188
  );
172
189
 
173
190
  // Email is hidden
@@ -197,13 +214,11 @@ describe("canProceed", () => {
197
214
  name: { type: "text", label: "Name", required: true },
198
215
  age: { type: "number", label: "Age" }, // Not required
199
216
  },
200
- pages: [
201
- { id: "page1", title: "Page 1", fields: ["name", "age"] },
202
- ],
217
+ pages: [{ id: "page1", title: "Page 1", fields: ["name", "age"] }],
203
218
  });
204
219
 
205
- const { result } = renderHook(() =>
206
- useForma({ spec, initialData: { age: 25 } }) // name missing
220
+ const { result } = renderHook(
221
+ () => useForma({ spec, initialData: { age: 25 } }), // name missing
207
222
  );
208
223
 
209
224
  expect(result.current.wizard?.canProceed).toBe(false);
@@ -219,7 +234,11 @@ describe("canProceed", () => {
219
234
  const spec = createTestSpec({
220
235
  fields: {
221
236
  hasSpouse: { type: "boolean", label: "Has Spouse" },
222
- spouseName: { type: "text", label: "Spouse Name", requiredWhen: "hasSpouse = true" },
237
+ spouseName: {
238
+ type: "text",
239
+ label: "Spouse Name",
240
+ requiredWhen: "hasSpouse = true",
241
+ },
223
242
  },
224
243
  pages: [
225
244
  { id: "page1", title: "Page 1", fields: ["hasSpouse", "spouseName"] },
@@ -227,7 +246,7 @@ describe("canProceed", () => {
227
246
  });
228
247
 
229
248
  const { result } = renderHook(() =>
230
- useForma({ spec, initialData: { hasSpouse: false } })
249
+ useForma({ spec, initialData: { hasSpouse: false } }),
231
250
  );
232
251
 
233
252
  // Spouse name not required when hasSpouse is false
@@ -258,29 +277,31 @@ describe("canProceed", () => {
258
277
  // use a validation rule: { rule: "value = true", message: "Must accept terms" }
259
278
  const spec = createTestSpec({
260
279
  fields: {
261
- hasPets: { type: "boolean", label: "Do you have pets?", required: true },
280
+ hasPets: {
281
+ type: "boolean",
282
+ label: "Do you have pets?",
283
+ required: true,
284
+ },
262
285
  },
263
- pages: [
264
- { id: "page1", title: "Page 1", fields: ["hasPets"] },
265
- ],
286
+ pages: [{ id: "page1", title: "Page 1", fields: ["hasPets"] }],
266
287
  });
267
288
 
268
289
  // With auto-initialization, boolean defaults to false (a valid value)
269
290
  const { result: resultDefault } = renderHook(() =>
270
- useForma({ spec, initialData: {} })
291
+ useForma({ spec, initialData: {} }),
271
292
  );
272
293
  expect(resultDefault.current.data.hasPets).toBe(false);
273
294
  expect(resultDefault.current.wizard?.canProceed).toBe(true); // false is valid
274
295
 
275
296
  // explicit false should be valid (user answered "no")
276
297
  const { result: resultFalse } = renderHook(() =>
277
- useForma({ spec, initialData: { hasPets: false } })
298
+ useForma({ spec, initialData: { hasPets: false } }),
278
299
  );
279
300
  expect(resultFalse.current.wizard?.canProceed).toBe(true);
280
301
 
281
302
  // true should be valid (user answered "yes")
282
303
  const { result: resultTrue } = renderHook(() =>
283
- useForma({ spec, initialData: { hasPets: true } })
304
+ useForma({ spec, initialData: { hasPets: true } }),
284
305
  );
285
306
  expect(resultTrue.current.wizard?.canProceed).toBe(true);
286
307
  });
@@ -298,14 +319,10 @@ describe("canProceed", () => {
298
319
  ],
299
320
  },
300
321
  },
301
- pages: [
302
- { id: "page1", title: "Page 1", fields: ["country"] },
303
- ],
322
+ pages: [{ id: "page1", title: "Page 1", fields: ["country"] }],
304
323
  });
305
324
 
306
- const { result } = renderHook(() =>
307
- useForma({ spec, initialData: {} })
308
- );
325
+ const { result } = renderHook(() => useForma({ spec, initialData: {} }));
309
326
 
310
327
  expect(result.current.wizard?.canProceed).toBe(false);
311
328
 
@@ -323,26 +340,24 @@ describe("canProceed", () => {
323
340
  fields: {
324
341
  name: { type: "text", label: "Name", required: true },
325
342
  },
326
- pages: [
327
- { id: "page1", title: "Page 1", fields: ["name"] },
328
- ],
343
+ pages: [{ id: "page1", title: "Page 1", fields: ["name"] }],
329
344
  });
330
345
 
331
346
  // Empty string should be invalid
332
347
  const { result: resultEmpty } = renderHook(() =>
333
- useForma({ spec, initialData: { name: "" } })
348
+ useForma({ spec, initialData: { name: "" } }),
334
349
  );
335
350
  expect(resultEmpty.current.wizard?.canProceed).toBe(false);
336
351
 
337
352
  // Whitespace only should be invalid
338
353
  const { result: resultWhitespace } = renderHook(() =>
339
- useForma({ spec, initialData: { name: " " } })
354
+ useForma({ spec, initialData: { name: " " } }),
340
355
  );
341
356
  expect(resultWhitespace.current.wizard?.canProceed).toBe(false);
342
357
 
343
358
  // Actual value should be valid
344
359
  const { result: resultValid } = renderHook(() =>
345
- useForma({ spec, initialData: { name: "John" } })
360
+ useForma({ spec, initialData: { name: "John" } }),
346
361
  );
347
362
  expect(resultValid.current.wizard?.canProceed).toBe(true);
348
363
  });
@@ -352,26 +367,24 @@ describe("canProceed", () => {
352
367
  fields: {
353
368
  age: { type: "number", label: "Age", required: true },
354
369
  },
355
- pages: [
356
- { id: "page1", title: "Page 1", fields: ["age"] },
357
- ],
370
+ pages: [{ id: "page1", title: "Page 1", fields: ["age"] }],
358
371
  });
359
372
 
360
373
  // null should be invalid
361
374
  const { result: resultNull } = renderHook(() =>
362
- useForma({ spec, initialData: { age: null } })
375
+ useForma({ spec, initialData: { age: null } }),
363
376
  );
364
377
  expect(resultNull.current.wizard?.canProceed).toBe(false);
365
378
 
366
379
  // undefined should be invalid
367
380
  const { result: resultUndefined } = renderHook(() =>
368
- useForma({ spec, initialData: {} })
381
+ useForma({ spec, initialData: {} }),
369
382
  );
370
383
  expect(resultUndefined.current.wizard?.canProceed).toBe(false);
371
384
 
372
385
  // 0 should be valid (it's a real number)
373
386
  const { result: resultZero } = renderHook(() =>
374
- useForma({ spec, initialData: { age: 0 } })
387
+ useForma({ spec, initialData: { age: 0 } }),
375
388
  );
376
389
  expect(resultZero.current.wizard?.canProceed).toBe(true);
377
390
  });
@@ -388,26 +401,27 @@ describe("canProceed", () => {
388
401
  },
389
402
  },
390
403
  },
391
- pages: [
392
- { id: "page1", title: "Page 1", fields: ["items"] },
393
- ],
404
+ pages: [{ id: "page1", title: "Page 1", fields: ["items"] }],
394
405
  });
395
406
 
396
407
  // Empty array with minItems > 0 should be invalid
397
408
  const { result: resultEmpty } = renderHook(() =>
398
- useForma({ spec, initialData: { items: [] } })
409
+ useForma({ spec, initialData: { items: [] } }),
399
410
  );
400
411
  expect(resultEmpty.current.wizard?.canProceed).toBe(false);
401
412
 
402
413
  // 1 item but minItems is 2
403
414
  const { result: resultOne } = renderHook(() =>
404
- useForma({ spec, initialData: { items: [{ name: "Item 1" }] } })
415
+ useForma({ spec, initialData: { items: [{ name: "Item 1" }] } }),
405
416
  );
406
417
  expect(resultOne.current.wizard?.canProceed).toBe(false);
407
418
 
408
419
  // 2 items should be valid
409
420
  const { result: resultTwo } = renderHook(() =>
410
- useForma({ spec, initialData: { items: [{ name: "Item 1" }, { name: "Item 2" }] } })
421
+ useForma({
422
+ spec,
423
+ initialData: { items: [{ name: "Item 1" }, { name: "Item 2" }] },
424
+ }),
411
425
  );
412
426
  expect(resultTwo.current.wizard?.canProceed).toBe(true);
413
427
  });
@@ -425,9 +439,7 @@ describe("canProceed", () => {
425
439
  ],
426
440
  });
427
441
 
428
- const { result } = renderHook(() =>
429
- useForma({ spec, initialData: {} })
430
- );
442
+ const { result } = renderHook(() => useForma({ spec, initialData: {} }));
431
443
 
432
444
  // Empty page should allow progression
433
445
  expect(result.current.wizard?.canProceed).toBe(true);
@@ -437,16 +449,30 @@ describe("canProceed", () => {
437
449
  const spec = createTestSpec({
438
450
  fields: {
439
451
  showFields: { type: "boolean", label: "Show Fields" },
440
- name: { type: "text", label: "Name", required: true, visibleWhen: "showFields = true" },
441
- email: { type: "email", label: "Email", required: true, visibleWhen: "showFields = true" },
452
+ name: {
453
+ type: "text",
454
+ label: "Name",
455
+ required: true,
456
+ visibleWhen: "showFields = true",
457
+ },
458
+ email: {
459
+ type: "email",
460
+ label: "Email",
461
+ required: true,
462
+ visibleWhen: "showFields = true",
463
+ },
442
464
  },
443
465
  pages: [
444
- { id: "page1", title: "Page 1", fields: ["showFields", "name", "email"] },
466
+ {
467
+ id: "page1",
468
+ title: "Page 1",
469
+ fields: ["showFields", "name", "email"],
470
+ },
445
471
  ],
446
472
  });
447
473
 
448
474
  const { result } = renderHook(() =>
449
- useForma({ spec, initialData: { showFields: false } })
475
+ useForma({ spec, initialData: { showFields: false } }),
450
476
  );
451
477
 
452
478
  // All required fields are hidden, only showFields is visible (not required)
@@ -467,8 +493,8 @@ describe("canProceed", () => {
467
493
  ],
468
494
  });
469
495
 
470
- const { result } = renderHook(() =>
471
- useForma({ spec, initialData: { name: "John" } }) // Only page 1 is valid
496
+ const { result } = renderHook(
497
+ () => useForma({ spec, initialData: { name: "John" } }), // Only page 1 is valid
472
498
  );
473
499
 
474
500
  // Page 1 should be valid
@@ -500,7 +526,7 @@ describe("canProceed", () => {
500
526
  });
501
527
 
502
528
  const { result } = renderHook(() =>
503
- useForma({ spec, initialData: { price: 10, quantity: 2 } })
529
+ useForma({ spec, initialData: { price: 10, quantity: 2 } }),
504
530
  );
505
531
 
506
532
  // Computed fields don't block - only real fields do
@@ -515,14 +541,10 @@ describe("canProceed", () => {
515
541
  fields: {
516
542
  name: { type: "text", label: "Name", required: true },
517
543
  },
518
- pages: [
519
- { id: "page1", title: "Page 1", fields: ["name"] },
520
- ],
544
+ pages: [{ id: "page1", title: "Page 1", fields: ["name"] }],
521
545
  });
522
546
 
523
- const { result } = renderHook(() =>
524
- useForma({ spec, initialData: {} })
525
- );
547
+ const { result } = renderHook(() => useForma({ spec, initialData: {} }));
526
548
 
527
549
  // Both should return the same result
528
550
  expect(result.current.wizard?.canProceed).toBe(false);
@@ -543,13 +565,11 @@ describe("canProceed", () => {
543
565
  name: { type: "text", label: "Name", required: true },
544
566
  email: { type: "email", label: "Email", required: true },
545
567
  },
546
- pages: [
547
- { id: "page1", title: "Page 1", fields: ["name", "email"] },
548
- ],
568
+ pages: [{ id: "page1", title: "Page 1", fields: ["name", "email"] }],
549
569
  });
550
570
 
551
571
  const { result } = renderHook(() =>
552
- useForma({ spec, initialData: {}, validateOn: "blur" })
572
+ useForma({ spec, initialData: {}, validateOn: "blur" }),
553
573
  );
554
574
 
555
575
  // Fields not touched yet - errors exist but not visible to user
@@ -574,14 +594,10 @@ describe("canProceed", () => {
574
594
  fields: {
575
595
  name: { type: "text", label: "Name", required: true },
576
596
  },
577
- pages: [
578
- { id: "page1", title: "Page 1", fields: ["name"] },
579
- ],
597
+ pages: [{ id: "page1", title: "Page 1", fields: ["name"] }],
580
598
  });
581
599
 
582
- const { result } = renderHook(() =>
583
- useForma({ spec, initialData: {} })
584
- );
600
+ const { result } = renderHook(() => useForma({ spec, initialData: {} }));
585
601
 
586
602
  expect(result.current.wizard?.canProceed).toBe(false);
587
603
 
@@ -606,13 +622,15 @@ describe("canProceed", () => {
606
622
  email: { type: "email", label: "Email", required: true },
607
623
  },
608
624
  pages: [
609
- { id: "page1", title: "Page 1", fields: ["firstName", "lastName", "email"] },
625
+ {
626
+ id: "page1",
627
+ title: "Page 1",
628
+ fields: ["firstName", "lastName", "email"],
629
+ },
610
630
  ],
611
631
  });
612
632
 
613
- const { result } = renderHook(() =>
614
- useForma({ spec, initialData: {} })
615
- );
633
+ const { result } = renderHook(() => useForma({ spec, initialData: {} }));
616
634
 
617
635
  expect(result.current.wizard?.canProceed).toBe(false);
618
636
 
@@ -644,18 +662,18 @@ describe("canProceed", () => {
644
662
  fields: {
645
663
  name: { type: "text", label: "Name", required: true },
646
664
  },
647
- pages: [
648
- { id: "page1", title: "Page 1", fields: ["name"] },
649
- ],
665
+ pages: [{ id: "page1", title: "Page 1", fields: ["name"] }],
650
666
  });
651
667
 
652
668
  const { result } = renderHook(() =>
653
- useForma({ spec, initialData: { name: "John" } })
669
+ useForma({ spec, initialData: { name: "John" } }),
654
670
  );
655
671
 
656
672
  // No errors - should be able to proceed
657
673
  expect(result.current.wizard?.canProceed).toBe(true);
658
- expect(result.current.errors.filter(e => e.severity === "error")).toHaveLength(0);
674
+ expect(
675
+ result.current.errors.filter((e) => e.severity === "error"),
676
+ ).toHaveLength(0);
659
677
  });
660
678
  });
661
679
 
@@ -669,12 +687,17 @@ describe("canProceed", () => {
669
687
  },
670
688
  pages: [
671
689
  { id: "page1", title: "Page 1", fields: ["showPage2", "page1Field"] },
672
- { id: "page2", title: "Page 2", fields: ["page2Field"], visibleWhen: "showPage2 = true" },
690
+ {
691
+ id: "page2",
692
+ title: "Page 2",
693
+ fields: ["page2Field"],
694
+ visibleWhen: "showPage2 = true",
695
+ },
673
696
  ],
674
697
  });
675
698
 
676
699
  const { result } = renderHook(() =>
677
- useForma({ spec, initialData: { showPage2: true } })
700
+ useForma({ spec, initialData: { showPage2: true } }),
678
701
  );
679
702
 
680
703
  // Navigate to page 2
@@ -703,14 +726,23 @@ describe("canProceed", () => {
703
726
  page3Field: { type: "text", label: "Page 3 Field" },
704
727
  },
705
728
  pages: [
706
- { id: "page1", title: "Page 1", fields: ["skipMiddle", "page1Field"] },
707
- { id: "page2", title: "Page 2", fields: ["page2Field"], visibleWhen: "skipMiddle = false" },
729
+ {
730
+ id: "page1",
731
+ title: "Page 1",
732
+ fields: ["skipMiddle", "page1Field"],
733
+ },
734
+ {
735
+ id: "page2",
736
+ title: "Page 2",
737
+ fields: ["page2Field"],
738
+ visibleWhen: "skipMiddle = false",
739
+ },
708
740
  { id: "page3", title: "Page 3", fields: ["page3Field"] },
709
741
  ],
710
742
  });
711
743
 
712
744
  const { result } = renderHook(() =>
713
- useForma({ spec, initialData: { skipMiddle: true } })
745
+ useForma({ spec, initialData: { skipMiddle: true } }),
714
746
  );
715
747
 
716
748
  // With skipMiddle=true, page2 is hidden
@@ -734,13 +766,22 @@ describe("canProceed", () => {
734
766
  optionalField: { type: "text", label: "Optional" },
735
767
  },
736
768
  pages: [
737
- { id: "main", title: "Main", fields: ["showOptional", "requiredField"] },
738
- { id: "optional", title: "Optional", fields: ["optionalField"], visibleWhen: "showOptional = true" },
769
+ {
770
+ id: "main",
771
+ title: "Main",
772
+ fields: ["showOptional", "requiredField"],
773
+ },
774
+ {
775
+ id: "optional",
776
+ title: "Optional",
777
+ fields: ["optionalField"],
778
+ visibleWhen: "showOptional = true",
779
+ },
739
780
  ],
740
781
  });
741
782
 
742
783
  const { result } = renderHook(() =>
743
- useForma({ spec, initialData: { showOptional: false } })
784
+ useForma({ spec, initialData: { showOptional: false } }),
744
785
  );
745
786
 
746
787
  // All pages are returned