@alepha/ui 0.16.0 → 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.
- package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CoTOTfgU.js} +4 -3
- package/dist/admin/{AdminApiKeys-GMORg-1l.js.map → AdminApiKeys-CoTOTfgU.js.map} +1 -1
- package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BmsxFbDa.js} +4 -3
- package/dist/admin/{AdminAudits-pkWrjq1Z.js.map → AdminAudits-BmsxFbDa.js.map} +1 -1
- package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-BBB8knca.js} +4 -3
- package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-BBB8knca.js.map} +1 -1
- package/dist/admin/{AdminJobs-B-q9iGO3.js → AdminJobs-C604joTz.js} +4 -3
- package/dist/admin/{AdminJobs-B-q9iGO3.js.map → AdminJobs-C604joTz.js.map} +1 -1
- package/dist/admin/{AdminLayout-D8yZ-8lG.js → AdminLayout-CsjvpeD1.js} +6 -10
- package/dist/admin/AdminLayout-CsjvpeD1.js.map +1 -0
- package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-LwR6RKrx.js} +4 -3
- package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-LwR6RKrx.js.map} +1 -1
- package/dist/admin/AdminParameters-B_83Vie9.js +767 -0
- package/dist/admin/AdminParameters-B_83Vie9.js.map +1 -0
- package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-CWnPosdd.js} +4 -3
- package/dist/admin/{AdminSessions-DzIOxM3b.js.map → AdminSessions-CWnPosdd.js.map} +1 -1
- package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-nHv636E_.js} +4 -3
- package/dist/admin/{AdminUserAudits-CiUPN2BC.js.map → AdminUserAudits-nHv636E_.js.map} +1 -1
- package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CjYD3Kjc.js} +4 -3
- package/dist/admin/{AdminUserCreate-BwQKr4xE.js.map → AdminUserCreate-CjYD3Kjc.js.map} +1 -1
- package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-Ccq-LsZ0.js} +4 -3
- package/dist/admin/{AdminUserDetails-uqtC5aJ1.js.map → AdminUserDetails-Ccq-LsZ0.js.map} +1 -1
- package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-7s41DiF_.js} +6 -7
- package/dist/admin/AdminUserLayout-7s41DiF_.js.map +1 -0
- package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Ds3ODq_d.js} +4 -3
- package/dist/admin/{AdminUserSessions-DAE8Nf1F.js.map → AdminUserSessions-Ds3ODq_d.js.map} +1 -1
- package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-CGh4gROo.js} +4 -3
- package/dist/admin/{AdminUserSettings-EbahaV2a.js.map → AdminUserSettings-CGh4gROo.js.map} +1 -1
- package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-CvPiBzQK.js} +4 -3
- package/dist/admin/{AdminUsers-Dcjh0KNW.js.map → AdminUsers-CvPiBzQK.js.map} +1 -1
- package/dist/admin/index.d.ts +22 -10
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +47 -48
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/rolldown-runtime-CjeV3_4I.js +18 -0
- package/dist/auth/{AuthLayout-mFOWbiSP.js → AuthLayout-CdJcrPs4.js} +2 -4
- package/dist/auth/AuthLayout-CdJcrPs4.js.map +1 -0
- package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
- package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
- package/dist/auth/{Login-BBqTosqZ.js → Login-DS_OqA0G.js} +7 -6
- package/dist/auth/Login-DS_OqA0G.js.map +1 -0
- package/dist/auth/{Profile-Bxj8Nwom.js → Profile-Di7N7HZL.js} +2 -3
- package/dist/auth/{Profile-Bxj8Nwom.js.map → Profile-Di7N7HZL.js.map} +1 -1
- package/dist/auth/{Register-Ce675Crg.js → Register-BRR2_gux.js} +7 -6
- package/dist/auth/Register-BRR2_gux.js.map +1 -0
- package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-oQu72lod.js} +4 -3
- package/dist/auth/{ResetPassword-DWdt7c40.js.map → ResetPassword-oQu72lod.js.map} +1 -1
- package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DC6HPZjd.js} +4 -3
- package/dist/auth/{VerifyEmail-CI4JwByV.js.map → VerifyEmail-DC6HPZjd.js.map} +1 -1
- package/dist/auth/index.d.ts +14 -14
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +13 -17
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
- package/dist/core/index.d.ts +147 -68
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +349 -287
- package/dist/core/index.js.map +1 -1
- package/dist/demo/{DemoDataTable-CguplbR7.js → DemoDataTable-DCsJq8v5.js} +4 -5
- package/dist/demo/DemoDataTable-DCsJq8v5.js.map +1 -0
- package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-DpRrPlBC.js} +4 -3
- package/dist/demo/{DemoHome-Cce2bWmg.js.map → DemoHome-DpRrPlBC.js.map} +1 -1
- package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-zeucGKHV.js} +6 -5
- package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +1 -0
- package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-PhgbAAiQ.js} +6 -5
- package/dist/demo/DemoLayout-PhgbAAiQ.js.map +1 -0
- package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DSzP0Lkv.js} +8 -10
- package/dist/demo/DemoLogin-DSzP0Lkv.js.map +1 -0
- package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DavFBsCz.js} +8 -10
- package/dist/demo/DemoRegister-DavFBsCz.js.map +1 -0
- package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-BS2rIAQK.js} +5 -7
- package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +1 -0
- package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-zNkUmHRl.js} +4 -5
- package/dist/demo/DemoSidebar-zNkUmHRl.js.map +1 -0
- package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-B9q7oT0b.js} +4 -5
- package/dist/demo/DemoTypeForm-B9q7oT0b.js.map +1 -0
- package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-Bi4SdWz0.js} +5 -7
- package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +1 -0
- package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-CTeZyrek.js} +2 -4
- package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-CTeZyrek.js.map} +1 -1
- package/dist/demo/{Showcase-CQrMWars.js → Showcase-C9btr_SJ.js} +3 -5
- package/dist/demo/Showcase-C9btr_SJ.js.map +1 -0
- package/dist/demo/index.d.ts +2 -2
- package/dist/demo/index.d.ts.map +1 -1
- package/dist/demo/index.js +15 -15
- package/dist/demo/rolldown-runtime-CjeV3_4I.js +18 -0
- package/package.json +5 -3
- package/src/admin/AdminRouter.ts +15 -24
- package/src/admin/components/AdminLayout.tsx +6 -10
- package/src/admin/components/parameters/AdminParameters.tsx +154 -76
- package/src/admin/components/parameters/ParameterDetails.tsx +153 -93
- package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
- package/src/admin/components/parameters/ParameterHistory.tsx +15 -20
- package/src/admin/components/parameters/ParameterTree.tsx +280 -104
- package/src/admin/components/parameters/types.ts +3 -3
- package/src/admin/primitives/$uiAdmin.ts +2 -2
- package/src/auth/AuthRouter.ts +1 -4
- package/src/auth/components/AuthLayout.tsx +0 -1
- package/src/core/components/buttons/ActionButton.tsx +4 -15
- package/src/core/components/buttons/DarkModeButton.tsx +8 -4
- package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
- package/src/core/components/form/Control.tsx +10 -32
- package/src/core/components/form/ControlArray.tsx +200 -89
- package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
- package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
- package/src/core/components/layout/Breadcrumb.tsx +91 -0
- package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
- package/src/core/components/layout/Sidebar.tsx +58 -18
- package/src/core/constants/ui.ts +1 -1
- package/src/core/helpers/renderIcon.tsx +5 -2
- package/src/core/index.ts +9 -5
- package/src/core/styles.css +7 -6
- package/src/core/utils/string.ts +28 -4
- package/src/demo/components/DemoLayout.tsx +6 -2
- package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
- package/dist/admin/AdminAudits-8SM96viT.js +0 -3
- package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
- package/dist/admin/AdminJobs-CED1syCn.js +0 -3
- package/dist/admin/AdminLayout-D8yZ-8lG.js.map +0 -1
- package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
- package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
- package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
- package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
- package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
- package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
- package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
- package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
- package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
- package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
- package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
- package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
- package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
- package/dist/auth/AuthLayout-mFOWbiSP.js.map +0 -1
- package/dist/auth/Login-BBqTosqZ.js.map +0 -1
- package/dist/auth/Login-CoU63mMR.js +0 -4
- package/dist/auth/Register-BV_oa_AK.js +0 -4
- package/dist/auth/Register-Ce675Crg.js.map +0 -1
- package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
- package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
- package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
- package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
- package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
- package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
- package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
- package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
- package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
- package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
- package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
- package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
- package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
- package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
- 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
|
+
});
|