@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.
@@ -29,10 +29,7 @@ describe("FormRenderer", () => {
29
29
  });
30
30
 
31
31
  render(
32
- <FormRenderer
33
- spec={spec}
34
- components={createTestComponentMap()}
35
- />
32
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
36
33
  );
37
34
 
38
35
  expect(screen.getByTestId("field-name")).toBeInTheDocument();
@@ -50,10 +47,7 @@ describe("FormRenderer", () => {
50
47
  });
51
48
 
52
49
  const { container } = render(
53
- <FormRenderer
54
- spec={spec}
55
- components={createTestComponentMap()}
56
- />
50
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
57
51
  );
58
52
 
59
53
  const fields = container.querySelectorAll("[data-testid^='field-']");
@@ -74,7 +68,7 @@ describe("FormRenderer", () => {
74
68
  spec={spec}
75
69
  initialData={{ name: "John Doe" }}
76
70
  components={createTestComponentMap()}
77
- />
71
+ />,
78
72
  );
79
73
 
80
74
  const input = screen.getByRole("textbox");
@@ -95,7 +89,7 @@ describe("FormRenderer", () => {
95
89
  spec={spec}
96
90
  components={createTestComponentMap()}
97
91
  layout={CustomLayout}
98
- />
92
+ />,
99
93
  );
100
94
 
101
95
  expect(screen.getByTestId("custom-layout")).toBeInTheDocument();
@@ -113,10 +107,7 @@ describe("FormRenderer", () => {
113
107
  });
114
108
 
115
109
  render(
116
- <FormRenderer
117
- spec={spec}
118
- components={createTestComponentMap()}
119
- />
110
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
120
111
  );
121
112
 
122
113
  expect(screen.getByRole("textbox")).toBeInTheDocument();
@@ -128,10 +119,7 @@ describe("FormRenderer", () => {
128
119
  });
129
120
 
130
121
  render(
131
- <FormRenderer
132
- spec={spec}
133
- components={createTestComponentMap()}
134
- />
122
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
135
123
  );
136
124
 
137
125
  expect(screen.getByRole("spinbutton")).toBeInTheDocument();
@@ -143,10 +131,7 @@ describe("FormRenderer", () => {
143
131
  });
144
132
 
145
133
  render(
146
- <FormRenderer
147
- spec={spec}
148
- components={createTestComponentMap()}
149
- />
134
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
150
135
  );
151
136
 
152
137
  expect(screen.getByRole("checkbox")).toBeInTheDocument();
@@ -166,10 +151,7 @@ describe("FormRenderer", () => {
166
151
  });
167
152
 
168
153
  render(
169
- <FormRenderer
170
- spec={spec}
171
- components={createTestComponentMap()}
172
- />
154
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
173
155
  );
174
156
 
175
157
  expect(screen.getByRole("combobox")).toBeInTheDocument();
@@ -193,7 +175,7 @@ describe("FormRenderer", () => {
193
175
  spec={spec}
194
176
  initialData={{ items: [{ name: "Item 1" }] }}
195
177
  components={createTestComponentMap()}
196
- />
178
+ />,
197
179
  );
198
180
 
199
181
  expect(screen.getByTestId("field-items")).toBeInTheDocument();
@@ -218,7 +200,7 @@ describe("FormRenderer", () => {
218
200
  spec={spec}
219
201
  components={createTestComponentMap()}
220
202
  onChange={onChange}
221
- />
203
+ />,
222
204
  );
223
205
 
224
206
  const input = screen.getByRole("textbox");
@@ -239,7 +221,7 @@ describe("FormRenderer", () => {
239
221
  spec={spec}
240
222
  initialData={{ agree: false }}
241
223
  components={createTestComponentMap()}
242
- />
224
+ />,
243
225
  );
244
226
 
245
227
  const checkbox = screen.getByRole("checkbox");
@@ -264,10 +246,7 @@ describe("FormRenderer", () => {
264
246
  });
265
247
 
266
248
  render(
267
- <FormRenderer
268
- spec={spec}
269
- components={createTestComponentMap()}
270
- />
249
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
271
250
  );
272
251
 
273
252
  const select = screen.getByRole("combobox");
@@ -295,7 +274,7 @@ describe("FormRenderer", () => {
295
274
  initialData={{ name: "John" }}
296
275
  onSubmit={onSubmit}
297
276
  components={createTestComponentMap()}
298
- />
277
+ />,
299
278
  );
300
279
 
301
280
  const submitButton = screen.getByRole("button", { name: /submit/i });
@@ -316,7 +295,7 @@ describe("FormRenderer", () => {
316
295
  spec={spec}
317
296
  onSubmit={onSubmit}
318
297
  components={createTestComponentMap()}
319
- />
298
+ />,
320
299
  );
321
300
 
322
301
  const submitButton = screen.getByRole("button", { name: /submit/i });
@@ -332,10 +311,7 @@ describe("FormRenderer", () => {
332
311
  });
333
312
 
334
313
  render(
335
- <FormRenderer
336
- spec={spec}
337
- components={createTestComponentMap()}
338
- />
314
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
339
315
  );
340
316
 
341
317
  const submitButton = screen.getByRole("button", { name: /submit/i });
@@ -350,7 +326,7 @@ describe("FormRenderer", () => {
350
326
  it("should disable submit button while submitting", async () => {
351
327
  const user = userEvent.setup();
352
328
  const onSubmit = vi.fn(
353
- (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 100))
329
+ (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 100)),
354
330
  );
355
331
  const spec = createTestSpec({
356
332
  fields: { name: { type: "text" } },
@@ -362,7 +338,7 @@ describe("FormRenderer", () => {
362
338
  initialData={{ name: "John" }}
363
339
  onSubmit={onSubmit}
364
340
  components={createTestComponentMap()}
365
- />
341
+ />,
366
342
  );
367
343
 
368
344
  const submitButton = screen.getByRole("button", { name: /submit/i });
@@ -594,7 +570,7 @@ describe("FormRenderer", () => {
594
570
  spec={spec}
595
571
  initialData={{ name: "Test" }}
596
572
  components={components}
597
- />
573
+ />,
598
574
  );
599
575
 
600
576
  expect(screen.getByTestId("consumer")).toBeInTheDocument();
@@ -609,7 +585,7 @@ describe("FormRenderer", () => {
609
585
  }
610
586
 
611
587
  expect(() => render(<BadComponent />)).toThrow(
612
- /useFormaContext must be used within/
588
+ /useFormaContext must be used within/,
613
589
  );
614
590
  });
615
591
  });
@@ -632,10 +608,7 @@ describe("FormRenderer", () => {
632
608
  });
633
609
 
634
610
  render(
635
- <FormRenderer
636
- spec={spec}
637
- components={createTestComponentMap()}
638
- />
611
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
639
612
  );
640
613
 
641
614
  // First page should be visible
@@ -662,7 +635,7 @@ describe("FormRenderer", () => {
662
635
  spec={spec}
663
636
  components={createTestComponentMap()}
664
637
  pageWrapper={CustomPageWrapper}
665
- />
638
+ />,
666
639
  );
667
640
 
668
641
  const pageWrapper = screen.getByTestId("custom-page");
@@ -693,7 +666,7 @@ describe("FormRenderer", () => {
693
666
  spec={spec}
694
667
  initialData={{ showDetails: false }}
695
668
  components={createTestComponentMap()}
696
- />
669
+ />,
697
670
  );
698
671
 
699
672
  // Details field should not be rendered when hidden
@@ -718,7 +691,7 @@ describe("FormRenderer", () => {
718
691
  spec={spec}
719
692
  initialData={{ showDetails: false }}
720
693
  components={createTestComponentMap()}
721
- />
694
+ />,
722
695
  );
723
696
 
724
697
  // Initially hidden
@@ -756,29 +729,46 @@ describe("FormRenderer", () => {
756
729
  type: "select",
757
730
  label: "Position",
758
731
  options: [
759
- { value: "dev", label: "Developer", visibleWhen: 'department = "engineering"' },
760
- { value: "qa", label: "QA Engineer", visibleWhen: 'department = "engineering"' },
761
- { value: "rep", label: "Sales Rep", visibleWhen: 'department = "sales"' },
762
- { value: "mgr", label: "Sales Manager", visibleWhen: 'department = "sales"' },
732
+ {
733
+ value: "dev",
734
+ label: "Developer",
735
+ visibleWhen: 'department = "engineering"',
736
+ },
737
+ {
738
+ value: "qa",
739
+ label: "QA Engineer",
740
+ visibleWhen: 'department = "engineering"',
741
+ },
742
+ {
743
+ value: "rep",
744
+ label: "Sales Rep",
745
+ visibleWhen: 'department = "sales"',
746
+ },
747
+ {
748
+ value: "mgr",
749
+ label: "Sales Manager",
750
+ visibleWhen: 'department = "sales"',
751
+ },
763
752
  ],
764
753
  },
765
754
  },
766
755
  });
767
756
 
768
757
  render(
769
- <FormRenderer
770
- spec={spec}
771
- components={createTestComponentMap()}
772
- />
758
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
773
759
  );
774
760
 
775
761
  // Initially no department selected - no position options should show
776
- const positionSelect = screen.getByTestId("field-position").querySelector("select")!;
762
+ const positionSelect = screen
763
+ .getByTestId("field-position")
764
+ .querySelector("select")!;
777
765
  // Only the placeholder "Select..." option should be present
778
766
  expect(positionSelect.querySelectorAll("option")).toHaveLength(1);
779
767
 
780
768
  // Select Engineering department
781
- const departmentSelect = screen.getByTestId("field-department").querySelector("select")!;
769
+ const departmentSelect = screen
770
+ .getByTestId("field-department")
771
+ .querySelector("select")!;
782
772
  await user.selectOptions(departmentSelect, "engineering");
783
773
 
784
774
  // Now only engineering positions should show
@@ -823,10 +813,7 @@ describe("FormRenderer", () => {
823
813
  });
824
814
 
825
815
  render(
826
- <FormRenderer
827
- spec={spec}
828
- components={createTestComponentMap()}
829
- />
816
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
830
817
  );
831
818
 
832
819
  expect(screen.getByText("Red")).toBeInTheDocument();
@@ -851,22 +838,29 @@ describe("FormRenderer", () => {
851
838
  label: "Features",
852
839
  options: [
853
840
  { value: "email", label: "Email Support" },
854
- { value: "phone", label: "Phone Support", visibleWhen: 'tier = "premium"' },
855
- { value: "priority", label: "Priority Queue", visibleWhen: 'tier = "premium"' },
841
+ {
842
+ value: "phone",
843
+ label: "Phone Support",
844
+ visibleWhen: 'tier = "premium"',
845
+ },
846
+ {
847
+ value: "priority",
848
+ label: "Priority Queue",
849
+ visibleWhen: 'tier = "premium"',
850
+ },
856
851
  ],
857
852
  },
858
853
  },
859
854
  });
860
855
 
861
856
  render(
862
- <FormRenderer
863
- spec={spec}
864
- components={createTestComponentMap()}
865
- />
857
+ <FormRenderer spec={spec} components={createTestComponentMap()} />,
866
858
  );
867
859
 
868
860
  // Initially no tier selected - only non-conditional option visible
869
- const featuresSelect = screen.getByTestId("field-features").querySelector("select")!;
861
+ const featuresSelect = screen
862
+ .getByTestId("field-features")
863
+ .querySelector("select")!;
870
864
  await waitFor(() => {
871
865
  // placeholder + 1 option without visibleWhen
872
866
  expect(featuresSelect.querySelectorAll("option")).toHaveLength(2);
@@ -875,7 +869,9 @@ describe("FormRenderer", () => {
875
869
  });
876
870
 
877
871
  // Select Premium tier
878
- const tierSelect = screen.getByTestId("field-tier").querySelector("select")!;
872
+ const tierSelect = screen
873
+ .getByTestId("field-tier")
874
+ .querySelector("select")!;
879
875
  await user.selectOptions(tierSelect, "premium");
880
876
 
881
877
  // All options should now show
@@ -895,8 +891,8 @@ describe("FormRenderer", () => {
895
891
  type: "select",
896
892
  label: "Category",
897
893
  options: [
898
- { value: "a", label: "Option A", visibleWhen: 'toggle = true' },
899
- { value: "b", label: "Option B", visibleWhen: 'toggle = true' },
894
+ { value: "a", label: "Option A", visibleWhen: "toggle = true" },
895
+ { value: "b", label: "Option B", visibleWhen: "toggle = true" },
900
896
  ],
901
897
  },
902
898
  toggle: {
@@ -911,11 +907,13 @@ describe("FormRenderer", () => {
911
907
  spec={spec}
912
908
  initialData={{ toggle: false }}
913
909
  components={createTestComponentMap()}
914
- />
910
+ />,
915
911
  );
916
912
 
917
913
  // All options have visibleWhen that evaluates to false
918
- const categorySelect = screen.getByTestId("field-category").querySelector("select")!;
914
+ const categorySelect = screen
915
+ .getByTestId("field-category")
916
+ .querySelector("select")!;
919
917
  // Only placeholder
920
918
  expect(categorySelect.querySelectorAll("option")).toHaveLength(1);
921
919
  });
@@ -943,7 +941,7 @@ describe("FormRenderer", () => {
943
941
  spec={spec}
944
942
  initialData={{ items: [] }}
945
943
  components={createTestComponentMap()}
946
- />
944
+ />,
947
945
  );
948
946
 
949
947
  const addButton = screen.getByTestId("add-items");
@@ -971,7 +969,7 @@ describe("FormRenderer", () => {
971
969
  spec={spec}
972
970
  initialData={{ items: [{ name: "Item 1" }, { name: "Item 2" }] }}
973
971
  components={createTestComponentMap()}
974
- />
972
+ />,
975
973
  );
976
974
 
977
975
  expect(screen.getByTestId("array-item-items-0")).toBeInTheDocument();
@@ -982,7 +980,9 @@ describe("FormRenderer", () => {
982
980
 
983
981
  await waitFor(() => {
984
982
  // After removing first item, we should have only 1 item (at index 0)
985
- expect(screen.queryByTestId("array-item-items-1")).not.toBeInTheDocument();
983
+ expect(
984
+ screen.queryByTestId("array-item-items-1"),
985
+ ).not.toBeInTheDocument();
986
986
  });
987
987
  });
988
988
 
@@ -1006,7 +1006,7 @@ describe("FormRenderer", () => {
1006
1006
  spec={spec}
1007
1007
  initialData={{ items: [] }}
1008
1008
  components={createTestComponentMap()}
1009
- />
1009
+ />,
1010
1010
  );
1011
1011
 
1012
1012
  const addButton = screen.getByTestId("add-items");
@@ -1050,7 +1050,7 @@ describe("FormRenderer", () => {
1050
1050
  spec={spec}
1051
1051
  initialData={{ items: [] }}
1052
1052
  components={createTestComponentMap()}
1053
- />
1053
+ />,
1054
1054
  );
1055
1055
 
1056
1056
  const addButton = screen.getByTestId("add-items");
@@ -1072,7 +1072,9 @@ describe("FormRenderer", () => {
1072
1072
  // Should have 2 items remaining (indices 0 and 1)
1073
1073
  expect(screen.getByTestId("array-item-items-0")).toBeInTheDocument();
1074
1074
  expect(screen.getByTestId("array-item-items-1")).toBeInTheDocument();
1075
- expect(screen.queryByTestId("array-item-items-2")).not.toBeInTheDocument();
1075
+ expect(
1076
+ screen.queryByTestId("array-item-items-2"),
1077
+ ).not.toBeInTheDocument();
1076
1078
  });
1077
1079
  });
1078
1080
 
@@ -1095,7 +1097,7 @@ describe("FormRenderer", () => {
1095
1097
  initialData={{ items: [{ name: "Existing Item" }] }}
1096
1098
  components={createTestComponentMap()}
1097
1099
  onChange={onChange}
1098
- />
1100
+ />,
1099
1101
  );
1100
1102
 
1101
1103
  // Verify initial state
@@ -1134,7 +1136,7 @@ describe("FormRenderer", () => {
1134
1136
  spec={spec}
1135
1137
  initialData={{ items: [] }}
1136
1138
  components={createTestComponentMap()}
1137
- />
1139
+ />,
1138
1140
  );
1139
1141
 
1140
1142
  const addButton = screen.getByTestId("add-items");
@@ -1147,10 +1149,187 @@ describe("FormRenderer", () => {
1147
1149
  // All 5 items should be present
1148
1150
  await waitFor(() => {
1149
1151
  for (let i = 0; i < 5; i++) {
1150
- expect(screen.getByTestId(`array-item-items-${i}`)).toBeInTheDocument();
1152
+ expect(
1153
+ screen.getByTestId(`array-item-items-${i}`),
1154
+ ).toBeInTheDocument();
1151
1155
  }
1152
1156
  });
1153
1157
  });
1154
1158
  });
1155
1159
  });
1160
+
1161
+ // ============================================================================
1162
+ // Visibility Wrapper Stability
1163
+ // ============================================================================
1164
+
1165
+ describe("visibility wrapper stability", () => {
1166
+ it("should render a hidden wrapper div when field is invisible", () => {
1167
+ const spec = createTestSpec({
1168
+ fields: {
1169
+ toggle: { type: "boolean", label: "Toggle" },
1170
+ details: {
1171
+ type: "text",
1172
+ label: "Details",
1173
+ visibleWhen: "toggle = true",
1174
+ },
1175
+ },
1176
+ });
1177
+
1178
+ const { container } = render(
1179
+ <FormRenderer
1180
+ spec={spec}
1181
+ initialData={{ toggle: false }}
1182
+ components={createTestComponentMap()}
1183
+ />,
1184
+ );
1185
+
1186
+ // The wrapper div should exist with hidden attribute
1187
+ const wrapper = container.querySelector('[data-field-path="details"]');
1188
+ expect(wrapper).toBeInTheDocument();
1189
+ expect(wrapper).toHaveAttribute("hidden");
1190
+ // Should have no children (field content not rendered)
1191
+ expect(wrapper!.children).toHaveLength(0);
1192
+ });
1193
+
1194
+ it("should remove hidden attribute when field becomes visible", async () => {
1195
+ const user = userEvent.setup();
1196
+ const spec = createTestSpec({
1197
+ fields: {
1198
+ toggle: { type: "boolean", label: "Toggle" },
1199
+ details: {
1200
+ type: "text",
1201
+ label: "Details",
1202
+ visibleWhen: "toggle = true",
1203
+ },
1204
+ },
1205
+ });
1206
+
1207
+ const { container } = render(
1208
+ <FormRenderer
1209
+ spec={spec}
1210
+ initialData={{ toggle: false }}
1211
+ components={createTestComponentMap()}
1212
+ />,
1213
+ );
1214
+
1215
+ // Initially hidden
1216
+ const wrapper = container.querySelector('[data-field-path="details"]');
1217
+ expect(wrapper).toHaveAttribute("hidden");
1218
+
1219
+ // Toggle visibility
1220
+ const checkbox = screen.getByRole("checkbox");
1221
+ await user.click(checkbox);
1222
+
1223
+ // Now visible - wrapper should not have hidden attribute
1224
+ await waitFor(() => {
1225
+ const visibleWrapper = container.querySelector(
1226
+ '[data-field-path="details"]',
1227
+ );
1228
+ expect(visibleWrapper).toBeInTheDocument();
1229
+ expect(visibleWrapper).not.toHaveAttribute("hidden");
1230
+ });
1231
+ });
1232
+
1233
+ it("should reuse the same DOM node when toggling visibility", async () => {
1234
+ const user = userEvent.setup();
1235
+ const spec = createTestSpec({
1236
+ fields: {
1237
+ toggle: { type: "boolean", label: "Toggle" },
1238
+ details: {
1239
+ type: "text",
1240
+ label: "Details",
1241
+ visibleWhen: "toggle = true",
1242
+ },
1243
+ },
1244
+ });
1245
+
1246
+ const { container } = render(
1247
+ <FormRenderer
1248
+ spec={spec}
1249
+ initialData={{ toggle: false }}
1250
+ components={createTestComponentMap()}
1251
+ />,
1252
+ );
1253
+
1254
+ // Get reference to the wrapper DOM node
1255
+ const wrapperBefore = container.querySelector(
1256
+ '[data-field-path="details"]',
1257
+ );
1258
+ expect(wrapperBefore).toHaveAttribute("hidden");
1259
+
1260
+ // Toggle to visible
1261
+ const checkbox = screen.getByRole("checkbox");
1262
+ await user.click(checkbox);
1263
+
1264
+ await waitFor(() => {
1265
+ const wrapperAfter = container.querySelector(
1266
+ '[data-field-path="details"]',
1267
+ );
1268
+ expect(wrapperAfter).not.toHaveAttribute("hidden");
1269
+ // Same DOM node should be reused (React reconciliation with same key + element type)
1270
+ expect(wrapperAfter).toBe(wrapperBefore);
1271
+ });
1272
+ });
1273
+
1274
+ it("should not render field content inside hidden wrapper", () => {
1275
+ const spec = createTestSpec({
1276
+ fields: {
1277
+ toggle: { type: "boolean", label: "Toggle" },
1278
+ details: {
1279
+ type: "text",
1280
+ label: "Details",
1281
+ visibleWhen: "toggle = true",
1282
+ },
1283
+ },
1284
+ });
1285
+
1286
+ const { container } = render(
1287
+ <FormRenderer
1288
+ spec={spec}
1289
+ initialData={{ toggle: false }}
1290
+ components={createTestComponentMap()}
1291
+ />,
1292
+ );
1293
+
1294
+ const wrapper = container.querySelector('[data-field-path="details"]');
1295
+ expect(wrapper).toHaveAttribute("hidden");
1296
+ // The test component renders data-testid="field-details" - should not exist
1297
+ expect(screen.queryByTestId("field-details")).not.toBeInTheDocument();
1298
+ // Wrapper should be empty
1299
+ expect(wrapper!.innerHTML).toBe("");
1300
+ });
1301
+
1302
+ it("should render field content inside visible wrapper", async () => {
1303
+ const user = userEvent.setup();
1304
+ const spec = createTestSpec({
1305
+ fields: {
1306
+ toggle: { type: "boolean", label: "Toggle" },
1307
+ details: {
1308
+ type: "text",
1309
+ label: "Details",
1310
+ visibleWhen: "toggle = true",
1311
+ },
1312
+ },
1313
+ });
1314
+
1315
+ const { container } = render(
1316
+ <FormRenderer
1317
+ spec={spec}
1318
+ initialData={{ toggle: false }}
1319
+ components={createTestComponentMap()}
1320
+ />,
1321
+ );
1322
+
1323
+ // Toggle to visible
1324
+ await user.click(screen.getByRole("checkbox"));
1325
+
1326
+ await waitFor(() => {
1327
+ const wrapper = container.querySelector('[data-field-path="details"]');
1328
+ expect(wrapper).not.toHaveAttribute("hidden");
1329
+ // Field content should be rendered inside
1330
+ expect(wrapper!.children.length).toBeGreaterThan(0);
1331
+ expect(screen.getByTestId("field-details")).toBeInTheDocument();
1332
+ });
1333
+ });
1334
+ });
1156
1335
  });