@fogpipe/forma-react 0.11.2 → 0.12.0-alpha.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/dist/index.d.ts +45 -3
- package/dist/index.js +553 -323
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/src/FieldRenderer.tsx +107 -20
- package/src/FormRenderer.tsx +321 -157
- package/src/__tests__/FieldRenderer.test.tsx +136 -20
- package/src/__tests__/FormRenderer.test.tsx +264 -85
- package/src/index.ts +2 -0
- package/src/types.ts +44 -1
- package/src/useForma.ts +392 -235
|
@@ -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 {
|
|
14
|
-
import
|
|
15
|
-
import type {
|
|
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) =>
|
|
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) =>
|
|
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
|
});
|