@xmachines/play-router 1.0.0-beta.1
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/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +3 -0
- package/README.md +436 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/build-tree.ts.html +316 -0
- package/coverage/connect-router.ts.html +505 -0
- package/coverage/coverage-summary.json +15 -0
- package/coverage/crawl-machine.ts.html +385 -0
- package/coverage/create-browser-history.ts.html +556 -0
- package/coverage/create-route-map.ts.html +400 -0
- package/coverage/create-router.ts.html +328 -0
- package/coverage/extract-route.ts.html +322 -0
- package/coverage/extract-routes.ts.html +286 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +296 -0
- package/coverage/index.ts.html +610 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/query.ts.html +307 -0
- package/coverage/router-bridge-base.ts.html +919 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/types.ts.html +787 -0
- package/coverage/validate-routes.ts.html +319 -0
- package/dist/build-tree.d.ts +13 -0
- package/dist/build-tree.d.ts.map +1 -0
- package/dist/build-tree.js +67 -0
- package/dist/build-tree.js.map +1 -0
- package/dist/connect-router.d.ts +56 -0
- package/dist/connect-router.d.ts.map +1 -0
- package/dist/connect-router.js +119 -0
- package/dist/connect-router.js.map +1 -0
- package/dist/crawl-machine.d.ts +74 -0
- package/dist/crawl-machine.d.ts.map +1 -0
- package/dist/crawl-machine.js +95 -0
- package/dist/crawl-machine.js.map +1 -0
- package/dist/create-browser-history.d.ts +68 -0
- package/dist/create-browser-history.d.ts.map +1 -0
- package/dist/create-browser-history.js +94 -0
- package/dist/create-browser-history.js.map +1 -0
- package/dist/create-route-map.d.ts +46 -0
- package/dist/create-route-map.d.ts.map +1 -0
- package/dist/create-route-map.js +73 -0
- package/dist/create-route-map.js.map +1 -0
- package/dist/create-router.d.ts +73 -0
- package/dist/create-router.d.ts.map +1 -0
- package/dist/create-router.js +63 -0
- package/dist/create-router.js.map +1 -0
- package/dist/extract-route.d.ts +25 -0
- package/dist/extract-route.d.ts.map +1 -0
- package/dist/extract-route.js +63 -0
- package/dist/extract-route.js.map +1 -0
- package/dist/extract-routes.d.ts +41 -0
- package/dist/extract-routes.d.ts.map +1 -0
- package/dist/extract-routes.js +61 -0
- package/dist/extract-routes.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +52 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +69 -0
- package/dist/query.js.map +1 -0
- package/dist/router-bridge-base.d.ts +150 -0
- package/dist/router-bridge-base.d.ts.map +1 -0
- package/dist/router-bridge-base.js +240 -0
- package/dist/router-bridge-base.js.map +1 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-routes.d.ts +39 -0
- package/dist/validate-routes.d.ts.map +1 -0
- package/dist/validate-routes.js +65 -0
- package/dist/validate-routes.js.map +1 -0
- package/examples/demo/README.md +127 -0
- package/examples/demo/index.html +41 -0
- package/examples/demo/package.json +27 -0
- package/examples/demo/src/main.ts +28 -0
- package/examples/demo/src/router.ts +37 -0
- package/examples/demo/src/shell.ts +316 -0
- package/examples/demo/test/browser/auth-flow.browser.test.ts +60 -0
- package/examples/demo/test/browser/startup.browser.test.ts +37 -0
- package/examples/demo/test/library-pattern.test.ts +51 -0
- package/examples/demo/tsconfig.json +17 -0
- package/examples/demo/vite.config.ts +7 -0
- package/examples/demo/vitest.browser.config.ts +20 -0
- package/examples/demo/vitest.config.ts +9 -0
- package/examples/shared/dist/auth-machine.d.ts +20 -0
- package/examples/shared/dist/auth-machine.d.ts.map +1 -0
- package/examples/shared/dist/auth-machine.js +212 -0
- package/examples/shared/dist/auth-machine.js.map +1 -0
- package/examples/shared/dist/catalog.d.ts +85 -0
- package/examples/shared/dist/catalog.d.ts.map +1 -0
- package/examples/shared/dist/catalog.js +86 -0
- package/examples/shared/dist/catalog.js.map +1 -0
- package/examples/shared/dist/index.d.ts +4 -0
- package/examples/shared/dist/index.d.ts.map +1 -0
- package/examples/shared/dist/index.js +3 -0
- package/examples/shared/dist/index.js.map +1 -0
- package/examples/shared/package.json +37 -0
- package/examples/shared/src/auth-machine.ts +234 -0
- package/examples/shared/src/catalog.ts +95 -0
- package/examples/shared/src/index.css +3 -0
- package/examples/shared/src/index.ts +3 -0
- package/examples/shared/src/styles/layout.css +413 -0
- package/examples/shared/src/styles/reset.css +42 -0
- package/examples/shared/src/styles/tokens.css +183 -0
- package/examples/shared/tsconfig.json +14 -0
- package/examples/shared/tsconfig.tsbuildinfo +1 -0
- package/package.json +44 -0
- package/src/build-tree.ts +77 -0
- package/src/connect-router.ts +142 -0
- package/src/crawl-machine.ts +100 -0
- package/src/create-browser-history.ts +157 -0
- package/src/create-route-map.ts +105 -0
- package/src/create-router.ts +87 -0
- package/src/extract-route.ts +79 -0
- package/src/extract-routes.ts +67 -0
- package/src/index.ts +175 -0
- package/src/query.ts +74 -0
- package/src/router-bridge-base.ts +279 -0
- package/src/types.ts +234 -0
- package/src/validate-routes.ts +76 -0
- package/test/connect-route-map.test.ts +320 -0
- package/test/crawl-extract.test.js +473 -0
- package/test/create-browser-history.test.ts +123 -0
- package/test/create-router.test.ts +23 -0
- package/test/extract-routes.test.ts +80 -0
- package/test/find-route-by-path-patterns.test.ts +69 -0
- package/test/integration.test.js +438 -0
- package/test/query.test.ts +56 -0
- package/test/router-bridge-base-edge.test.ts +165 -0
- package/test/router-bridge-base.test.ts +119 -0
- package/test/tree-query.test.js +692 -0
- package/test/validation.test.js +158 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import { expect } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
buildRouteTree,
|
|
5
|
+
getNavigableRoutes,
|
|
6
|
+
routeExists,
|
|
7
|
+
findRouteById,
|
|
8
|
+
findRouteByPath,
|
|
9
|
+
} from "../src/index.js";
|
|
10
|
+
|
|
11
|
+
describe("buildRouteTree()", () => {
|
|
12
|
+
test("creates root with children for flat routes", () => {
|
|
13
|
+
const routes = [
|
|
14
|
+
{
|
|
15
|
+
stateId: "home",
|
|
16
|
+
statePath: ["home"],
|
|
17
|
+
routePath: "/home",
|
|
18
|
+
isAbsolute: true,
|
|
19
|
+
routable: true,
|
|
20
|
+
metadata: {},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
stateId: "about",
|
|
24
|
+
statePath: ["about"],
|
|
25
|
+
routePath: "/about",
|
|
26
|
+
isAbsolute: true,
|
|
27
|
+
routable: true,
|
|
28
|
+
metadata: {},
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const tree = buildRouteTree(routes);
|
|
33
|
+
|
|
34
|
+
// Root node exists with correct properties
|
|
35
|
+
expect(tree.root.id).toBe("__root__");
|
|
36
|
+
expect(tree.root.path).toBe("/");
|
|
37
|
+
expect(tree.root.fullPath).toBe("/");
|
|
38
|
+
|
|
39
|
+
// Two children attached to root
|
|
40
|
+
expect(tree.root.children.length).toBe(2);
|
|
41
|
+
|
|
42
|
+
// Children are linked to root as parent
|
|
43
|
+
expect(tree.root.children[0].parent).toBe(tree.root);
|
|
44
|
+
expect(tree.root.children[1].parent).toBe(tree.root);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("builds nested hierarchy with parent-child relationships", () => {
|
|
48
|
+
const routes = [
|
|
49
|
+
{
|
|
50
|
+
stateId: "dashboard",
|
|
51
|
+
statePath: ["dashboard"],
|
|
52
|
+
routePath: "/dashboard",
|
|
53
|
+
isAbsolute: true,
|
|
54
|
+
routable: true,
|
|
55
|
+
metadata: {},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
stateId: "overview",
|
|
59
|
+
statePath: ["dashboard", "overview"],
|
|
60
|
+
routePath: "/overview",
|
|
61
|
+
isAbsolute: false,
|
|
62
|
+
routable: true,
|
|
63
|
+
metadata: {},
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const tree = buildRouteTree(routes);
|
|
68
|
+
const dashboard = tree.byStateId.get("dashboard");
|
|
69
|
+
const overview = tree.byStateId.get("overview");
|
|
70
|
+
|
|
71
|
+
// Parent-child relationship correct
|
|
72
|
+
expect(overview.parent).toBe(dashboard);
|
|
73
|
+
expect(dashboard.children.includes(overview)).toBe(true);
|
|
74
|
+
|
|
75
|
+
// Relative path inherits from parent
|
|
76
|
+
expect(overview.fullPath).toBe("/dashboard/overview");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("handles absolute child paths correctly", () => {
|
|
80
|
+
const routes = [
|
|
81
|
+
{
|
|
82
|
+
stateId: "parent",
|
|
83
|
+
statePath: ["parent"],
|
|
84
|
+
routePath: "/parent",
|
|
85
|
+
isAbsolute: true,
|
|
86
|
+
routable: true,
|
|
87
|
+
metadata: {},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
stateId: "child",
|
|
91
|
+
statePath: ["parent", "child"],
|
|
92
|
+
routePath: "/other",
|
|
93
|
+
isAbsolute: true,
|
|
94
|
+
routable: true,
|
|
95
|
+
metadata: {},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const tree = buildRouteTree(routes);
|
|
100
|
+
const child = tree.byStateId.get("child");
|
|
101
|
+
|
|
102
|
+
// Absolute child path does not inherit from parent
|
|
103
|
+
expect(child.fullPath).toBe("/other");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("populates byStateId map correctly", () => {
|
|
107
|
+
const routes = [
|
|
108
|
+
{
|
|
109
|
+
stateId: "home",
|
|
110
|
+
statePath: ["home"],
|
|
111
|
+
routePath: "/home",
|
|
112
|
+
isAbsolute: true,
|
|
113
|
+
routable: true,
|
|
114
|
+
metadata: {},
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const tree = buildRouteTree(routes);
|
|
119
|
+
|
|
120
|
+
// Root in map
|
|
121
|
+
expect(tree.byStateId.has("__root__")).toBe(true);
|
|
122
|
+
expect(tree.byStateId.get("__root__")).toBe(tree.root);
|
|
123
|
+
|
|
124
|
+
// Route in map
|
|
125
|
+
expect(tree.byStateId.has("home")).toBe(true);
|
|
126
|
+
expect(tree.byStateId.get("home")).toBe(tree.root.children[0]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("populates byPath map correctly", () => {
|
|
130
|
+
const routes = [
|
|
131
|
+
{
|
|
132
|
+
stateId: "home",
|
|
133
|
+
statePath: ["home"],
|
|
134
|
+
routePath: "/home",
|
|
135
|
+
isAbsolute: true,
|
|
136
|
+
routable: true,
|
|
137
|
+
metadata: {},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const tree = buildRouteTree(routes);
|
|
142
|
+
|
|
143
|
+
// Root in map
|
|
144
|
+
expect(tree.byPath.has("/")).toBe(true);
|
|
145
|
+
expect(tree.byPath.get("/")).toBe(tree.root);
|
|
146
|
+
|
|
147
|
+
// Route in map
|
|
148
|
+
expect(tree.byPath.has("/home")).toBe(true);
|
|
149
|
+
expect(tree.byPath.get("/home")).toBe(tree.root.children[0]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("handles complex 3-level nesting", () => {
|
|
153
|
+
const routes = [
|
|
154
|
+
{
|
|
155
|
+
stateId: "dashboard",
|
|
156
|
+
statePath: ["dashboard"],
|
|
157
|
+
routePath: "/dashboard",
|
|
158
|
+
isAbsolute: true,
|
|
159
|
+
routable: true,
|
|
160
|
+
metadata: {},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
stateId: "dashboard.settings",
|
|
164
|
+
statePath: ["dashboard", "settings"],
|
|
165
|
+
routePath: "/settings",
|
|
166
|
+
isAbsolute: false,
|
|
167
|
+
routable: true,
|
|
168
|
+
metadata: {},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
stateId: "dashboard.settings.profile",
|
|
172
|
+
statePath: ["dashboard", "settings", "profile"],
|
|
173
|
+
routePath: "/profile",
|
|
174
|
+
isAbsolute: false,
|
|
175
|
+
routable: true,
|
|
176
|
+
metadata: {},
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const tree = buildRouteTree(routes);
|
|
181
|
+
const dashboard = tree.byStateId.get("dashboard");
|
|
182
|
+
const settings = tree.byStateId.get("dashboard.settings");
|
|
183
|
+
const profile = tree.byStateId.get("dashboard.settings.profile");
|
|
184
|
+
|
|
185
|
+
// Grandparent-parent-child links
|
|
186
|
+
expect(settings.parent).toBe(dashboard);
|
|
187
|
+
expect(profile.parent).toBe(settings);
|
|
188
|
+
|
|
189
|
+
// Full path inheritance through 3 levels
|
|
190
|
+
expect(profile.fullPath).toBe("/dashboard/settings/profile");
|
|
191
|
+
|
|
192
|
+
// All children in parent arrays
|
|
193
|
+
expect(dashboard.children.includes(settings)).toBe(true);
|
|
194
|
+
expect(settings.children.includes(profile)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("handles multiple children per parent", () => {
|
|
198
|
+
const routes = [
|
|
199
|
+
{
|
|
200
|
+
stateId: "dashboard",
|
|
201
|
+
statePath: ["dashboard"],
|
|
202
|
+
routePath: "/dashboard",
|
|
203
|
+
isAbsolute: true,
|
|
204
|
+
routable: true,
|
|
205
|
+
metadata: {},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
stateId: "overview",
|
|
209
|
+
statePath: ["dashboard", "overview"],
|
|
210
|
+
routePath: "/overview",
|
|
211
|
+
isAbsolute: false,
|
|
212
|
+
routable: true,
|
|
213
|
+
metadata: {},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
stateId: "settings",
|
|
217
|
+
statePath: ["dashboard", "settings"],
|
|
218
|
+
routePath: "/settings",
|
|
219
|
+
isAbsolute: false,
|
|
220
|
+
routable: true,
|
|
221
|
+
metadata: {},
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const tree = buildRouteTree(routes);
|
|
226
|
+
const dashboard = tree.byStateId.get("dashboard");
|
|
227
|
+
|
|
228
|
+
// Dashboard has both children
|
|
229
|
+
expect(dashboard.children.length).toBe(2);
|
|
230
|
+
|
|
231
|
+
// Both children reference dashboard as parent
|
|
232
|
+
expect(dashboard.children[0].parent).toBe(dashboard);
|
|
233
|
+
expect(dashboard.children[1].parent).toBe(dashboard);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("handles mixed absolute and relative paths", () => {
|
|
237
|
+
const routes = [
|
|
238
|
+
{
|
|
239
|
+
stateId: "dashboard",
|
|
240
|
+
statePath: ["dashboard"],
|
|
241
|
+
routePath: "/dashboard",
|
|
242
|
+
isAbsolute: true,
|
|
243
|
+
routable: true,
|
|
244
|
+
metadata: {},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
stateId: "relative",
|
|
248
|
+
statePath: ["dashboard", "relative"],
|
|
249
|
+
routePath: "/relative",
|
|
250
|
+
isAbsolute: false,
|
|
251
|
+
routable: true,
|
|
252
|
+
metadata: {},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
stateId: "absolute",
|
|
256
|
+
statePath: ["dashboard", "absolute"],
|
|
257
|
+
routePath: "/absolute",
|
|
258
|
+
isAbsolute: true,
|
|
259
|
+
routable: true,
|
|
260
|
+
metadata: {},
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
const tree = buildRouteTree(routes);
|
|
265
|
+
const relative = tree.byStateId.get("relative");
|
|
266
|
+
const absolute = tree.byStateId.get("absolute");
|
|
267
|
+
|
|
268
|
+
// Relative inherits parent path
|
|
269
|
+
expect(relative.fullPath).toBe("/dashboard/relative");
|
|
270
|
+
|
|
271
|
+
// Absolute does not inherit
|
|
272
|
+
expect(absolute.fullPath).toBe("/absolute");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("getNavigableRoutes()", () => {
|
|
277
|
+
test("returns child routes for state with children", () => {
|
|
278
|
+
const routes = [
|
|
279
|
+
{
|
|
280
|
+
stateId: "dashboard",
|
|
281
|
+
statePath: ["dashboard"],
|
|
282
|
+
routePath: "/dashboard",
|
|
283
|
+
isAbsolute: true,
|
|
284
|
+
routable: true,
|
|
285
|
+
metadata: {},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
stateId: "overview",
|
|
289
|
+
statePath: ["dashboard", "overview"],
|
|
290
|
+
routePath: "/overview",
|
|
291
|
+
isAbsolute: false,
|
|
292
|
+
routable: true,
|
|
293
|
+
metadata: {},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
stateId: "settings",
|
|
297
|
+
statePath: ["dashboard", "settings"],
|
|
298
|
+
routePath: "/settings",
|
|
299
|
+
isAbsolute: false,
|
|
300
|
+
routable: true,
|
|
301
|
+
metadata: {},
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const tree = buildRouteTree(routes);
|
|
306
|
+
const navigable = getNavigableRoutes(tree, "dashboard");
|
|
307
|
+
|
|
308
|
+
// Returns child routes
|
|
309
|
+
expect(navigable.length).toBe(2);
|
|
310
|
+
|
|
311
|
+
// Returned nodes match children
|
|
312
|
+
const overview = tree.byStateId.get("overview");
|
|
313
|
+
const settings = tree.byStateId.get("settings");
|
|
314
|
+
expect(navigable.includes(overview)).toBe(true);
|
|
315
|
+
expect(navigable.includes(settings)).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("returns empty array for state with no children", () => {
|
|
319
|
+
const routes = [
|
|
320
|
+
{
|
|
321
|
+
stateId: "dashboard",
|
|
322
|
+
statePath: ["dashboard"],
|
|
323
|
+
routePath: "/dashboard",
|
|
324
|
+
isAbsolute: true,
|
|
325
|
+
routable: true,
|
|
326
|
+
metadata: {},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
stateId: "overview",
|
|
330
|
+
statePath: ["dashboard", "overview"],
|
|
331
|
+
routePath: "/overview",
|
|
332
|
+
isAbsolute: false,
|
|
333
|
+
routable: true,
|
|
334
|
+
metadata: {},
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const tree = buildRouteTree(routes);
|
|
339
|
+
const navigable = getNavigableRoutes(tree, "overview");
|
|
340
|
+
|
|
341
|
+
// No children, returns empty
|
|
342
|
+
expect(navigable.length).toBe(0);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("returns empty array for non-existent state", () => {
|
|
346
|
+
const routes = [
|
|
347
|
+
{
|
|
348
|
+
stateId: "dashboard",
|
|
349
|
+
statePath: ["dashboard"],
|
|
350
|
+
routePath: "/dashboard",
|
|
351
|
+
isAbsolute: true,
|
|
352
|
+
routable: true,
|
|
353
|
+
metadata: {},
|
|
354
|
+
},
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
const tree = buildRouteTree(routes);
|
|
358
|
+
const navigable = getNavigableRoutes(tree, "nonexistent");
|
|
359
|
+
|
|
360
|
+
// State doesn't exist, returns empty
|
|
361
|
+
expect(navigable.length).toBe(0);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("routeExists()", () => {
|
|
366
|
+
test("returns true for existing path", () => {
|
|
367
|
+
const routes = [
|
|
368
|
+
{
|
|
369
|
+
stateId: "dashboard",
|
|
370
|
+
statePath: ["dashboard"],
|
|
371
|
+
routePath: "/dashboard",
|
|
372
|
+
isAbsolute: true,
|
|
373
|
+
routable: true,
|
|
374
|
+
metadata: {},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
stateId: "overview",
|
|
378
|
+
statePath: ["dashboard", "overview"],
|
|
379
|
+
routePath: "/overview",
|
|
380
|
+
isAbsolute: false,
|
|
381
|
+
routable: true,
|
|
382
|
+
metadata: {},
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
const tree = buildRouteTree(routes);
|
|
387
|
+
|
|
388
|
+
// Existing paths return true
|
|
389
|
+
expect(routeExists(tree, "/dashboard")).toBe(true);
|
|
390
|
+
expect(routeExists(tree, "/dashboard/overview")).toBe(true);
|
|
391
|
+
expect(routeExists(tree, "/")).toBe(true); // Root exists
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("returns false for non-existent path", () => {
|
|
395
|
+
const routes = [
|
|
396
|
+
{
|
|
397
|
+
stateId: "dashboard",
|
|
398
|
+
statePath: ["dashboard"],
|
|
399
|
+
routePath: "/dashboard",
|
|
400
|
+
isAbsolute: true,
|
|
401
|
+
routable: true,
|
|
402
|
+
metadata: {},
|
|
403
|
+
},
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
const tree = buildRouteTree(routes);
|
|
407
|
+
|
|
408
|
+
// Non-existent paths return false
|
|
409
|
+
expect(routeExists(tree, "/nonexistent")).toBe(false);
|
|
410
|
+
expect(routeExists(tree, "/dashboard/fake")).toBe(false);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("returns false for partial paths not in tree", () => {
|
|
414
|
+
const routes = [
|
|
415
|
+
{
|
|
416
|
+
stateId: "dashboard",
|
|
417
|
+
statePath: ["dashboard"],
|
|
418
|
+
routePath: "/dashboard",
|
|
419
|
+
isAbsolute: true,
|
|
420
|
+
routable: true,
|
|
421
|
+
metadata: {},
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
const tree = buildRouteTree(routes);
|
|
426
|
+
|
|
427
|
+
// Partial paths that aren't actual routes return false
|
|
428
|
+
expect(routeExists(tree, "/dash")).toBe(false);
|
|
429
|
+
expect(routeExists(tree, "/dashboard/")).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("findRouteById()", () => {
|
|
434
|
+
test("finds route by state ID", () => {
|
|
435
|
+
const routes = [
|
|
436
|
+
{
|
|
437
|
+
stateId: "home",
|
|
438
|
+
statePath: ["home"],
|
|
439
|
+
routePath: "/home",
|
|
440
|
+
isAbsolute: true,
|
|
441
|
+
routable: true,
|
|
442
|
+
metadata: {},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
stateId: "dashboard",
|
|
446
|
+
statePath: ["dashboard"],
|
|
447
|
+
routePath: "/dashboard",
|
|
448
|
+
isAbsolute: true,
|
|
449
|
+
routable: true,
|
|
450
|
+
metadata: {},
|
|
451
|
+
},
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const tree = buildRouteTree(routes);
|
|
455
|
+
const node = findRouteById(tree, "dashboard");
|
|
456
|
+
|
|
457
|
+
expect(node).not.toBe(undefined);
|
|
458
|
+
expect(node.id).toBe("dashboard");
|
|
459
|
+
expect(node.fullPath).toBe("/dashboard");
|
|
460
|
+
expect(node.routable).toBe(true);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("returns undefined for non-existent ID", () => {
|
|
464
|
+
const routes = [
|
|
465
|
+
{
|
|
466
|
+
stateId: "home",
|
|
467
|
+
statePath: ["home"],
|
|
468
|
+
routePath: "/home",
|
|
469
|
+
isAbsolute: true,
|
|
470
|
+
routable: false,
|
|
471
|
+
metadata: {},
|
|
472
|
+
},
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
const tree = buildRouteTree(routes);
|
|
476
|
+
const node = findRouteById(tree, "nonexistent");
|
|
477
|
+
|
|
478
|
+
expect(node).toBe(undefined);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("finds nested route by state ID", () => {
|
|
482
|
+
const routes = [
|
|
483
|
+
{
|
|
484
|
+
stateId: "dashboard",
|
|
485
|
+
statePath: ["dashboard"],
|
|
486
|
+
routePath: "/dashboard",
|
|
487
|
+
isAbsolute: true,
|
|
488
|
+
routable: true,
|
|
489
|
+
metadata: {},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
stateId: "settings",
|
|
493
|
+
statePath: ["dashboard", "settings"],
|
|
494
|
+
routePath: "/settings",
|
|
495
|
+
isAbsolute: false,
|
|
496
|
+
routable: true,
|
|
497
|
+
metadata: {},
|
|
498
|
+
},
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
const tree = buildRouteTree(routes);
|
|
502
|
+
const node = findRouteById(tree, "settings");
|
|
503
|
+
|
|
504
|
+
expect(node).not.toBe(undefined);
|
|
505
|
+
expect(node.id).toBe("settings");
|
|
506
|
+
expect(node.fullPath).toBe("/dashboard/settings");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("distinguishes between routable and non-routable states", () => {
|
|
510
|
+
const routes = [
|
|
511
|
+
{
|
|
512
|
+
stateId: "home",
|
|
513
|
+
statePath: ["home"],
|
|
514
|
+
routePath: "/home",
|
|
515
|
+
isAbsolute: true,
|
|
516
|
+
routable: true, // Has route: {} config
|
|
517
|
+
metadata: {},
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
stateId: "about",
|
|
521
|
+
statePath: ["about"],
|
|
522
|
+
routePath: "/about",
|
|
523
|
+
isAbsolute: true,
|
|
524
|
+
routable: false, // Only meta.route (legacy)
|
|
525
|
+
metadata: {},
|
|
526
|
+
},
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
const tree = buildRouteTree(routes);
|
|
530
|
+
const homeNode = findRouteById(tree, "home");
|
|
531
|
+
const aboutNode = findRouteById(tree, "about");
|
|
532
|
+
|
|
533
|
+
expect(homeNode.routable).toBe(true);
|
|
534
|
+
expect(aboutNode.routable).toBe(false);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe("findRouteByPath()", () => {
|
|
539
|
+
test("finds route by URL path", () => {
|
|
540
|
+
const routes = [
|
|
541
|
+
{
|
|
542
|
+
stateId: "contact",
|
|
543
|
+
statePath: ["contact"],
|
|
544
|
+
routePath: "/contact",
|
|
545
|
+
isAbsolute: true,
|
|
546
|
+
routable: true,
|
|
547
|
+
metadata: {},
|
|
548
|
+
},
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
const tree = buildRouteTree(routes);
|
|
552
|
+
const node = findRouteByPath(tree, "/contact");
|
|
553
|
+
|
|
554
|
+
expect(node).not.toBe(undefined);
|
|
555
|
+
expect(node.id).toBe("contact");
|
|
556
|
+
expect(node.stateId).toBe("contact");
|
|
557
|
+
expect(node.routable).toBe(true);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("returns undefined for non-existent path", () => {
|
|
561
|
+
const routes = [
|
|
562
|
+
{
|
|
563
|
+
stateId: "home",
|
|
564
|
+
statePath: ["home"],
|
|
565
|
+
routePath: "/home",
|
|
566
|
+
isAbsolute: true,
|
|
567
|
+
routable: false,
|
|
568
|
+
metadata: {},
|
|
569
|
+
},
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
const tree = buildRouteTree(routes);
|
|
573
|
+
const node = findRouteByPath(tree, "/nonexistent");
|
|
574
|
+
|
|
575
|
+
expect(node).toBe(undefined);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("finds nested route by full path", () => {
|
|
579
|
+
const routes = [
|
|
580
|
+
{
|
|
581
|
+
stateId: "dashboard",
|
|
582
|
+
statePath: ["dashboard"],
|
|
583
|
+
routePath: "/dashboard",
|
|
584
|
+
isAbsolute: true,
|
|
585
|
+
routable: true,
|
|
586
|
+
metadata: {},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
stateId: "profile",
|
|
590
|
+
statePath: ["dashboard", "profile"],
|
|
591
|
+
routePath: "/profile",
|
|
592
|
+
isAbsolute: false,
|
|
593
|
+
routable: true,
|
|
594
|
+
metadata: {},
|
|
595
|
+
},
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
const tree = buildRouteTree(routes);
|
|
599
|
+
const node = findRouteByPath(tree, "/dashboard/profile");
|
|
600
|
+
|
|
601
|
+
expect(node).not.toBe(undefined);
|
|
602
|
+
expect(node.id).toBe("profile");
|
|
603
|
+
expect(node.fullPath).toBe("/dashboard/profile");
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("can be used to send play.route events", () => {
|
|
607
|
+
// Simulates browser navigation → play.route event flow
|
|
608
|
+
const routes = [
|
|
609
|
+
{
|
|
610
|
+
stateId: "settings",
|
|
611
|
+
statePath: ["settings"],
|
|
612
|
+
routePath: "/settings",
|
|
613
|
+
isAbsolute: true,
|
|
614
|
+
routable: true,
|
|
615
|
+
metadata: {},
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
|
|
619
|
+
const tree = buildRouteTree(routes);
|
|
620
|
+
const pathname = "/settings";
|
|
621
|
+
const node = findRouteByPath(tree, pathname);
|
|
622
|
+
|
|
623
|
+
// Check if routable before sending play.route
|
|
624
|
+
if (node?.routable) {
|
|
625
|
+
const event = { type: "play.route", to: `#${node.id}` };
|
|
626
|
+
expect(event.to).toBe("#settings");
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe("bidirectional mapping", () => {
|
|
632
|
+
test("byId and byPath provide bidirectional lookup", () => {
|
|
633
|
+
const routes = [
|
|
634
|
+
{
|
|
635
|
+
stateId: "dashboard",
|
|
636
|
+
statePath: ["dashboard"],
|
|
637
|
+
routePath: "/dashboard",
|
|
638
|
+
isAbsolute: true,
|
|
639
|
+
routable: true,
|
|
640
|
+
metadata: {},
|
|
641
|
+
},
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
const tree = buildRouteTree(routes);
|
|
645
|
+
|
|
646
|
+
// State ID → path
|
|
647
|
+
const nodeById = tree.byStateId.get("dashboard");
|
|
648
|
+
expect(nodeById.fullPath).toBe("/dashboard");
|
|
649
|
+
|
|
650
|
+
// Path → state ID
|
|
651
|
+
const nodeByPath = tree.byPath.get("/dashboard");
|
|
652
|
+
expect(nodeByPath.id).toBe("dashboard");
|
|
653
|
+
|
|
654
|
+
// Both references point to same node
|
|
655
|
+
expect(nodeById).toBe(nodeByPath);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("supports URL sync workflow", () => {
|
|
659
|
+
// Simulates complete URL sync flow: actor → URL → actor
|
|
660
|
+
const routes = [
|
|
661
|
+
{
|
|
662
|
+
stateId: "profile",
|
|
663
|
+
statePath: ["profile"],
|
|
664
|
+
routePath: "/profile/:userId",
|
|
665
|
+
isAbsolute: true,
|
|
666
|
+
routable: true,
|
|
667
|
+
metadata: {},
|
|
668
|
+
},
|
|
669
|
+
];
|
|
670
|
+
|
|
671
|
+
const tree = buildRouteTree(routes);
|
|
672
|
+
|
|
673
|
+
// Actor transitioned to 'profile' state
|
|
674
|
+
const stateId = "profile";
|
|
675
|
+
const node = findRouteById(tree, stateId);
|
|
676
|
+
|
|
677
|
+
// Update browser URL with path
|
|
678
|
+
const urlPath = node.fullPath;
|
|
679
|
+
expect(urlPath).toBe("/profile/:userId");
|
|
680
|
+
|
|
681
|
+
// Browser navigated to /profile/123
|
|
682
|
+
// (Would need param matching in real implementation)
|
|
683
|
+
const browserPath = "/profile/:userId";
|
|
684
|
+
const targetNode = findRouteByPath(tree, browserPath);
|
|
685
|
+
|
|
686
|
+
// Send play.route event
|
|
687
|
+
if (targetNode?.routable) {
|
|
688
|
+
const event = { type: "play.route", to: `#${targetNode.id}` };
|
|
689
|
+
expect(event.to).toBe("#profile");
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
});
|