@alepha/ui 0.16.1 → 0.16.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.
Files changed (150) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CoTOTfgU.js} +4 -3
  2. package/dist/admin/{AdminApiKeys-GMORg-1l.js.map → AdminApiKeys-CoTOTfgU.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BmsxFbDa.js} +4 -3
  4. package/dist/admin/{AdminAudits-pkWrjq1Z.js.map → AdminAudits-BmsxFbDa.js.map} +1 -1
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-BBB8knca.js} +4 -3
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-BBB8knca.js.map} +1 -1
  7. package/dist/admin/{AdminJobs-B-q9iGO3.js → AdminJobs-C604joTz.js} +4 -3
  8. package/dist/admin/{AdminJobs-B-q9iGO3.js.map → AdminJobs-C604joTz.js.map} +1 -1
  9. package/dist/admin/{AdminLayout-BqZiXx4H.js → AdminLayout-CsjvpeD1.js} +6 -9
  10. package/dist/admin/AdminLayout-CsjvpeD1.js.map +1 -0
  11. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-LwR6RKrx.js} +4 -3
  12. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-LwR6RKrx.js.map} +1 -1
  13. package/dist/admin/AdminParameters-B_83Vie9.js +767 -0
  14. package/dist/admin/AdminParameters-B_83Vie9.js.map +1 -0
  15. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-CWnPosdd.js} +4 -3
  16. package/dist/admin/{AdminSessions-DzIOxM3b.js.map → AdminSessions-CWnPosdd.js.map} +1 -1
  17. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-nHv636E_.js} +4 -3
  18. package/dist/admin/{AdminUserAudits-CiUPN2BC.js.map → AdminUserAudits-nHv636E_.js.map} +1 -1
  19. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CjYD3Kjc.js} +4 -3
  20. package/dist/admin/{AdminUserCreate-BwQKr4xE.js.map → AdminUserCreate-CjYD3Kjc.js.map} +1 -1
  21. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-Ccq-LsZ0.js} +4 -3
  22. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js.map → AdminUserDetails-Ccq-LsZ0.js.map} +1 -1
  23. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-7s41DiF_.js} +6 -7
  24. package/dist/admin/AdminUserLayout-7s41DiF_.js.map +1 -0
  25. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Ds3ODq_d.js} +4 -3
  26. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js.map → AdminUserSessions-Ds3ODq_d.js.map} +1 -1
  27. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-CGh4gROo.js} +4 -3
  28. package/dist/admin/{AdminUserSettings-EbahaV2a.js.map → AdminUserSettings-CGh4gROo.js.map} +1 -1
  29. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-CvPiBzQK.js} +4 -3
  30. package/dist/admin/{AdminUsers-Dcjh0KNW.js.map → AdminUsers-CvPiBzQK.js.map} +1 -1
  31. package/dist/admin/index.d.ts +22 -10
  32. package/dist/admin/index.d.ts.map +1 -1
  33. package/dist/admin/index.js +47 -48
  34. package/dist/admin/index.js.map +1 -1
  35. package/dist/admin/rolldown-runtime-CjeV3_4I.js +18 -0
  36. package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-CdJcrPs4.js} +2 -3
  37. package/dist/auth/{AuthLayout-Dj5K4SIN.js.map → AuthLayout-CdJcrPs4.js.map} +1 -1
  38. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  39. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  40. package/dist/auth/{Login-BBqTosqZ.js → Login-DS_OqA0G.js} +7 -6
  41. package/dist/auth/Login-DS_OqA0G.js.map +1 -0
  42. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-Di7N7HZL.js} +2 -3
  43. package/dist/auth/{Profile-Bxj8Nwom.js.map → Profile-Di7N7HZL.js.map} +1 -1
  44. package/dist/auth/{Register-Ce675Crg.js → Register-BRR2_gux.js} +7 -6
  45. package/dist/auth/Register-BRR2_gux.js.map +1 -0
  46. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-oQu72lod.js} +4 -3
  47. package/dist/auth/{ResetPassword-DWdt7c40.js.map → ResetPassword-oQu72lod.js.map} +1 -1
  48. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DC6HPZjd.js} +4 -3
  49. package/dist/auth/{VerifyEmail-CI4JwByV.js.map → VerifyEmail-DC6HPZjd.js.map} +1 -1
  50. package/dist/auth/index.d.ts +14 -14
  51. package/dist/auth/index.d.ts.map +1 -1
  52. package/dist/auth/index.js +13 -13
  53. package/dist/auth/index.js.map +1 -1
  54. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  55. package/dist/core/index.d.ts +147 -68
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +349 -287
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/demo/{DemoDataTable-CguplbR7.js → DemoDataTable-DCsJq8v5.js} +4 -5
  60. package/dist/demo/DemoDataTable-DCsJq8v5.js.map +1 -0
  61. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-DpRrPlBC.js} +4 -3
  62. package/dist/demo/{DemoHome-Cce2bWmg.js.map → DemoHome-DpRrPlBC.js.map} +1 -1
  63. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-zeucGKHV.js} +6 -5
  64. package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +1 -0
  65. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-PhgbAAiQ.js} +6 -5
  66. package/dist/demo/DemoLayout-PhgbAAiQ.js.map +1 -0
  67. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DSzP0Lkv.js} +8 -10
  68. package/dist/demo/DemoLogin-DSzP0Lkv.js.map +1 -0
  69. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DavFBsCz.js} +8 -10
  70. package/dist/demo/DemoRegister-DavFBsCz.js.map +1 -0
  71. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-BS2rIAQK.js} +5 -7
  72. package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +1 -0
  73. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-zNkUmHRl.js} +4 -5
  74. package/dist/demo/DemoSidebar-zNkUmHRl.js.map +1 -0
  75. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-B9q7oT0b.js} +4 -5
  76. package/dist/demo/DemoTypeForm-B9q7oT0b.js.map +1 -0
  77. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-Bi4SdWz0.js} +5 -7
  78. package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +1 -0
  79. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-CTeZyrek.js} +2 -4
  80. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-CTeZyrek.js.map} +1 -1
  81. package/dist/demo/{Showcase-CQrMWars.js → Showcase-C9btr_SJ.js} +3 -5
  82. package/dist/demo/Showcase-C9btr_SJ.js.map +1 -0
  83. package/dist/demo/index.d.ts +2 -2
  84. package/dist/demo/index.d.ts.map +1 -1
  85. package/dist/demo/index.js +15 -15
  86. package/dist/demo/rolldown-runtime-CjeV3_4I.js +18 -0
  87. package/package.json +5 -3
  88. package/src/admin/AdminRouter.ts +15 -24
  89. package/src/admin/components/AdminLayout.tsx +6 -9
  90. package/src/admin/components/parameters/AdminParameters.tsx +154 -76
  91. package/src/admin/components/parameters/ParameterDetails.tsx +153 -93
  92. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  93. package/src/admin/components/parameters/ParameterHistory.tsx +15 -20
  94. package/src/admin/components/parameters/ParameterTree.tsx +280 -104
  95. package/src/admin/components/parameters/types.ts +3 -3
  96. package/src/admin/primitives/$uiAdmin.ts +2 -2
  97. package/src/auth/AuthRouter.ts +1 -0
  98. package/src/core/components/buttons/ActionButton.tsx +4 -15
  99. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  100. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  101. package/src/core/components/form/Control.tsx +10 -32
  102. package/src/core/components/form/ControlArray.tsx +200 -89
  103. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  104. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  105. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  106. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  107. package/src/core/components/layout/Sidebar.tsx +58 -18
  108. package/src/core/constants/ui.ts +1 -1
  109. package/src/core/helpers/renderIcon.tsx +5 -2
  110. package/src/core/index.ts +9 -5
  111. package/src/core/styles.css +7 -7
  112. package/src/core/utils/string.ts +28 -4
  113. package/src/demo/components/DemoLayout.tsx +6 -2
  114. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  115. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  116. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  117. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  118. package/dist/admin/AdminLayout-BqZiXx4H.js.map +0 -1
  119. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  120. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  121. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  122. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  123. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  124. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  125. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  126. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  127. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  128. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  129. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  130. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  131. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  132. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  133. package/dist/auth/Login-CoU63mMR.js +0 -4
  134. package/dist/auth/Register-BV_oa_AK.js +0 -4
  135. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  136. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  137. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  138. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  139. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  140. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  141. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  142. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  143. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  144. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  145. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  146. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  147. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  148. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  149. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  150. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
