@fogpipe/forma-react 0.12.0-alpha.1 → 0.12.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.
@@ -8,18 +8,29 @@
8
8
  */
9
9
 
10
10
  import { describe, it, expect } from "vitest";
11
- import { render } from "@testing-library/react";
11
+ import { render, screen, waitFor } from "@testing-library/react";
12
+ import { userEvent } from "@testing-library/user-event";
12
13
  import { FormRenderer } from "../FormRenderer.js";
13
- import { createTestComponentMap } from "./test-utils.js";
14
- import type { Forma, JSONSchemaNumber, JSONSchemaInteger } from "@fogpipe/forma-core";
15
- import type { ComponentMap, NumberComponentProps, IntegerComponentProps } from "../types.js";
14
+ import { FieldRenderer } from "../FieldRenderer.js";
15
+ import { createTestSpec, createTestComponentMap } from "./test-utils.js";
16
+ import type {
17
+ Forma,
18
+ JSONSchemaNumber,
19
+ JSONSchemaInteger,
20
+ } from "@fogpipe/forma-core";
21
+ import type {
22
+ ComponentMap,
23
+ LayoutProps,
24
+ NumberComponentProps,
25
+ IntegerComponentProps,
26
+ } from "../types.js";
16
27
 
17
28
  /**
18
29
  * Create a minimal Forma spec for testing numeric fields
19
30
  */
