@fogpipe/forma-react 0.12.0 → 0.14.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/README.md +140 -61
- package/dist/index.d.ts +90 -1
- package/dist/index.js +303 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/ErrorBoundary.tsx +14 -7
- package/src/FieldRenderer.tsx +3 -1
- package/src/FormRenderer.tsx +3 -1
- package/src/__tests__/FieldRenderer.test.tsx +128 -1
- package/src/__tests__/FormRenderer.test.tsx +54 -0
- package/src/__tests__/canProceed.test.ts +141 -100
- package/src/__tests__/diabetes-trial-flow.test.ts +235 -66
- package/src/__tests__/events.test.ts +752 -0
- package/src/__tests__/null-handling.test.ts +27 -8
- package/src/__tests__/optionVisibility.test.tsx +199 -58
- package/src/__tests__/test-utils.tsx +26 -7
- package/src/__tests__/useForma.test.ts +244 -73
- package/src/context.ts +3 -1
- package/src/events.ts +186 -0
- package/src/index.ts +11 -1
- package/src/types.ts +65 -14
- package/src/useForma.ts +292 -53
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Field type rendering
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { describe, it, expect, vi } from "vitest";
|
|
11
11
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
12
12
|
import { userEvent } from "@testing-library/user-event";
|
|
13
13
|
import { FormRenderer } from "../FormRenderer.js";
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
JSONSchemaInteger,
|
|
20
20
|
} from "@fogpipe/forma-core";
|
|
21
21
|
import type {
|
|
22
|
+
ArrayComponentProps,
|
|
22
23
|
ComponentMap,
|
|
23
24
|
LayoutProps,
|
|
24
25
|
NumberComponentProps,
|
|
@@ -380,6 +381,132 @@ describe("FieldRenderer", () => {
|
|
|
380
381
|
});
|
|
381
382
|
});
|
|
382
383
|
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// Array Item Default Values
|
|
386
|
+
// ============================================================================
|
|
387
|
+
|
|
388
|
+
describe("array item default values", () => {
|
|
389
|
+
/**
|
|
390
|
+
* Create a component map where the array add button calls push()
|
|
391
|
+
* without arguments, triggering createDefaultItem() in FieldRenderer.
|
|
392
|
+
*/
|
|
393
|
+
function createDefaultItemComponentMap(): ComponentMap {
|
|
394
|
+
const base = createTestComponentMap();
|
|
395
|
+
return {
|
|
396
|
+
...base,
|
|
397
|
+
array: ({ field: props }: { field: ArrayComponentProps["field"] }) => {
|
|
398
|
+
const { name, field, value, helpers } = props;
|
|
399
|
+
const items = (value || []) as unknown[];
|
|
400
|
+
return (
|
|
401
|
+
<div data-testid={`field-${name}`}>
|
|
402
|
+
<label>{field.label}</label>
|
|
403
|
+
<div>
|
|
404
|
+
{items.map((_, index) => (
|
|
405
|
+
<div key={index} data-testid={`array-item-${name}-${index}`}>
|
|
406
|
+
Item {index}
|
|
407
|
+
</div>
|
|
408
|
+
))}
|
|
409
|
+
</div>
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
onClick={() => helpers.push()}
|
|
413
|
+
data-testid={`add-${name}`}
|
|
414
|
+
>
|
|
415
|
+
Add
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
it("should use defaultValue from itemFields when adding array items", async () => {
|
|
424
|
+
const user = userEvent.setup();
|
|
425
|
+
const onChange = vi.fn();
|
|
426
|
+
const spec = createTestSpec({
|
|
427
|
+
fields: {
|
|
428
|
+
items: {
|
|
429
|
+
type: "array",
|
|
430
|
+
label: "Items",
|
|
431
|
+
itemFields: {
|
|
432
|
+
name: { type: "text", defaultValue: "New Item" },
|
|
433
|
+
quantity: { type: "integer", defaultValue: 1 },
|
|
434
|
+
active: { type: "boolean", defaultValue: true },
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
render(
|
|
441
|
+
<FormRenderer
|
|
442
|
+
spec={spec}
|
|
443
|
+
initialData={{ items: [] }}
|
|
444
|
+
components={createDefaultItemComponentMap()}
|
|
445
|
+
onChange={onChange}
|
|
446
|
+
/>,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const addButton = screen.getByTestId("add-items");
|
|
450
|
+
await user.click(addButton);
|
|
451
|
+
|
|
452
|
+
await waitFor(() => {
|
|
453
|
+
expect(screen.getByTestId("array-item-items-0")).toBeInTheDocument();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Verify onChange was called with default values from itemFields
|
|
457
|
+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
|
458
|
+
const data = lastCall[0];
|
|
459
|
+
expect(data.items).toHaveLength(1);
|
|
460
|
+
expect(data.items[0]).toEqual({
|
|
461
|
+
name: "New Item",
|
|
462
|
+
quantity: 1,
|
|
463
|
+
active: true,
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("should fall back to type defaults when itemFields lack defaultValue", async () => {
|
|
468
|
+
const user = userEvent.setup();
|
|
469
|
+
const onChange = vi.fn();
|
|
470
|
+
const spec = createTestSpec({
|
|
471
|
+
fields: {
|
|
472
|
+
items: {
|
|
473
|
+
type: "array",
|
|
474
|
+
label: "Items",
|
|
475
|
+
itemFields: {
|
|
476
|
+
name: { type: "text" },
|
|
477
|
+
count: { type: "number" },
|
|
478
|
+
enabled: { type: "boolean" },
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
render(
|
|
485
|
+
<FormRenderer
|
|
486
|
+
spec={spec}
|
|
487
|
+
initialData={{ items: [] }}
|
|
488
|
+
components={createDefaultItemComponentMap()}
|
|
489
|
+
onChange={onChange}
|
|
490
|
+
/>,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const addButton = screen.getByTestId("add-items");
|
|
494
|
+
await user.click(addButton);
|
|
495
|
+
|
|
496
|
+
await waitFor(() => {
|
|
497
|
+
expect(screen.getByTestId("array-item-items-0")).toBeInTheDocument();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
|
|
501
|
+
const data = lastCall[0];
|
|
502
|
+
expect(data.items[0]).toEqual({
|
|
503
|
+
name: "",
|
|
504
|
+
count: null,
|
|
505
|
+
enabled: false,
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
383
510
|
// ============================================================================
|
|
384
511
|
// FieldRenderer Visibility Wrapper Stability
|
|
385
512
|
// ============================================================================
|
|
@@ -75,6 +75,60 @@ describe("FormRenderer", () => {
|
|
|
75
75
|
expect(input).toHaveValue("John Doe");
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
it("should render with defaultValue pre-populated", () => {
|
|
79
|
+
const spec = createTestSpec({
|
|
80
|
+
fields: {
|
|
81
|
+
country: {
|
|
82
|
+
type: "select",
|
|
83
|
+
label: "Country",
|
|
84
|
+
defaultValue: "us",
|
|
85
|
+
options: [
|
|
86
|
+
{ value: "us", label: "United States" },
|
|
87
|
+
{ value: "ca", label: "Canada" },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
quantity: {
|
|
91
|
+
type: "number",
|
|
92
|
+
label: "Quantity",
|
|
93
|
+
defaultValue: 5,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
render(
|
|
99
|
+
<FormRenderer spec={spec} components={createTestComponentMap()} />,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const select = screen.getByRole("combobox");
|
|
103
|
+
expect(select).toHaveValue("us");
|
|
104
|
+
|
|
105
|
+
const numberInput = screen.getByRole("spinbutton");
|
|
106
|
+
expect(numberInput).toHaveValue(5);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should let initialData override defaultValue in rendered output", () => {
|
|
110
|
+
const spec = createTestSpec({
|
|
111
|
+
fields: {
|
|
112
|
+
name: {
|
|
113
|
+
type: "text",
|
|
114
|
+
label: "Name",
|
|
115
|
+
defaultValue: "Default Name",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
render(
|
|
121
|
+
<FormRenderer
|
|
122
|
+
spec={spec}
|
|
123
|
+
initialData={{ name: "Override" }}
|
|
124
|
+
components={createTestComponentMap()}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const input = screen.getByRole("textbox");
|
|
129
|
+
expect(input).toHaveValue("Override");
|
|
130
|
+
});
|
|
131
|
+
|
|
78
132
|
it("should use custom layout component", () => {
|
|
79
133
|
const spec = createTestSpec({
|
|
80
134
|
fields: { name: { type: "text" } },
|