@@ -0,0 +1,727 @@
1
+ import { MantineProvider } from "@mantine/core";
2
+ import { fireEvent, screen, waitFor } from "@testing-library/react";
3
+ import { Alepha, t } from "alepha";
4
+ import { AlephaLogger } from "alepha/logger";
5
+ import { useForm } from "alepha/react/form";
6
+ import {
7
+ renderWithAlepha as renderWithAlephaUtil,
8
+ setupJsdomMocks,
9
+ } from "alepha/react/testing";
10
+ import { beforeAll, describe, it } from "vitest";
11
+ import TypeForm from "./TypeForm.tsx";
12
+
13
+ // Setup jsdom mocks for Mantine components
14
+ beforeAll(() => {
15
+ setupJsdomMocks();
16
+ });
17
+
18
+ describe("TypeForm", () => {
19
+ /**
20
+ * Helper to render with Alepha and Mantine.
21
+ * Uses the new alepha/react/testing utilities.
22
+ */
23
+ const renderWithAlepha = async (element: React.ReactElement) => {
24
+ const alepha = Alepha.create().with(AlephaLogger);
25
+ const result = await renderWithAlephaUtil(element, {
26
+ alepha,
27
+ wrapper: MantineProvider,
28
+ });
29
+ return { ...result, alepha };
30
+ };
31
+
32
+ describe("Text Input", () => {
33
+ it("should render text input and submit value", async ({ expect }) => {
34
+ const calls: Array<any> = [];
35
+
36
+ const Form = () => {
37
+ const form = useForm({
38
+ id: "text-test",
39
+ schema: t.object({
40
+ username: t.text(),
41
+ }),
42
+ handler: (values) => {
43
+ calls.push(values);
44
+ },
45
+ });
46
+
47
+ return <TypeForm form={form} />;
48
+ };
49
+
50
+ await renderWithAlepha(<Form />);
51
+
52
+ const input = screen.getByTestId("text-test-username");
53
+ fireEvent.change(input, { target: { value: "testuser" } });
54
+ fireEvent.submit(screen.getByText("Submit"));
55
+
56
+ await waitFor(() => expect(calls.length).toBe(1));
57
+ expect(calls[0]).toEqual({ username: "testuser" });
58
+ });
59
+
60
+ it("should use title from schema as label", async ({ expect }) => {
61
+ const Form = () => {
62
+ const form = useForm({
63
+ id: "title-test",
64
+ schema: t.object({
65
+ username: t.text({ title: "User Name" }),
66
+ }),
67
+ handler: () => {},
68
+ });
69
+
70
+ return <TypeForm form={form} />;
71
+ };
72
+
73
+ await renderWithAlepha(<Form />);
74
+
75
+ expect(screen.getByText("User Name")).toBeDefined();
76
+ });
77
+ });
78
+
79
+ describe("Number Input", () => {
80
+ it("should render number input for integer type", async ({ expect }) => {
81
+ const calls: Array<any> = [];
82
+
83
+ const Form = () => {
84
+ const form = useForm({
85
+ id: "number-test",
86
+ schema: t.object({
87
+ age: t.integer(),
88
+ }),
89
+ handler: (values) => {
90
+ calls.push(values);
91
+ },
92
+ });
93
+
94
+ return <TypeForm form={form} />;
95
+ };
96
+
97
+ await renderWithAlepha(<Form />);
98
+
99
+ const input = screen.getByTestId("number-test-age");
100
+ fireEvent.change(input, { target: { value: "25" } });
101
+ fireEvent.submit(screen.getByText("Submit"));
102
+
103
+ await waitFor(() => expect(calls.length).toBe(1));
104
+ expect(calls[0]).toEqual({ age: 25 });
105
+ });
106
+
107
+ it("should render number input for number type", async ({ expect }) => {
108
+ const calls: Array<any> = [];
109
+
110
+ const Form = () => {
111
+ const form = useForm({
112
+ id: "float-test",
113
+ schema: t.object({
114
+ price: t.number(),
115
+ }),
116
+ handler: (values) => {
117
+ calls.push(values);
118
+ },
119
+ });
120
+
121
+ return <TypeForm form={form} />;
122
+ };
123
+
124
+ await renderWithAlepha(<Form />);
125
+
126
+ const input = screen.getByTestId("float-test-price");
127
+ fireEvent.change(input, { target: { value: "19.99" } });
128
+ fireEvent.submit(screen.getByText("Submit"));
129
+
130
+ await waitFor(() => expect(calls.length).toBe(1));
131
+ expect(calls[0]).toEqual({ price: 19.99 });
132
+ });
133
+ });
134
+
135
+ describe("Boolean/Switch", () => {
136
+ it("should render switch for boolean type and submit true", async ({
137
+ expect,
138
+ }) => {
139
+ const calls: Array<any> = [];
140
+
141
+ const Form = () => {
142
+ const form = useForm({
143
+ id: "bool-test",
144
+ schema: t.object({
145
+ subscribe: t.boolean(),
146
+ }),
147
+ handler: (values) => {
148
+ calls.push(values);
149
+ },
150
+ });
151
+
152
+ return <TypeForm form={form} />;
153
+ };
154
+
155
+ await renderWithAlepha(<Form />);
156
+
157
+ // Find the switch input
158
+ const switchInput = screen.getByRole("switch");
159
+ expect(switchInput).toBeDefined();
160
+
161
+ // Click to toggle on
162
+ fireEvent.click(switchInput);
163
+ fireEvent.submit(screen.getByText("Submit"));
164
+
165
+ await waitFor(() => expect(calls.length).toBe(1));
166
+ expect(calls[0]).toEqual({ subscribe: true });
167
+ });
168
+
169
+ it("should render switch unchecked by default", async ({ expect }) => {
170
+ const Form = () => {
171
+ const form = useForm({
172
+ id: "bool-false-test",
173
+ schema: t.object({
174
+ subscribe: t.boolean(),
175
+ }),
176
+ handler: () => {},
177
+ });
178
+
179
+ return <TypeForm form={form} />;
180
+ };
181
+
182
+ await renderWithAlepha(<Form />);
183
+
184
+ const switchInput = screen.getByRole("switch");
185
+ // Should not be checked by default (no default value)
186
+ expect((switchInput as HTMLInputElement).checked).toBe(false);
187
+ });
188
+
189
+ it("should handle default true value for boolean", async ({ expect }) => {
190
+ const calls: Array<any> = [];
191
+
192
+ const Form = () => {
193
+ const form = useForm({
194
+ id: "bool-default-test",
195
+ schema: t.object({
196
+ enabled: t.boolean({ default: true }),
197
+ }),
198
+ handler: (values) => {
199
+ calls.push(values);
200
+ },
201
+ });
202
+
203
+ return <TypeForm form={form} />;
204
+ };
205
+
206
+ await renderWithAlepha(<Form />);
207
+
208
+ const switchInput = screen.getByRole("switch");
209
+ // Should be checked by default
210
+ expect((switchInput as HTMLInputElement).checked).toBe(true);
211
+
212
+ // Toggle off
213
+ fireEvent.click(switchInput);
214
+ fireEvent.submit(screen.getByText("Submit"));
215
+
216
+ await waitFor(() => expect(calls.length).toBe(1));
217
+ expect(calls[0]).toEqual({ enabled: false });
218
+ });
219
+ });
220
+
221
+ describe("Enum/Select", () => {
222
+ it("should render select for enum type", async ({ expect }) => {
223
+ const Form = () => {
224
+ const form = useForm({
225
+ id: "enum-test",
226
+ schema: t.object({
227
+ status: t.enum(["active", "inactive", "pending"]),
228
+ }),
229
+ handler: () => {},
230
+ });
231
+
232
+ return <TypeForm form={form} />;
233
+ };
234
+
235
+ await renderWithAlepha(<Form />);
236
+
237
+ // Select should be rendered - Mantine Select uses textbox role with aria-haspopup
238
+ const selectInput = screen.getByRole("textbox", { name: "Status" });
239
+ expect(selectInput).toBeDefined();
240
+ expect(selectInput.getAttribute("aria-haspopup")).toBe("listbox");
241
+ });
242
+ });
243
+
244
+ describe("Nested Objects", () => {
245
+ it("should render nested object fields", async ({ expect }) => {
246
+ const calls: Array<any> = [];
247
+
248
+ const Form = () => {
249
+ const form = useForm({
250
+ id: "nested-test",
251
+ schema: t.object({
252
+ address: t.object({
253
+ street: t.text(),
254
+ city: t.text(),
255
+ }),
256
+ }),
257
+ handler: (values) => {
258
+ calls.push(values);
259
+ },
260
+ });
261
+
262
+ return <TypeForm form={form} />;
263
+ };
264
+
265
+ await renderWithAlepha(<Form />);
266
+
267
+ // Find nested inputs (IDs use dots for nested paths)
268
+ const streetInput = screen.getByTestId("nested-test-address.street");
269
+ const cityInput = screen.getByTestId("nested-test-address.city");
270
+
271
+ fireEvent.change(streetInput, { target: { value: "123 Main St" } });
272
+ fireEvent.change(cityInput, { target: { value: "New York" } });
273
+ fireEvent.submit(screen.getByText("Submit"));
274
+
275
+ await waitFor(() => expect(calls.length).toBe(1));
276
+ expect(calls[0]).toEqual({
277
+ address: {
278
+ street: "123 Main St",
279
+ city: "New York",
280
+ },
281
+ });
282
+ });
283
+
284
+ it("should render deeply nested objects", async ({ expect }) => {
285
+ const calls: Array<any> = [];
286
+
287
+ const Form = () => {
288
+ const form = useForm({
289
+ id: "deep-nested-test",
290
+ schema: t.object({
291
+ company: t.object({
292
+ headquarters: t.object({
293
+ country: t.text(),
294
+ }),
295
+ }),
296
+ }),
297
+ handler: (values) => {
298
+ calls.push(values);
299
+ },
300
+ });
301
+
302
+ return <TypeForm form={form} />;
303
+ };
304
+
305
+ await renderWithAlepha(<Form />);
306
+
307
+ const countryInput = screen.getByTestId(
308
+ "deep-nested-test-company.headquarters.country",
309
+ );
310
+ fireEvent.change(countryInput, { target: { value: "USA" } });
311
+ fireEvent.submit(screen.getByText("Submit"));
312
+
313
+ await waitFor(() => expect(calls.length).toBe(1));
314
+ expect(calls[0]).toEqual({
315
+ company: {
316
+ headquarters: {
317
+ country: "USA",
318
+ },
319
+ },
320
+ });
321
+ });
322
+ });
323
+
324
+ describe("Array of Strings (TagsInput)", () => {
325
+ it("should render tags input for array of strings", async ({ expect }) => {
326
+ const Form = () => {
327
+ const form = useForm({
328
+ id: "tags-test",
329
+ schema: t.object({
330
+ tags: t.array(t.text()),
331
+ }),
332
+ handler: () => {},
333
+ });
334
+
335
+ return <TypeForm form={form} />;
336
+ };
337
+
338
+ await renderWithAlepha(<Form />);
339
+
340
+ // TagsInput should be rendered - check by label
341
+ expect(screen.getByText("Tags")).toBeDefined();
342
+ });
343
+
344
+ it("should have default array values in form", async ({ expect }) => {
345
+ const Form = () => {
346
+ const form = useForm({
347
+ id: "tags-default-test",
348
+ schema: t.object({
349
+ tags: t.array(t.text(), { default: ["tag1", "tag2"] }),
350
+ }),
351
+ handler: () => {},
352
+ });
353
+
354
+ return <TypeForm form={form} />;
355
+ };
356
+
357
+ await renderWithAlepha(<Form />);
358
+
359
+ // Check that default tags are rendered as pills
360
+ expect(screen.getByText("tag1")).toBeDefined();
361
+ expect(screen.getByText("tag2")).toBeDefined();
362
+ });
363
+ });
364
+
365
+ describe("Array of Enums (MultiSelect)", () => {
366
+ it("should render multiselect for array of enums", async ({ expect }) => {
367
+ const Form = () => {
368
+ const form = useForm({
369
+ id: "multi-enum-test",
370
+ schema: t.object({
371
+ roles: t.array(t.enum(["admin", "user", "guest"])),
372
+ }),
373
+ handler: () => {},
374
+ });
375
+
376
+ return <TypeForm form={form} />;
377
+ };
378
+
379
+ await renderWithAlepha(<Form />);
380
+
381
+ // MultiSelect should be rendered - check by label
382
+ expect(screen.getByText("Roles")).toBeDefined();
383
+ });
384
+ });
385
+
386
+ describe("Array of Objects (ControlArray)", () => {
387
+ it("should render array of objects with add button", async ({ expect }) => {
388
+ const Form = () => {
389
+ const form = useForm({
390
+ id: "array-obj-test",
391
+ schema: t.object({
392
+ contacts: t.array(
393
+ t.object({
394
+ name: t.text(),
395
+ email: t.text(),
396
+ }),
397
+ ),
398
+ }),
399
+ handler: () => {},
400
+ });
401
+
402
+ return <TypeForm form={form} />;
403
+ };
404
+
405
+ await renderWithAlepha(<Form />);
406
+
407
+ // ControlArray should render with an Add button
408
+ const addButton = screen.getByText("Add");
409
+ expect(addButton).toBeDefined();
410
+
411
+ // Initially no textboxes in the array section
412
+ const initialTextboxes = screen.queryAllByRole("textbox");
413
+ const initialCount = initialTextboxes.length;
414
+
415
+ // Click add to create a new item
416
+ fireEvent.click(addButton);
417
+
418
+ // After clicking add, new textboxes should appear for name and email fields
419
+ await waitFor(() => {
420
+ const newTextboxes = screen.queryAllByRole("textbox");
421
+ expect(newTextboxes.length).toBeGreaterThan(initialCount);
422
+ });
423
+ });
424
+ });
425
+
426
+ describe("Multiple Field Types Combined", () => {
427
+ it("should handle form with all field types", async ({ expect }) => {
428
+ const calls: Array<any> = [];
429
+
430
+ const Form = () => {
431
+ const form = useForm({
432
+ id: "combined-test",
433
+ schema: t.object({
434
+ name: t.text(),
435
+ age: t.integer(),
436
+ active: t.boolean(),
437
+ role: t.enum(["admin", "user"]),
438
+ }),
439
+ handler: (values) => {
440
+ calls.push(values);
441
+ },
442
+ });
443
+
444
+ return <TypeForm form={form} />;
445
+ };
446
+
447
+ await renderWithAlepha(<Form />);
448
+
449
+ // All field types should render
450
+ // Text input
451
+ const nameInput = screen.getByTestId("combined-test-name");
452
+ expect(nameInput).toBeDefined();
453
+
454
+ // Number input
455
+ const ageInput = screen.getByTestId("combined-test-age");
456
+ expect(ageInput).toBeDefined();
457
+
458
+ // Boolean switch
459
+ const activeSwitch = screen.getByRole("switch");
460
+ expect(activeSwitch).toBeDefined();
461
+
462
+ // Enum select (Mantine uses textbox with aria-haspopup)
463
+ const roleSelect = screen.getByRole("textbox", { name: "Role" });
464
+ expect(roleSelect).toBeDefined();
465
+
466
+ // Verify we can interact with fields
467
+ fireEvent.change(nameInput, { target: { value: "Alice" } });
468
+ expect((nameInput as HTMLInputElement).value).toBe("Alice");
469
+
470
+ fireEvent.change(ageInput, { target: { value: "30" } });
471
+
472
+ fireEvent.click(activeSwitch);
473
+ expect((activeSwitch as HTMLInputElement).checked).toBe(true);
474
+ });
475
+ });
476
+
477
+ describe("Form Reset", () => {
478
+ it("should reset all fields to default values", async ({ expect }) => {
479
+ const Form = () => {
480
+ const form = useForm({
481
+ id: "reset-test",
482
+ schema: t.object({
483
+ name: t.text({ default: "default name" }),
484
+ active: t.boolean({ default: false }),
485
+ }),
486
+ handler: () => {},
487
+ });
488
+
489
+ return <TypeForm form={form} />;
490
+ };
491
+
492
+ await renderWithAlepha(<Form />);
493
+
494
+ // Change values
495
+ const nameInput = screen.getByTestId("reset-test-name");
496
+ fireEvent.change(nameInput, { target: { value: "changed" } });
497
+
498
+ const switchInput = screen.getByRole("switch");
499
+ fireEvent.click(switchInput);
500
+
501
+ // Click reset
502
+ fireEvent.click(screen.getByText("Reset"));
503
+
504
+ // Values should be reset
505
+ await waitFor(() => {
506
+ expect((nameInput as HTMLInputElement).value).toBe("default name");
507
+ expect((switchInput as HTMLInputElement).checked).toBe(false);
508
+ });
509
+ });
510
+ });
511
+
512
+ describe("TypeForm Props", () => {
513
+ it("should respect skipSubmitButton prop", async ({ expect }) => {
514
+ const Form = () => {
515
+ const form = useForm({
516
+ id: "no-submit-test",
517
+ schema: t.object({
518
+ name: t.text(),
519
+ }),
520
+ handler: () => {},
521
+ });
522
+
523
+ return <TypeForm form={form} skipSubmitButton />;
524
+ };
525
+
526
+ await renderWithAlepha(<Form />);
527
+
528
+ expect(screen.queryByText("Submit")).toBeNull();
529
+ });
530
+
531
+ it("should apply controlProps to all fields", async ({ expect }) => {
532
+ const Form = () => {
533
+ const form = useForm({
534
+ id: "control-props-test",
535
+ schema: t.object({
536
+ field1: t.text(),
537
+ field2: t.text(),
538
+ }),
539
+ handler: () => {},
540
+ });
541
+
542
+ return (
543
+ <TypeForm
544
+ form={form}
545
+ controlProps={{ title: "Custom Title" }}
546
+ skipSubmitButton
547
+ />
548
+ );
549
+ };
550
+
551
+ await renderWithAlepha(<Form />);
552
+
553
+ // Both fields should have the custom title
554
+ const titles = screen.getAllByText("Custom Title");
555
+ expect(titles.length).toBe(2);
556
+ });
557
+
558
+ it("should apply fieldControlProps to specific fields", async ({
559
+ expect,
560
+ }) => {
561
+ const Form = () => {
562
+ const form = useForm({
563
+ id: "field-control-props-test",
564
+ schema: t.object({
565
+ field1: t.text(),
566
+ field2: t.text(),
567
+ }),
568
+ handler: () => {},
569
+ });
570
+
571
+ return (
572
+ <TypeForm
573
+ form={form}
574
+ fieldControlProps={{
575
+ field1: { title: "Field One" },
576
+ field2: { title: "Field Two" },
577
+ }}
578
+ skipSubmitButton
579
+ />
580
+ );
581
+ };
582
+
583
+ await renderWithAlepha(<Form />);
584
+
585
+ expect(screen.getByText("Field One")).toBeDefined();
586
+ expect(screen.getByText("Field Two")).toBeDefined();
587
+ });
588
+
589
+ it("should use custom submit button text", async ({ expect }) => {
590
+ const Form = () => {
591
+ const form = useForm({
592
+ id: "custom-submit-test",
593
+ schema: t.object({
594
+ name: t.text(),
595
+ }),
596
+ handler: () => {},
597
+ });
598
+
599
+ return (
600
+ <TypeForm
601
+ form={form}
602
+ submitButtonProps={{ children: "Save Changes" }}
603
+ />
604
+ );
605
+ };
606
+
607
+ await renderWithAlepha(<Form />);
608
+
609
+ expect(screen.getByText("Save Changes")).toBeDefined();
610
+ });
611
+
612
+ it("should render children function instead of auto fields", async ({
613
+ expect,
614
+ }) => {
615
+ const Form = () => {
616
+ const form = useForm({
617
+ id: "children-test",
618
+ schema: t.object({
619
+ name: t.text(),
620
+ email: t.text(),
621
+ }),
622
+ handler: () => {},
623
+ });
624
+
625
+ return (
626
+ <TypeForm form={form} skipSubmitButton>
627
+ {(input) => (
628
+ <div>
629
+ <input {...input.name.props} data-testid="custom-name" />
630
+ <span>Custom Layout</span>
631
+ </div>
632
+ )}
633
+ </TypeForm>
634
+ );
635
+ };
636
+
637
+ await renderWithAlepha(<Form />);
638
+
639
+ expect(screen.getByTestId("custom-name")).toBeDefined();
640
+ expect(screen.getByText("Custom Layout")).toBeDefined();
641
+ // Auto-generated fields should not exist
642
+ expect(screen.queryByTestId("children-test-email")).toBeNull();
643
+ });
644
+ });
645
+
646
+ describe("Required Fields", () => {
647
+ it("should mark required fields", async ({ expect }) => {
648
+ const Form = () => {
649
+ const form = useForm({
650
+ id: "required-test",
651
+ schema: t.object(
652
+ {
653
+ requiredField: t.text(),
654
+ optionalField: t.optional(t.text()),
655
+ },
656
+ { required: ["requiredField"] },
657
+ ),
658
+ handler: () => {},
659
+ });
660
+
661
+ return <TypeForm form={form} skipSubmitButton />;
662
+ };
663
+
664
+ await renderWithAlepha(<Form />);
665
+
666
+ // Required indicator should be present
667
+ // This depends on how Mantine renders required fields
668
+ const requiredInput = screen.getByTestId("required-test-requiredField");
669
+ expect(requiredInput).toBeDefined();
670
+ });
671
+ });
672
+
673
+ describe("Columns Configuration", () => {
674
+ it("should accept numeric columns prop", async ({ expect }) => {
675
+ const Form = () => {
676
+ const form = useForm({
677
+ id: "columns-test",
678
+ schema: t.object({
679
+ field1: t.text(),
680
+ field2: t.text(),
681
+ field3: t.text(),
682
+ }),
683
+ handler: () => {},
684
+ });
685
+
686
+ return <TypeForm form={form} columns={3} skipSubmitButton />;
687
+ };
688
+
689
+ await renderWithAlepha(<Form />);
690
+
691
+ // All fields should render
692
+ expect(screen.getByTestId("columns-test-field1")).toBeDefined();
693
+ expect(screen.getByTestId("columns-test-field2")).toBeDefined();
694
+ expect(screen.getByTestId("columns-test-field3")).toBeDefined();
695
+ });
696
+
697
+ it("should accept responsive columns config", async ({ expect }) => {
698
+ const Form = () => {
699
+ const form = useForm({
700
+ id: "responsive-columns-test",
701
+ schema: t.object({
702
+ field1: t.text(),
703
+ field2: t.text(),
704
+ }),
705
+ handler: () => {},
706
+ });
707
+
708
+ return (
709
+ <TypeForm
710
+ form={form}
711
+ columns={{ xs: 1, sm: 2, lg: 3 }}
712
+ skipSubmitButton
713
+ />
714
+ );
715
+ };
716
+
717
+ await renderWithAlepha(<Form />);
718
+
719
+ expect(
720
+ screen.getByTestId("responsive-columns-test-field1"),
721
+ ).toBeDefined();
722
+ expect(
723
+ screen.getByTestId("responsive-columns-test-field2"),
724
+ ).toBeDefined();
725
+ });
726
+ });
727
+ });