20
31
  function createNumericSpec(
21
32
  schemaProps: JSONSchemaNumber | JSONSchemaInteger,
22
- fieldDef: Record<string, unknown> = {}
33
+ fieldDef: Record<string, unknown> = {},
23
34
  ): Forma {
24
35
  return {
25
36
  version: "1.0",
@@ -46,7 +57,7 @@ function createNumericSpec(
46
57
  */
47
58
  function createPropsCapturingComponentMap(
48
59
  onRenderNumber: (props: NumberComponentProps["field"]) => void,
49
- onRenderInteger?: (props: IntegerComponentProps["field"]) => void
60
+ onRenderInteger?: (props: IntegerComponentProps["field"]) => void,
50
61
  ): ComponentMap {
51
62
  const baseComponents = createTestComponentMap();
52
63
 
@@ -62,7 +73,9 @@ function createPropsCapturingComponentMap(
62
73
  data-max={props.max}
63
74
  data-step={props.step}
64
75
  value={props.value ?? ""}
65
- onChange={(e) => props.onChange(e.target.value ? Number(e.target.value) : null)}
76
+ onChange={(e) =>
77
+ props.onChange(e.target.value ? Number(e.target.value) : null)
78
+ }
66
79
  />
67
80
  </div>
68
81
  );
@@ -76,7 +89,9 @@ function createPropsCapturingComponentMap(
76
89
  data-min={props.min}
77
90
  data-max={props.max}
78
91
  value={props.value ?? ""}
79
- onChange={(e) => props.onChange(e.target.value ? Number(e.target.value) : null)}
92
+ onChange={(e) =>
93
+ props.onChange(e.target.value ? Number(e.target.value) : null)
94
+ }
80
95
  />
81
96
  </div>
82
97
  );
@@ -107,7 +122,7 @@ describe("FieldRenderer", () => {
107
122
  components={createPropsCapturingComponentMap((props) => {
108
123
  capturedProps = props;
109
124
  })}
110
- />
125
+ />,
111
126
  );
112
127
 
113
128
  expect(capturedProps).not.toBeNull();
@@ -130,7 +145,7 @@ describe("FieldRenderer", () => {
130
145
  components={createPropsCapturingComponentMap((props) => {
131
146
  capturedProps = props;
132
147
  })}
133
- />
148
+ />,
134
149
  );
135
150
 
136
151
  expect(capturedProps!.step).toBe(0.5);
@@ -150,7 +165,7 @@ describe("FieldRenderer", () => {
150
165
  components={createPropsCapturingComponentMap((props) => {
151
166
  capturedProps = props;
152
167
  })}
153
- />
168
+ />,
154
169
  );
155
170
 
156
171
  expect(capturedProps!.step).toBe(0.1);
@@ -172,9 +187,9 @@ describe("FieldRenderer", () => {
172
187
  () => {},
173
188
  (props) => {
174
189
  capturedProps = props;
175
- }
190
+ },
176
191
  )}
177
- />
192
+ />,
178
193
  );
179
194
 
180
195
  expect(capturedProps).not.toBeNull();
@@ -214,7 +229,7 @@ describe("FieldRenderer", () => {
214
229
  components={createPropsCapturingComponentMap((props) => {
215
230
  capturedProps = props;
216
231
  })}
217
- />
232
+ />,
218
233
  );
219
234
 
220
235
  expect(capturedProps!.step).toBe(15);
@@ -236,7 +251,7 @@ describe("FieldRenderer", () => {
236
251
  components={createPropsCapturingComponentMap((props) => {
237
252
  capturedProps = props;
238
253
  })}
239
- />
254
+ />,
240
255
  );
241
256
 
242
257
  expect(capturedProps!.min).toBe(0);
@@ -261,7 +276,7 @@ describe("FieldRenderer", () => {
261
276
  components={createPropsCapturingComponentMap((props) => {
262
277
  capturedProps = props;
263
278
  })}
264
- />
279
+ />,
265
280
  );
266
281
 
267
282
  expect(capturedProps!.min).toBe(0.05);
@@ -282,7 +297,7 @@ describe("FieldRenderer", () => {
282
297
  components={createPropsCapturingComponentMap((props) => {
283
298
  capturedProps = props;
284
299
  })}
285
- />
300
+ />,
286
301
  );
287
302
 
288
303
  expect(capturedProps!.min).toBeUndefined();
@@ -307,7 +322,7 @@ describe("FieldRenderer", () => {
307
322
  components={createPropsCapturingComponentMap((props) => {
308
323
  capturedProps = props;
309
324
  })}
310
- />
325
+ />,
311
326
  );
312
327
 
313
328
  expect(capturedProps!.min).toBe(0.05);
@@ -331,7 +346,7 @@ describe("FieldRenderer", () => {
331
346
  components={createPropsCapturingComponentMap((props) => {
332
347
  capturedProps = props;
333
348
  })}
334
- />
349
+ />,
335
350
  );
336
351
 
337
352
  expect(capturedProps!.min).toBe(0);
@@ -355,7 +370,7 @@ describe("FieldRenderer", () => {
355
370
  components={createPropsCapturingComponentMap((props) => {
356
371
  capturedProps = props;
357
372
  })}
358
- />
373
+ />,
359
374
  );
360
375
 
361
376
  expect(capturedProps!.min).toBe(0);
@@ -364,4 +379,105 @@ describe("FieldRenderer", () => {
364
379
  });
365
380
  });
366
381
  });
382
+
383
+ // ============================================================================
384
+ // FieldRenderer Visibility Wrapper Stability
385
+ // ============================================================================
386
+
387
+ describe("visibility wrapper stability (FieldRenderer)", () => {
388
+ /**
389
+ * FieldRenderer needs FormaContext, so we render it inside FormRenderer
390
+ * with a custom layout that uses FieldRenderer directly.
391
+ */
392
+ function createFieldRendererLayout(fieldPath: string) {
393
+ return function FieldRendererLayout({ children, onSubmit }: LayoutProps) {
394
+ return (
395
+ <form
396
+ onSubmit={(e) => {
397
+ e.preventDefault();
398
+ onSubmit();
399
+ }}
400
+ >
401
+ {children}
402
+ <FieldRenderer
403
+ fieldPath={fieldPath}
404
+ components={createTestComponentMap()}
405
+ />
406
+ </form>
407
+ );
408
+ };
409
+ }
410
+
411
+ it("should render a hidden wrapper div when field is invisible", () => {
412
+ const spec = createTestSpec({
413
+ fields: {
414
+ toggle: { type: "boolean", label: "Toggle" },
415
+ details: {
416
+ type: "text",
417
+ label: "Details",
418
+ visibleWhen: "toggle = true",
419
+ },
420
+ },
421
+ });
422
+
423
+ const { container } = render(
424
+ <FormRenderer
425
+ spec={spec}
426
+ initialData={{ toggle: false }}
427
+ components={createTestComponentMap()}
428
+ layout={createFieldRendererLayout("details")}
429
+ />,
430
+ );
431
+
432
+ // FieldRenderer should produce a hidden wrapper
433
+ // Note: FormRenderer also renders its own wrapper, so find all
434
+ const wrappers = container.querySelectorAll(
435
+ '[data-field-path="details"]',
436
+ );
437
+ // At least one should have hidden attribute (the FieldRenderer one)
438
+ const hiddenWrapper = Array.from(wrappers).find((el) =>
439
+ el.hasAttribute("hidden"),
440
+ );
441
+ expect(hiddenWrapper).toBeTruthy();
442
+ expect(hiddenWrapper!.children).toHaveLength(0);
443
+ });
444
+
445
+ it("should remove hidden attribute when field becomes visible", async () => {
446
+ const user = userEvent.setup();
447
+ const spec = createTestSpec({
448
+ fields: {
449
+ toggle: { type: "boolean", label: "Toggle" },
450
+ details: {
451
+ type: "text",
452
+ label: "Details",
453
+ visibleWhen: "toggle = true",
454
+ },
455
+ },
456
+ });
457
+
458
+ const { container } = render(
459
+ <FormRenderer
460
+ spec={spec}
461
+ initialData={{ toggle: false }}
462
+ components={createTestComponentMap()}
463
+ layout={createFieldRendererLayout("details")}
464
+ />,
465
+ );
466
+
467
+ // Toggle visibility
468
+ const checkbox = screen.getByRole("checkbox");
469
+ await user.click(checkbox);
470
+
471
+ await waitFor(() => {
472
+ const wrappers = container.querySelectorAll(
473
+ '[data-field-path="details"]',
474
+ );
475
+ // The FieldRenderer wrapper should now be visible (no hidden attr)
476
+ const visibleWrappers = Array.from(wrappers).filter(
477
+ (el) => !el.hasAttribute("hidden"),
478
+ );
479
+ expect(visibleWrappers.length).toBeGreaterThan(0);
480
+ });
481
+ });
482
+ });
367
483
  });