@alepha/react 0.14.2 → 0.14.4
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/auth/index.browser.js +29 -14
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.js +960 -195
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -4
- package/dist/core/index.js.map +1 -1
- package/dist/head/index.browser.js +59 -19
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +99 -560
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +92 -87
- package/dist/head/index.js.map +1 -1
- package/dist/router/index.browser.js +30 -15
- package/dist/router/index.browser.js.map +1 -1
- package/dist/router/index.d.ts +616 -192
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +961 -196
- package/dist/router/index.js.map +1 -1
- package/package.json +4 -4
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/head/helpers/SeoExpander.spec.ts +203 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +11 -28
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
- package/src/head/providers/BrowserHeadProvider.ts +25 -19
- package/src/head/providers/HeadProvider.ts +76 -10
- package/src/head/providers/ServerHeadProvider.ts +22 -138
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
- package/src/router/__tests__/page-head.spec.ts +44 -0
- package/src/router/__tests__/seo-head.spec.ts +121 -0
- package/src/router/atoms/ssrManifestAtom.ts +60 -0
- package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
- package/src/router/errors/Redirection.ts +1 -1
- package/src/router/index.shared.ts +1 -0
- package/src/router/index.ts +16 -2
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/router/primitives/$page.ts +46 -10
- package/src/router/providers/ReactBrowserProvider.ts +14 -29
- package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
- package/src/router/providers/ReactPageProvider.ts +11 -4
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/router/providers/ReactServerProvider.ts +331 -315
- package/src/router/providers/ReactServerTemplateProvider.ts +775 -0
- package/src/router/providers/SSRManifestProvider.ts +365 -0
- package/src/router/services/ReactPageServerService.ts +5 -3
- package/src/router/services/ReactRouter.ts +3 -3
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { AlephaReact } from "@alepha/react";
|
|
2
|
+
import { $page, NestedView, Redirection, ReactRouter } from "../index.browser.ts";
|
|
3
|
+
import { waitFor } from "@testing-library/dom";
|
|
4
|
+
import { Alepha, t } from "alepha";
|
|
5
|
+
import { act } from "react";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
describe("$page browser tests", () => {
|
|
9
|
+
let alepha: Alepha;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Reset document state
|
|
13
|
+
document.title = "";
|
|
14
|
+
document.head.innerHTML = "";
|
|
15
|
+
document.body.innerHTML = '<div id="root"></div>';
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should render home page via router.go", async () => {
|
|
19
|
+
class App {
|
|
20
|
+
home = $page({
|
|
21
|
+
path: "/",
|
|
22
|
+
component: () => <div data-testid="home">Welcome Home</div>,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
27
|
+
await alepha.start();
|
|
28
|
+
|
|
29
|
+
const router = alepha.inject(ReactRouter);
|
|
30
|
+
|
|
31
|
+
await act(async () => {
|
|
32
|
+
await router.go("/");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await waitFor(() => {
|
|
36
|
+
const element = document.querySelector('[data-testid="home"]');
|
|
37
|
+
expect(element).toBeDefined();
|
|
38
|
+
expect(element?.textContent).toBe("Welcome Home");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should navigate between pages", async () => {
|
|
43
|
+
class App {
|
|
44
|
+
home = $page({
|
|
45
|
+
path: "/",
|
|
46
|
+
component: () => <div data-testid="home">Home</div>,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
about = $page({
|
|
50
|
+
path: "/about",
|
|
51
|
+
component: () => <div data-testid="about">About Us</div>,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
56
|
+
await alepha.start();
|
|
57
|
+
|
|
58
|
+
const router = alepha.inject(ReactRouter);
|
|
59
|
+
|
|
60
|
+
// Navigate to home
|
|
61
|
+
await act(async () => {
|
|
62
|
+
await router.go("/");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
expect(document.querySelector('[data-testid="home"]')).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Navigate to about
|
|
70
|
+
await act(async () => {
|
|
71
|
+
await router.go("/about");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(document.querySelector('[data-testid="about"]')).toBeDefined();
|
|
76
|
+
expect(document.querySelector('[data-testid="about"]')?.textContent).toBe(
|
|
77
|
+
"About Us",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("resolve function", () => {
|
|
83
|
+
it("should pass resolved data to component", async () => {
|
|
84
|
+
class App {
|
|
85
|
+
user = $page({
|
|
86
|
+
path: "/user/:id",
|
|
87
|
+
schema: {
|
|
88
|
+
params: t.object({
|
|
89
|
+
id: t.text(),
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
loader: ({ params }) => ({
|
|
93
|
+
userId: params.id,
|
|
94
|
+
userName: `User ${params.id}`,
|
|
95
|
+
}),
|
|
96
|
+
component: ({
|
|
97
|
+
userId,
|
|
98
|
+
userName,
|
|
99
|
+
}: {
|
|
100
|
+
userId: string;
|
|
101
|
+
userName: string;
|
|
102
|
+
}) => (
|
|
103
|
+
<div data-testid="user">
|
|
104
|
+
<span data-testid="id">{userId}</span>
|
|
105
|
+
<span data-testid="name">{userName}</span>
|
|
106
|
+
</div>
|
|
107
|
+
),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
112
|
+
await alepha.start();
|
|
113
|
+
|
|
114
|
+
const router = alepha.inject(ReactRouter);
|
|
115
|
+
|
|
116
|
+
await act(async () => {
|
|
117
|
+
await router.go("/user/123");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(document.querySelector('[data-testid="id"]')?.textContent).toBe(
|
|
122
|
+
"123",
|
|
123
|
+
);
|
|
124
|
+
expect(
|
|
125
|
+
document.querySelector('[data-testid="name"]')?.textContent,
|
|
126
|
+
).toBe("User 123");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it.skip("should handle async resolve function", async () => {
|
|
131
|
+
// Skipped: Timing issues with multiple rapid navigations in jsdom
|
|
132
|
+
class App {
|
|
133
|
+
async = $page({
|
|
134
|
+
path: "/async",
|
|
135
|
+
loader: async () => {
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
137
|
+
return { message: "Loaded async data" };
|
|
138
|
+
},
|
|
139
|
+
component: ({ message }: { message: string }) => (
|
|
140
|
+
<div data-testid="async">{message}</div>
|
|
141
|
+
),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
146
|
+
await alepha.start();
|
|
147
|
+
|
|
148
|
+
const router = alepha.inject(ReactRouter);
|
|
149
|
+
|
|
150
|
+
await act(async () => {
|
|
151
|
+
await router.go("/async");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await waitFor(
|
|
155
|
+
() => {
|
|
156
|
+
expect(
|
|
157
|
+
document.querySelector('[data-testid="async"]')?.textContent,
|
|
158
|
+
).toBe("Loaded async data");
|
|
159
|
+
},
|
|
160
|
+
{ timeout: 3000 },
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("errorHandler", () => {
|
|
166
|
+
it("should handle errors and redirect", async () => {
|
|
167
|
+
let isAuthenticated = false;
|
|
168
|
+
|
|
169
|
+
class App {
|
|
170
|
+
login = $page({
|
|
171
|
+
path: "/login",
|
|
172
|
+
component: () => <div data-testid="login">Login Page</div>,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
protected = $page({
|
|
176
|
+
path: "/protected",
|
|
177
|
+
loader: () => {
|
|
178
|
+
if (!isAuthenticated) {
|
|
179
|
+
throw new Error("Unauthorized");
|
|
180
|
+
}
|
|
181
|
+
return { data: "secret" };
|
|
182
|
+
},
|
|
183
|
+
errorHandler: (error) => {
|
|
184
|
+
if (error.message === "Unauthorized") {
|
|
185
|
+
return new Redirection("/login");
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
},
|
|
189
|
+
component: ({ data }: { data: string }) => (
|
|
190
|
+
<div data-testid="protected">Data: {data}</div>
|
|
191
|
+
),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
196
|
+
await alepha.start();
|
|
197
|
+
|
|
198
|
+
const router = alepha.inject(ReactRouter);
|
|
199
|
+
|
|
200
|
+
// Try to access protected - should redirect to login
|
|
201
|
+
await act(async () => {
|
|
202
|
+
await router.go("/protected");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(document.querySelector('[data-testid="login"]')).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Now authenticate
|
|
210
|
+
isAuthenticated = true;
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
await router.go("/protected");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await waitFor(() => {
|
|
217
|
+
expect(
|
|
218
|
+
document.querySelector('[data-testid="protected"]'),
|
|
219
|
+
).toBeDefined();
|
|
220
|
+
expect(
|
|
221
|
+
document.querySelector('[data-testid="protected"]')?.textContent,
|
|
222
|
+
).toBe("Data: secret");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should render error handler ReactNode", async () => {
|
|
227
|
+
class App {
|
|
228
|
+
errorPage = $page({
|
|
229
|
+
path: "/error",
|
|
230
|
+
loader: () => {
|
|
231
|
+
throw new Error("Something went wrong");
|
|
232
|
+
},
|
|
233
|
+
errorHandler: (error) => (
|
|
234
|
+
<div data-testid="error">Error: {error.message}</div>
|
|
235
|
+
),
|
|
236
|
+
component: () => <div>Should not render</div>,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
241
|
+
await alepha.start();
|
|
242
|
+
|
|
243
|
+
const router = alepha.inject(ReactRouter);
|
|
244
|
+
|
|
245
|
+
await act(async () => {
|
|
246
|
+
await router.go("/error");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(
|
|
251
|
+
document.querySelector('[data-testid="error"]')?.textContent,
|
|
252
|
+
).toBe("Error: Something went wrong");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("parent-child hierarchy", () => {
|
|
258
|
+
it("should render parent layout with nested child", async () => {
|
|
259
|
+
class App {
|
|
260
|
+
layout = $page({
|
|
261
|
+
path: "/",
|
|
262
|
+
loader: () => ({ appName: "My App" }),
|
|
263
|
+
component: ({ appName }: { appName: string }) => (
|
|
264
|
+
<div data-testid="layout">
|
|
265
|
+
<header data-testid="header">{appName}</header>
|
|
266
|
+
<NestedView />
|
|
267
|
+
</div>
|
|
268
|
+
),
|
|
269
|
+
children: [],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
home = $page({
|
|
273
|
+
path: "/",
|
|
274
|
+
parent: this.layout,
|
|
275
|
+
component: () => <div data-testid="home">Home Content</div>,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
about = $page({
|
|
279
|
+
path: "/about",
|
|
280
|
+
parent: this.layout,
|
|
281
|
+
component: () => <div data-testid="about">About Content</div>,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
286
|
+
await alepha.start();
|
|
287
|
+
|
|
288
|
+
const router = alepha.inject(ReactRouter);
|
|
289
|
+
|
|
290
|
+
// Navigate to home - should show layout and home
|
|
291
|
+
await act(async () => {
|
|
292
|
+
await router.go("/");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await waitFor(() => {
|
|
296
|
+
expect(
|
|
297
|
+
document.querySelector('[data-testid="header"]')?.textContent,
|
|
298
|
+
).toBe("My App");
|
|
299
|
+
expect(document.querySelector('[data-testid="home"]')).toBeDefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Navigate to about - layout should persist
|
|
303
|
+
await act(async () => {
|
|
304
|
+
await router.go("/about");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(
|
|
309
|
+
document.querySelector('[data-testid="header"]')?.textContent,
|
|
310
|
+
).toBe("My App");
|
|
311
|
+
expect(document.querySelector('[data-testid="about"]')).toBeDefined();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should pass parent resolved data to children", async () => {
|
|
316
|
+
class App {
|
|
317
|
+
layout = $page({
|
|
318
|
+
path: "/",
|
|
319
|
+
loader: () => ({ theme: "dark" }),
|
|
320
|
+
component: ({ theme }: { theme: string }) => (
|
|
321
|
+
<div data-testid="layout" data-theme={theme}>
|
|
322
|
+
<NestedView />
|
|
323
|
+
</div>
|
|
324
|
+
),
|
|
325
|
+
children: [],
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
page = $page({
|
|
329
|
+
path: "/page",
|
|
330
|
+
parent: this.layout,
|
|
331
|
+
loader: ({ theme }) => ({
|
|
332
|
+
message: `Theme is ${theme}`,
|
|
333
|
+
}),
|
|
334
|
+
component: ({ message }: { message: string }) => (
|
|
335
|
+
<div data-testid="page">{message}</div>
|
|
336
|
+
),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
341
|
+
await alepha.start();
|
|
342
|
+
|
|
343
|
+
const router = alepha.inject(ReactRouter);
|
|
344
|
+
|
|
345
|
+
await act(async () => {
|
|
346
|
+
await router.go("/page");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await waitFor(() => {
|
|
350
|
+
expect(
|
|
351
|
+
document
|
|
352
|
+
.querySelector('[data-testid="layout"]')
|
|
353
|
+
?.getAttribute("data-theme"),
|
|
354
|
+
).toBe("dark");
|
|
355
|
+
expect(
|
|
356
|
+
document.querySelector('[data-testid="page"]')?.textContent,
|
|
357
|
+
).toBe("Theme is dark");
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should handle multi-level nesting", async () => {
|
|
362
|
+
class App {
|
|
363
|
+
root = $page({
|
|
364
|
+
path: "/",
|
|
365
|
+
loader: () => ({ level: "root" }),
|
|
366
|
+
component: ({ level }: { level: string }) => (
|
|
367
|
+
<div data-testid="root">
|
|
368
|
+
{level}
|
|
369
|
+
<NestedView />
|
|
370
|
+
</div>
|
|
371
|
+
),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
section = $page({
|
|
375
|
+
path: "/section",
|
|
376
|
+
parent: this.root,
|
|
377
|
+
loader: ({ level }) => ({ level: `${level} > section` }),
|
|
378
|
+
component: ({ level }: { level: string }) => (
|
|
379
|
+
<div data-testid="section">
|
|
380
|
+
{level}
|
|
381
|
+
<NestedView />
|
|
382
|
+
</div>
|
|
383
|
+
),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
page = $page({
|
|
387
|
+
path: "/page",
|
|
388
|
+
parent: this.section,
|
|
389
|
+
loader: ({ level }) => ({ level: `${level} > page` }),
|
|
390
|
+
component: ({ level }: { level: string }) => (
|
|
391
|
+
<div data-testid="page">{level}</div>
|
|
392
|
+
),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
397
|
+
await alepha.start();
|
|
398
|
+
|
|
399
|
+
const router = alepha.inject(ReactRouter);
|
|
400
|
+
|
|
401
|
+
await act(async () => {
|
|
402
|
+
await router.go("/section/page");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await waitFor(() => {
|
|
406
|
+
expect(document.querySelector('[data-testid="root"]')).toBeDefined();
|
|
407
|
+
expect(document.querySelector('[data-testid="section"]')).toBeDefined();
|
|
408
|
+
expect(
|
|
409
|
+
document.querySelector('[data-testid="page"]')?.textContent,
|
|
410
|
+
).toBe("root > section > page");
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe("onLeave hook", () => {
|
|
416
|
+
it("should call onLeave when navigating away", async () => {
|
|
417
|
+
const onLeaveSpy = vi.fn();
|
|
418
|
+
|
|
419
|
+
class App {
|
|
420
|
+
home = $page({
|
|
421
|
+
path: "/",
|
|
422
|
+
onLeave: onLeaveSpy,
|
|
423
|
+
component: () => <div data-testid="home">Home</div>,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
about = $page({
|
|
427
|
+
path: "/about",
|
|
428
|
+
component: () => <div data-testid="about">About</div>,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
433
|
+
await alepha.start();
|
|
434
|
+
|
|
435
|
+
const router = alepha.inject(ReactRouter);
|
|
436
|
+
|
|
437
|
+
// Navigate to home
|
|
438
|
+
await act(async () => {
|
|
439
|
+
await router.go("/");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await waitFor(() => {
|
|
443
|
+
expect(document.querySelector('[data-testid="home"]')).toBeDefined();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(onLeaveSpy).not.toHaveBeenCalled();
|
|
447
|
+
|
|
448
|
+
// Navigate away from home to about
|
|
449
|
+
await act(async () => {
|
|
450
|
+
await router.go("/about");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
await waitFor(() => {
|
|
454
|
+
expect(document.querySelector('[data-testid="about"]')).toBeDefined();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// onLeave should have been called
|
|
458
|
+
expect(onLeaveSpy).toHaveBeenCalledTimes(1);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("schema validation", () => {
|
|
463
|
+
it("should validate and parse params", async () => {
|
|
464
|
+
class App {
|
|
465
|
+
user = $page({
|
|
466
|
+
path: "/user/:id",
|
|
467
|
+
schema: {
|
|
468
|
+
params: t.object({
|
|
469
|
+
id: t.text(),
|
|
470
|
+
}),
|
|
471
|
+
},
|
|
472
|
+
loader: ({ params }) => ({
|
|
473
|
+
userId: params.id,
|
|
474
|
+
}),
|
|
475
|
+
component: ({ userId }: { userId: string }) => (
|
|
476
|
+
<div data-testid="user">User: {userId}</div>
|
|
477
|
+
),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
482
|
+
await alepha.start();
|
|
483
|
+
|
|
484
|
+
const router = alepha.inject(ReactRouter);
|
|
485
|
+
|
|
486
|
+
await act(async () => {
|
|
487
|
+
await router.go("/user/abc123");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await waitFor(() => {
|
|
491
|
+
expect(
|
|
492
|
+
document.querySelector('[data-testid="user"]')?.textContent,
|
|
493
|
+
).toBe("User: abc123");
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should validate and parse query params", async () => {
|
|
498
|
+
class App {
|
|
499
|
+
search = $page({
|
|
500
|
+
path: "/search",
|
|
501
|
+
schema: {
|
|
502
|
+
query: t.object({
|
|
503
|
+
q: t.text({ default: "" }),
|
|
504
|
+
page: t.number({ default: 1 }),
|
|
505
|
+
}),
|
|
506
|
+
},
|
|
507
|
+
loader: ({ query }) => ({
|
|
508
|
+
searchQuery: query.q,
|
|
509
|
+
currentPage: query.page,
|
|
510
|
+
}),
|
|
511
|
+
component: ({
|
|
512
|
+
searchQuery,
|
|
513
|
+
currentPage,
|
|
514
|
+
}: {
|
|
515
|
+
searchQuery: string;
|
|
516
|
+
currentPage: number;
|
|
517
|
+
}) => (
|
|
518
|
+
<div data-testid="search">
|
|
519
|
+
<div data-testid="query">{searchQuery}</div>
|
|
520
|
+
<div data-testid="page">{currentPage}</div>
|
|
521
|
+
</div>
|
|
522
|
+
),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
527
|
+
await alepha.start();
|
|
528
|
+
|
|
529
|
+
const router = alepha.inject(ReactRouter);
|
|
530
|
+
|
|
531
|
+
await act(async () => {
|
|
532
|
+
await router.go("/search?q=typescript&page=2");
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
await waitFor(() => {
|
|
536
|
+
expect(
|
|
537
|
+
document.querySelector('[data-testid="query"]')?.textContent,
|
|
538
|
+
).toBe("typescript");
|
|
539
|
+
expect(
|
|
540
|
+
document.querySelector('[data-testid="page"]')?.textContent,
|
|
541
|
+
).toBe("2");
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it.skip("should update when navigating with different query params", async () => {
|
|
546
|
+
// Skipped: Multiple rapid navigations to same page in jsdom have timing issues
|
|
547
|
+
class App {
|
|
548
|
+
search = $page({
|
|
549
|
+
path: "/search",
|
|
550
|
+
schema: {
|
|
551
|
+
query: t.object({
|
|
552
|
+
q: t.text({ default: "" }),
|
|
553
|
+
page: t.number({ default: 1 }),
|
|
554
|
+
}),
|
|
555
|
+
},
|
|
556
|
+
loader: ({ query }) => ({
|
|
557
|
+
searchQuery: query.q,
|
|
558
|
+
currentPage: query.page,
|
|
559
|
+
}),
|
|
560
|
+
component: ({
|
|
561
|
+
searchQuery,
|
|
562
|
+
currentPage,
|
|
563
|
+
}: {
|
|
564
|
+
searchQuery: string;
|
|
565
|
+
currentPage: number;
|
|
566
|
+
}) => (
|
|
567
|
+
<div data-testid="search">
|
|
568
|
+
<div data-testid="query">{searchQuery}</div>
|
|
569
|
+
<div data-testid="page">{currentPage}</div>
|
|
570
|
+
</div>
|
|
571
|
+
),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
576
|
+
await alepha.start();
|
|
577
|
+
|
|
578
|
+
const router = alepha.inject(ReactRouter);
|
|
579
|
+
|
|
580
|
+
// Navigate with query params
|
|
581
|
+
await act(async () => {
|
|
582
|
+
await router.go("/search?q=alepha&page=5");
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
await waitFor(() => {
|
|
586
|
+
expect(
|
|
587
|
+
document.querySelector('[data-testid="query"]')?.textContent,
|
|
588
|
+
).toBe("alepha");
|
|
589
|
+
expect(
|
|
590
|
+
document.querySelector('[data-testid="page"]')?.textContent,
|
|
591
|
+
).toBe("5");
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("should validate params and query together", async () => {
|
|
596
|
+
class App {
|
|
597
|
+
userPosts = $page({
|
|
598
|
+
path: "/users/:userId/posts",
|
|
599
|
+
schema: {
|
|
600
|
+
params: t.object({
|
|
601
|
+
userId: t.text(),
|
|
602
|
+
}),
|
|
603
|
+
query: t.object({
|
|
604
|
+
sort: t.text({ default: "recent" }),
|
|
605
|
+
limit: t.number({ default: 10 }),
|
|
606
|
+
}),
|
|
607
|
+
},
|
|
608
|
+
loader: ({ params, query }) => ({
|
|
609
|
+
userId: params.userId,
|
|
610
|
+
sortBy: query.sort,
|
|
611
|
+
limit: query.limit,
|
|
612
|
+
}),
|
|
613
|
+
component: ({
|
|
614
|
+
userId,
|
|
615
|
+
sortBy,
|
|
616
|
+
limit,
|
|
617
|
+
}: {
|
|
618
|
+
userId: string;
|
|
619
|
+
sortBy: string;
|
|
620
|
+
limit: number;
|
|
621
|
+
}) => (
|
|
622
|
+
<div data-testid="posts">
|
|
623
|
+
<div data-testid="user">{userId}</div>
|
|
624
|
+
<div data-testid="sort">{sortBy}</div>
|
|
625
|
+
<div data-testid="limit">{limit}</div>
|
|
626
|
+
</div>
|
|
627
|
+
),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
632
|
+
await alepha.start();
|
|
633
|
+
|
|
634
|
+
const router = alepha.inject(ReactRouter);
|
|
635
|
+
|
|
636
|
+
await act(async () => {
|
|
637
|
+
await router.go("/users/john/posts?sort=popular&limit=20");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await waitFor(() => {
|
|
641
|
+
expect(
|
|
642
|
+
document.querySelector('[data-testid="user"]')?.textContent,
|
|
643
|
+
).toBe("john");
|
|
644
|
+
expect(
|
|
645
|
+
document.querySelector('[data-testid="sort"]')?.textContent,
|
|
646
|
+
).toBe("popular");
|
|
647
|
+
expect(
|
|
648
|
+
document.querySelector('[data-testid="limit"]')?.textContent,
|
|
649
|
+
).toBe("20");
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("should use default values for missing query params", async () => {
|
|
654
|
+
class App {
|
|
655
|
+
search = $page({
|
|
656
|
+
path: "/search",
|
|
657
|
+
schema: {
|
|
658
|
+
query: t.object({
|
|
659
|
+
q: t.text({ default: "default search" }),
|
|
660
|
+
page: t.number({ default: 1 }),
|
|
661
|
+
}),
|
|
662
|
+
},
|
|
663
|
+
loader: ({ query }) => ({
|
|
664
|
+
searchQuery: query.q,
|
|
665
|
+
currentPage: query.page,
|
|
666
|
+
}),
|
|
667
|
+
component: ({
|
|
668
|
+
searchQuery,
|
|
669
|
+
currentPage,
|
|
670
|
+
}: {
|
|
671
|
+
searchQuery: string;
|
|
672
|
+
currentPage: number;
|
|
673
|
+
}) => (
|
|
674
|
+
<div data-testid="search">
|
|
675
|
+
<div data-testid="query">{searchQuery}</div>
|
|
676
|
+
<div data-testid="page">{currentPage}</div>
|
|
677
|
+
</div>
|
|
678
|
+
),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
alepha = Alepha.create().with(AlephaReact).with(App);
|
|
683
|
+
await alepha.start();
|
|
684
|
+
|
|
685
|
+
const router = alepha.inject(ReactRouter);
|
|
686
|
+
|
|
687
|
+
// Navigate without query params - should use defaults
|
|
688
|
+
await act(async () => {
|
|
689
|
+
await router.go("/search");
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
await waitFor(() => {
|
|
693
|
+
expect(
|
|
694
|
+
document.querySelector('[data-testid="query"]')?.textContent,
|
|
695
|
+
).toBe("default search");
|
|
696
|
+
expect(
|
|
697
|
+
document.querySelector('[data-testid="page"]')?.textContent,
|
|
698
|
+
).toBe("1");
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
});
|