@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 { $page, PagePrimitive, Redirection, NestedView, type ReactRouterState } from "../index.ts";
|
|
2
|
+
import { Alepha, t } from "alepha";
|
|
3
|
+
import type { FC } from "react";
|
|
4
|
+
import { beforeEach, describe, test, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
describe("$page primitive tests", () => {
|
|
7
|
+
let alepha: Alepha;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
alepha = Alepha.create();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("$page - basic creation and configuration", async ({ expect }) => {
|
|
14
|
+
class App {
|
|
15
|
+
basic = $page({
|
|
16
|
+
name: "Basic Page",
|
|
17
|
+
path: "/basic",
|
|
18
|
+
component: () => "Basic content",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const app = alepha.inject(App);
|
|
23
|
+
await alepha.start();
|
|
24
|
+
|
|
25
|
+
expect(app.basic).toBeInstanceOf(PagePrimitive);
|
|
26
|
+
expect(app.basic.name).toBe("Basic Page");
|
|
27
|
+
expect(app.basic.options.path).toBe("/basic");
|
|
28
|
+
|
|
29
|
+
const rendered = await app.basic.render();
|
|
30
|
+
expect(rendered.html).toBe("Basic content");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("$page - name defaults to property key", async ({ expect }) => {
|
|
34
|
+
class App {
|
|
35
|
+
testPageName = $page({
|
|
36
|
+
component: () => "test content",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const app = alepha.inject(App);
|
|
41
|
+
await alepha.start();
|
|
42
|
+
|
|
43
|
+
expect(app.testPageName.name).toBe("testPageName");
|
|
44
|
+
|
|
45
|
+
const rendered = await app.testPageName.render();
|
|
46
|
+
expect(rendered.html).toBe("test content");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("$page - schema validation with params and query", async ({
|
|
50
|
+
expect,
|
|
51
|
+
}) => {
|
|
52
|
+
class App {
|
|
53
|
+
user = $page({
|
|
54
|
+
path: "/user/:id",
|
|
55
|
+
schema: {
|
|
56
|
+
params: t.object({
|
|
57
|
+
id: t.text(),
|
|
58
|
+
}),
|
|
59
|
+
query: t.object({
|
|
60
|
+
tab: t.text({ default: "profile" }),
|
|
61
|
+
sort: t.optional(t.text()),
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
loader: ({ params, query }) => ({ params, query }),
|
|
65
|
+
component: ({ params, query }) =>
|
|
66
|
+
`User ${params.id} - Tab: ${query.tab}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const app = alepha.inject(App);
|
|
71
|
+
await alepha.start();
|
|
72
|
+
|
|
73
|
+
expect(app.user.options.schema?.params).toBeDefined();
|
|
74
|
+
expect(app.user.options.schema?.query).toBeDefined();
|
|
75
|
+
expect(app.user.options.loader).toBeDefined();
|
|
76
|
+
expect(app.user.options.component).toBeDefined();
|
|
77
|
+
|
|
78
|
+
const rendered = await app.user.render({
|
|
79
|
+
params: { id: "123" },
|
|
80
|
+
query: { tab: "settings", sort: "name" },
|
|
81
|
+
});
|
|
82
|
+
expect(rendered.html).toBe("User 123 - Tab: settings");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("$page - lazy component configuration", async ({ expect }) => {
|
|
86
|
+
const LazyComponent: FC<{ message: string }> = ({ message }) =>
|
|
87
|
+
`Lazy: ${message}`;
|
|
88
|
+
|
|
89
|
+
class App {
|
|
90
|
+
lazy = $page({
|
|
91
|
+
path: "/lazy",
|
|
92
|
+
loader: () => ({ message: "loaded" }),
|
|
93
|
+
lazy: async () => ({ default: LazyComponent }),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const app = alepha.inject(App);
|
|
98
|
+
await alepha.start();
|
|
99
|
+
|
|
100
|
+
expect(app.lazy.options.lazy).toBeDefined();
|
|
101
|
+
expect(typeof app.lazy.options.lazy).toBe("function");
|
|
102
|
+
|
|
103
|
+
const lazyModule = await app.lazy.options.lazy!();
|
|
104
|
+
expect(lazyModule.default).toBe(LazyComponent);
|
|
105
|
+
|
|
106
|
+
const rendered = await app.lazy.render();
|
|
107
|
+
expect(rendered.html).toBe("Lazy: loaded");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("$page - lazy takes precedence over component when both defined", async ({
|
|
111
|
+
expect,
|
|
112
|
+
}) => {
|
|
113
|
+
const Component: FC = () => "Component";
|
|
114
|
+
const LazyComponent: FC = () => "Lazy Component";
|
|
115
|
+
|
|
116
|
+
class App {
|
|
117
|
+
both = $page({
|
|
118
|
+
component: Component,
|
|
119
|
+
lazy: async () => ({ default: LazyComponent }),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const app = alepha.inject(App);
|
|
124
|
+
await alepha.start();
|
|
125
|
+
|
|
126
|
+
expect(app.both.options.component).toBe(Component);
|
|
127
|
+
expect(app.both.options.lazy).toBeDefined();
|
|
128
|
+
|
|
129
|
+
const rendered = await app.both.render();
|
|
130
|
+
expect(rendered.html).toBe("Lazy Component");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("$page - resolve function with async data loading", async ({
|
|
134
|
+
expect,
|
|
135
|
+
}) => {
|
|
136
|
+
class App {
|
|
137
|
+
async = $page({
|
|
138
|
+
path: "/async/:id",
|
|
139
|
+
schema: {
|
|
140
|
+
params: t.object({
|
|
141
|
+
id: t.text(),
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
loader: async ({ params }) => {
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
146
|
+
return { data: `Data for ${params.id}`, timestamp: Date.now() };
|
|
147
|
+
},
|
|
148
|
+
component: ({ data, timestamp }) => `${data} at ${timestamp}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const app = alepha.inject(App);
|
|
153
|
+
await alepha.start();
|
|
154
|
+
|
|
155
|
+
expect(app.async.options.loader).toBeDefined();
|
|
156
|
+
expect(typeof app.async.options.loader).toBe("function");
|
|
157
|
+
|
|
158
|
+
const mockContext = {
|
|
159
|
+
params: { id: "test" },
|
|
160
|
+
query: {},
|
|
161
|
+
pathname: "/async/test",
|
|
162
|
+
search: "",
|
|
163
|
+
};
|
|
164
|
+
const result = await app.async.options.loader!(mockContext as any);
|
|
165
|
+
expect(result.data).toBe("Data for test");
|
|
166
|
+
expect(typeof result.timestamp).toBe("number");
|
|
167
|
+
|
|
168
|
+
const rendered = await app.async.render({ params: { id: "test" } });
|
|
169
|
+
expect(rendered.html).toMatch(/^Data for test at \d+$/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("$page - parent-child relationship", async ({ expect }) => {
|
|
173
|
+
class App {
|
|
174
|
+
parent = $page({
|
|
175
|
+
path: "/parent",
|
|
176
|
+
loader: () => ({ parentData: "from parent" }),
|
|
177
|
+
children: [],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
child = $page({
|
|
181
|
+
path: "/child",
|
|
182
|
+
parent: this.parent,
|
|
183
|
+
loader: ({ parentData }) => ({
|
|
184
|
+
childData: `child with ${parentData}`,
|
|
185
|
+
}),
|
|
186
|
+
component: ({ childData }) => childData,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const app = alepha.inject(App);
|
|
191
|
+
await alepha.start();
|
|
192
|
+
|
|
193
|
+
expect(app.child.options.parent).toBe(app.parent);
|
|
194
|
+
|
|
195
|
+
const rendered = await app.child.render();
|
|
196
|
+
expect(rendered.html).toBe("child with from parent");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("$page - children as function", async ({ expect }) => {
|
|
200
|
+
class App {
|
|
201
|
+
c1 = $page({ path: "/child1", component: () => "Child 1" });
|
|
202
|
+
c2 = $page({ path: "/child2", component: () => "Child 2" });
|
|
203
|
+
parent = $page({
|
|
204
|
+
path: "/parent",
|
|
205
|
+
children: () => [this.c1, this.c2],
|
|
206
|
+
component: () => (
|
|
207
|
+
<>
|
|
208
|
+
Parent content
|
|
209
|
+
<NestedView />
|
|
210
|
+
</>
|
|
211
|
+
),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const app = alepha.inject(App);
|
|
216
|
+
await alepha.start();
|
|
217
|
+
|
|
218
|
+
const children =
|
|
219
|
+
typeof app.parent.options.children === "function"
|
|
220
|
+
? app.parent.options.children()
|
|
221
|
+
: app.parent.options.children;
|
|
222
|
+
|
|
223
|
+
expect(children).toHaveLength(2);
|
|
224
|
+
expect(children?.[0].options.path).toBe("/child1");
|
|
225
|
+
expect(children?.[1].options.path).toBe("/child2");
|
|
226
|
+
|
|
227
|
+
const parentRendered = await app.parent.render();
|
|
228
|
+
expect(parentRendered.html).toBe("Parent content");
|
|
229
|
+
|
|
230
|
+
const child1Rendered = await app.c1.render();
|
|
231
|
+
expect(child1Rendered.html).toBe("Parent content<!-- -->Child 1");
|
|
232
|
+
|
|
233
|
+
const child2Rendered = await app.c2.render();
|
|
234
|
+
expect(child2Rendered.html).toBe("Parent content<!-- -->Child 2");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("$page - can permission function", async ({ expect }) => {
|
|
238
|
+
let canAccess = true;
|
|
239
|
+
|
|
240
|
+
class App {
|
|
241
|
+
protected = $page({
|
|
242
|
+
path: "/protected",
|
|
243
|
+
can: () => canAccess,
|
|
244
|
+
component: () => "Protected content",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const app = alepha.inject(App);
|
|
249
|
+
await alepha.start();
|
|
250
|
+
|
|
251
|
+
expect(app.protected.options.can?.()).toBe(true);
|
|
252
|
+
|
|
253
|
+
canAccess = false;
|
|
254
|
+
expect(app.protected.options.can?.()).toBe(false);
|
|
255
|
+
|
|
256
|
+
const rendered = await app.protected.render();
|
|
257
|
+
expect(rendered.html).toBe("Protected content");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("$page - error handler returns ReactNode", async ({ expect }) => {
|
|
261
|
+
class App {
|
|
262
|
+
errorPage = $page({
|
|
263
|
+
path: "/error",
|
|
264
|
+
loader: () => {
|
|
265
|
+
throw new Error("Test error");
|
|
266
|
+
},
|
|
267
|
+
errorHandler: (error) => `Error: ${error.message}`,
|
|
268
|
+
component: () => "Should not render",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const app = alepha.inject(App);
|
|
273
|
+
await alepha.start();
|
|
274
|
+
|
|
275
|
+
expect(app.errorPage.options.errorHandler).toBeDefined();
|
|
276
|
+
|
|
277
|
+
const mockState = {} as ReactRouterState;
|
|
278
|
+
const result = app.errorPage.options.errorHandler?.(
|
|
279
|
+
new Error("Test error"),
|
|
280
|
+
mockState,
|
|
281
|
+
);
|
|
282
|
+
expect(result).toBe("Error: Test error");
|
|
283
|
+
|
|
284
|
+
const rendered = await app.errorPage.render();
|
|
285
|
+
expect(rendered.html).toBe("Error: Test error");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("$page - error handler returns Redirection", async ({ expect }) => {
|
|
289
|
+
class App {
|
|
290
|
+
errorPage = $page({
|
|
291
|
+
path: "/error",
|
|
292
|
+
loader: () => {
|
|
293
|
+
throw new Error("unauthorized");
|
|
294
|
+
},
|
|
295
|
+
errorHandler: (error) => {
|
|
296
|
+
if (error.message === "unauthorized") {
|
|
297
|
+
return new Redirection("/login");
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
},
|
|
301
|
+
component: () => "Should not render",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const app = alepha.inject(App);
|
|
306
|
+
await alepha.start();
|
|
307
|
+
|
|
308
|
+
const mockState = {} as ReactRouterState;
|
|
309
|
+
|
|
310
|
+
const redirect = app.errorPage.options.errorHandler?.(
|
|
311
|
+
new Error("unauthorized"),
|
|
312
|
+
mockState,
|
|
313
|
+
);
|
|
314
|
+
expect(redirect).toBeInstanceOf(Redirection);
|
|
315
|
+
expect((redirect as Redirection).redirect).toBe("/login");
|
|
316
|
+
|
|
317
|
+
const noRedirect = app.errorPage.options.errorHandler?.(
|
|
318
|
+
new Error("other"),
|
|
319
|
+
mockState,
|
|
320
|
+
);
|
|
321
|
+
expect(noRedirect).toBeUndefined();
|
|
322
|
+
|
|
323
|
+
const rendered = await app.errorPage.render();
|
|
324
|
+
expect(rendered.redirect).toBe("/login");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("$page - static page configuration", async ({ expect }) => {
|
|
328
|
+
class App {
|
|
329
|
+
staticPage = $page({
|
|
330
|
+
path: "/static",
|
|
331
|
+
static: true,
|
|
332
|
+
component: () => "Static content",
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
staticWithEntries = $page({
|
|
336
|
+
path: "/static/:id",
|
|
337
|
+
schema: {
|
|
338
|
+
params: t.object({
|
|
339
|
+
id: t.text(),
|
|
340
|
+
}),
|
|
341
|
+
},
|
|
342
|
+
static: {
|
|
343
|
+
entries: [{ params: { id: "1" } }, { params: { id: "2" } }],
|
|
344
|
+
},
|
|
345
|
+
loader: ({ params }) => ({ id: params.id }),
|
|
346
|
+
component: ({ id }) => `Static page ${id}`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const app = alepha.inject(App);
|
|
351
|
+
await alepha.start();
|
|
352
|
+
|
|
353
|
+
expect(app.staticPage.options.static).toBe(true);
|
|
354
|
+
expect(app.staticWithEntries.options.static).toEqual({
|
|
355
|
+
entries: [{ params: { id: "1" } }, { params: { id: "2" } }],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const staticRendered = await app.staticPage.render();
|
|
359
|
+
expect(staticRendered.html).toBe("Static content");
|
|
360
|
+
|
|
361
|
+
const staticWithEntriesRendered = await app.staticWithEntries.render({
|
|
362
|
+
params: { id: "1" },
|
|
363
|
+
});
|
|
364
|
+
expect(staticWithEntriesRendered.html).toBe("Static page 1");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("$page - static page sets default cache configuration", async ({
|
|
368
|
+
expect,
|
|
369
|
+
}) => {
|
|
370
|
+
class App {
|
|
371
|
+
staticPage = $page({
|
|
372
|
+
path: "/static",
|
|
373
|
+
static: true,
|
|
374
|
+
component: () => "Static content",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const app = alepha.inject(App);
|
|
379
|
+
await alepha.start();
|
|
380
|
+
|
|
381
|
+
expect(app.staticPage.options.cache).toEqual({
|
|
382
|
+
store: {
|
|
383
|
+
provider: "memory",
|
|
384
|
+
ttl: [1, "week"],
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const rendered = await app.staticPage.render();
|
|
389
|
+
expect(rendered.html).toBe("Static content");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("$page - static page respects custom cache configuration", async ({
|
|
393
|
+
expect,
|
|
394
|
+
}) => {
|
|
395
|
+
class App {
|
|
396
|
+
staticPage = $page({
|
|
397
|
+
path: "/static",
|
|
398
|
+
static: true,
|
|
399
|
+
component: () => `Static ${Date.now()}`,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const app = alepha.inject(App);
|
|
404
|
+
await alepha.start();
|
|
405
|
+
|
|
406
|
+
const { html } = await app.staticPage.fetch();
|
|
407
|
+
expect(html).toMatch(/^Static \d+$/);
|
|
408
|
+
|
|
409
|
+
expect(await app.staticPage.fetch().then((it) => it.html)).toBe(html);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("$page - client-side only rendering", async ({ expect }) => {
|
|
413
|
+
class App {
|
|
414
|
+
clientOnly = $page({
|
|
415
|
+
path: "/client",
|
|
416
|
+
client: true,
|
|
417
|
+
component: () => "Client only",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const app = alepha.inject(App);
|
|
422
|
+
await alepha.start();
|
|
423
|
+
|
|
424
|
+
const clientRendered = await app.clientOnly.fetch();
|
|
425
|
+
expect(clientRendered.html).toBe("");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("$page - server response handler", async ({ expect }) => {
|
|
429
|
+
const mockHandler = vi.fn();
|
|
430
|
+
|
|
431
|
+
class App {
|
|
432
|
+
withHandler = $page({
|
|
433
|
+
path: "/handler",
|
|
434
|
+
onServerResponse: mockHandler,
|
|
435
|
+
component: () => "With handler",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const app = alepha.inject(App);
|
|
440
|
+
await alepha.start();
|
|
441
|
+
|
|
442
|
+
expect(app.withHandler.options.onServerResponse).toBe(mockHandler);
|
|
443
|
+
|
|
444
|
+
const rendered = await app.withHandler.render();
|
|
445
|
+
expect(rendered.html).toBe("With handler");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("$page - onLeave handler", async ({ expect }) => {
|
|
449
|
+
const mockLeaveHandler = vi.fn();
|
|
450
|
+
|
|
451
|
+
class App {
|
|
452
|
+
leavePage = $page({
|
|
453
|
+
path: "/leave",
|
|
454
|
+
onLeave: mockLeaveHandler,
|
|
455
|
+
component: () => "Leave page",
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const app = alepha.inject(App);
|
|
460
|
+
await alepha.start();
|
|
461
|
+
|
|
462
|
+
expect(app.leavePage.options.onLeave).toBe(mockLeaveHandler);
|
|
463
|
+
|
|
464
|
+
const rendered = await app.leavePage.render();
|
|
465
|
+
expect(rendered.html).toBe("Leave page");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("$page - animation configuration", async ({ expect }) => {
|
|
469
|
+
class App {
|
|
470
|
+
simpleAnimation = $page({
|
|
471
|
+
path: "/simple-anim",
|
|
472
|
+
animation: "fadeIn",
|
|
473
|
+
component: () => "Simple animation",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
detailedAnimation = $page({
|
|
477
|
+
path: "/detailed-anim",
|
|
478
|
+
animation: {
|
|
479
|
+
enter: { name: "fadeIn", duration: 300, timing: "ease-in" },
|
|
480
|
+
exit: { name: "fadeOut", duration: 200, timing: "ease-out" },
|
|
481
|
+
},
|
|
482
|
+
component: () => "Detailed animation",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
functionAnimation = $page({
|
|
486
|
+
path: "/function-anim",
|
|
487
|
+
animation: (state) => ({ enter: "slideIn", exit: "slideOut" }),
|
|
488
|
+
component: () => "Function animation",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const app = alepha.inject(App);
|
|
493
|
+
await alepha.start();
|
|
494
|
+
|
|
495
|
+
expect(app.simpleAnimation.options.animation).toBe("fadeIn");
|
|
496
|
+
expect(app.detailedAnimation.options.animation).toEqual({
|
|
497
|
+
enter: { name: "fadeIn", duration: 300, timing: "ease-in" },
|
|
498
|
+
exit: { name: "fadeOut", duration: 200, timing: "ease-out" },
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const mockState = {} as ReactRouterState;
|
|
502
|
+
const dynamicAnimation =
|
|
503
|
+
typeof app.functionAnimation.options.animation === "function"
|
|
504
|
+
? app.functionAnimation.options.animation(mockState)
|
|
505
|
+
: app.functionAnimation.options.animation;
|
|
506
|
+
expect(dynamicAnimation).toEqual({ enter: "slideIn", exit: "slideOut" });
|
|
507
|
+
|
|
508
|
+
const simpleRendered = await app.simpleAnimation.render();
|
|
509
|
+
expect(simpleRendered.html).toBe("Simple animation");
|
|
510
|
+
|
|
511
|
+
const detailedRendered = await app.detailedAnimation.render();
|
|
512
|
+
expect(detailedRendered.html).toBe("Detailed animation");
|
|
513
|
+
|
|
514
|
+
const functionRendered = await app.functionAnimation.render();
|
|
515
|
+
expect(functionRendered.html).toBe("Function animation");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("$page - match method (not implemented)", ({ expect }) => {
|
|
519
|
+
class App {
|
|
520
|
+
page = $page({
|
|
521
|
+
path: "/test",
|
|
522
|
+
component: () => "test",
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const app = alepha.inject(App);
|
|
527
|
+
|
|
528
|
+
expect(app.page.match("/test")).toBe(false);
|
|
529
|
+
expect(app.page.match("/other")).toBe(false);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("$page - pathname method", ({ expect }) => {
|
|
533
|
+
class App {
|
|
534
|
+
withPath = $page({
|
|
535
|
+
path: "/test/:id",
|
|
536
|
+
component: () => "test",
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
withoutPath = $page({
|
|
540
|
+
component: () => "test",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const app = alepha.inject(App);
|
|
545
|
+
|
|
546
|
+
expect(app.withPath.pathname({})).toBe("/test/:id");
|
|
547
|
+
expect(app.withoutPath.pathname({})).toBe("");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("$page - complex schema with nested objects", async ({ expect }) => {
|
|
551
|
+
class App {
|
|
552
|
+
complex = $page({
|
|
553
|
+
path: "/complex/:userId",
|
|
554
|
+
schema: {
|
|
555
|
+
params: t.object({
|
|
556
|
+
userId: t.text(),
|
|
557
|
+
}),
|
|
558
|
+
query: t.object({
|
|
559
|
+
filters: t.optional(
|
|
560
|
+
t.object({
|
|
561
|
+
status: t.text({ default: "active" }),
|
|
562
|
+
category: t.optional(t.text()),
|
|
563
|
+
}),
|
|
564
|
+
),
|
|
565
|
+
page: t.number({ default: 1 }),
|
|
566
|
+
limit: t.number({ default: 10 }),
|
|
567
|
+
}),
|
|
568
|
+
},
|
|
569
|
+
loader: ({ params, query }) => ({
|
|
570
|
+
user: { id: params.userId },
|
|
571
|
+
pagination: { page: query.page, limit: query.limit },
|
|
572
|
+
filters: query.filters,
|
|
573
|
+
}),
|
|
574
|
+
component: ({ user, pagination, filters }) => {
|
|
575
|
+
return `User ${user.id}, Page ${pagination.page}/${pagination.limit}, Filters: ${JSON.stringify(filters).replaceAll('"', " ")}`;
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const app = alepha.inject(App);
|
|
581
|
+
await alepha.start();
|
|
582
|
+
|
|
583
|
+
expect(app.complex.options.schema?.params).toBeDefined();
|
|
584
|
+
expect(app.complex.options.schema?.query).toBeDefined();
|
|
585
|
+
expect(app.complex.options.loader).toBeDefined();
|
|
586
|
+
|
|
587
|
+
const rendered = await app.complex.render({
|
|
588
|
+
params: { userId: "123" },
|
|
589
|
+
query: {
|
|
590
|
+
page: "2",
|
|
591
|
+
limit: "20",
|
|
592
|
+
filters: JSON.stringify({ status: "inactive", category: "premium" }),
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
expect(rendered.html).toBe(
|
|
596
|
+
"User 123, Page 2/20, Filters: { status : inactive , category : premium }",
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("$page - multiple error handlers in hierarchy", async ({ expect }) => {
|
|
601
|
+
class App {
|
|
602
|
+
parent = $page({
|
|
603
|
+
path: "/parent",
|
|
604
|
+
errorHandler: (error) => `Parent handled: ${error.message}`,
|
|
605
|
+
children: [],
|
|
606
|
+
component: () => "Parent content",
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
child = $page({
|
|
610
|
+
path: "/child",
|
|
611
|
+
loader: () => {
|
|
612
|
+
throw new Error("Child error");
|
|
613
|
+
},
|
|
614
|
+
errorHandler: (error) => {
|
|
615
|
+
if (error.message === "Child error") {
|
|
616
|
+
return `Child handled: ${error.message}`;
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
619
|
+
},
|
|
620
|
+
component: () => "Child",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const app = alepha.inject(App);
|
|
625
|
+
await alepha.start();
|
|
626
|
+
|
|
627
|
+
const mockState = {} as ReactRouterState;
|
|
628
|
+
|
|
629
|
+
const childResult = app.child.options.errorHandler?.(
|
|
630
|
+
new Error("Child error"),
|
|
631
|
+
mockState,
|
|
632
|
+
);
|
|
633
|
+
expect(childResult).toBe("Child handled: Child error");
|
|
634
|
+
|
|
635
|
+
const parentResult = app.parent.options.errorHandler?.(
|
|
636
|
+
new Error("Other error"),
|
|
637
|
+
mockState,
|
|
638
|
+
);
|
|
639
|
+
expect(parentResult).toBe("Parent handled: Other error");
|
|
640
|
+
|
|
641
|
+
const childRendered = await app.child.render();
|
|
642
|
+
expect(childRendered.html).toBe("Child handled: Child error");
|
|
643
|
+
|
|
644
|
+
const parentRendered = await app.parent.render();
|
|
645
|
+
expect(parentRendered.html).toBe("Parent content");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("$page - resolve function receives parent props", async ({ expect }) => {
|
|
649
|
+
class App {
|
|
650
|
+
parent = $page({
|
|
651
|
+
loader: () => ({ parentValue: "from parent" }),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
child = $page({
|
|
655
|
+
path: "/child",
|
|
656
|
+
parent: this.parent,
|
|
657
|
+
loader: ({ parentValue }) => ({
|
|
658
|
+
childData: `Child received: ${parentValue}`,
|
|
659
|
+
}),
|
|
660
|
+
component: ({ childData, parentValue }) =>
|
|
661
|
+
`${childData} and ${parentValue}`,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const app = alepha.inject(App);
|
|
666
|
+
await alepha.start();
|
|
667
|
+
|
|
668
|
+
expect(app.child.options.loader).toBeDefined();
|
|
669
|
+
|
|
670
|
+
const rendered = await app.child.fetch();
|
|
671
|
+
expect(rendered.html).toBe("Child received: from parent and from parent");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("$page - cache configuration without static", async ({ expect }) => {
|
|
675
|
+
class App {
|
|
676
|
+
cached = $page({
|
|
677
|
+
path: "/cached",
|
|
678
|
+
cache: {
|
|
679
|
+
store: {
|
|
680
|
+
provider: "memory",
|
|
681
|
+
ttl: [5, "minute"],
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
component: () => "Cached content",
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const app = alepha.inject(App);
|
|
689
|
+
await alepha.start();
|
|
690
|
+
|
|
691
|
+
expect(app.cached.options.cache).toEqual({
|
|
692
|
+
store: {
|
|
693
|
+
provider: "memory",
|
|
694
|
+
ttl: [5, "minute"],
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
expect(app.cached.options.static).toBeUndefined();
|
|
698
|
+
|
|
699
|
+
const rendered = await app.cached.render();
|
|
700
|
+
expect(rendered.html).toBe("Cached content");
|
|
701
|
+
});
|
|
702
|
+
});
|