@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.
Files changed (141) hide show
  1. package/.oxfmtrc.json +3 -0
  2. package/.oxlintrc.json +3 -0
  3. package/README.md +436 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/build-tree.ts.html +316 -0
  7. package/coverage/connect-router.ts.html +505 -0
  8. package/coverage/coverage-summary.json +15 -0
  9. package/coverage/crawl-machine.ts.html +385 -0
  10. package/coverage/create-browser-history.ts.html +556 -0
  11. package/coverage/create-route-map.ts.html +400 -0
  12. package/coverage/create-router.ts.html +328 -0
  13. package/coverage/extract-route.ts.html +322 -0
  14. package/coverage/extract-routes.ts.html +286 -0
  15. package/coverage/favicon.png +0 -0
  16. package/coverage/index.html +296 -0
  17. package/coverage/index.ts.html +610 -0
  18. package/coverage/prettify.css +1 -0
  19. package/coverage/prettify.js +2 -0
  20. package/coverage/query.ts.html +307 -0
  21. package/coverage/router-bridge-base.ts.html +919 -0
  22. package/coverage/sort-arrow-sprite.png +0 -0
  23. package/coverage/sorter.js +210 -0
  24. package/coverage/types.ts.html +787 -0
  25. package/coverage/validate-routes.ts.html +319 -0
  26. package/dist/build-tree.d.ts +13 -0
  27. package/dist/build-tree.d.ts.map +1 -0
  28. package/dist/build-tree.js +67 -0
  29. package/dist/build-tree.js.map +1 -0
  30. package/dist/connect-router.d.ts +56 -0
  31. package/dist/connect-router.d.ts.map +1 -0
  32. package/dist/connect-router.js +119 -0
  33. package/dist/connect-router.js.map +1 -0
  34. package/dist/crawl-machine.d.ts +74 -0
  35. package/dist/crawl-machine.d.ts.map +1 -0
  36. package/dist/crawl-machine.js +95 -0
  37. package/dist/crawl-machine.js.map +1 -0
  38. package/dist/create-browser-history.d.ts +68 -0
  39. package/dist/create-browser-history.d.ts.map +1 -0
  40. package/dist/create-browser-history.js +94 -0
  41. package/dist/create-browser-history.js.map +1 -0
  42. package/dist/create-route-map.d.ts +46 -0
  43. package/dist/create-route-map.d.ts.map +1 -0
  44. package/dist/create-route-map.js +73 -0
  45. package/dist/create-route-map.js.map +1 -0
  46. package/dist/create-router.d.ts +73 -0
  47. package/dist/create-router.d.ts.map +1 -0
  48. package/dist/create-router.js +63 -0
  49. package/dist/create-router.js.map +1 -0
  50. package/dist/extract-route.d.ts +25 -0
  51. package/dist/extract-route.d.ts.map +1 -0
  52. package/dist/extract-route.js +63 -0
  53. package/dist/extract-route.js.map +1 -0
  54. package/dist/extract-routes.d.ts +41 -0
  55. package/dist/extract-routes.d.ts.map +1 -0
  56. package/dist/extract-routes.js +61 -0
  57. package/dist/extract-routes.js.map +1 -0
  58. package/dist/index.d.ts +56 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +141 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/query.d.ts +52 -0
  63. package/dist/query.d.ts.map +1 -0
  64. package/dist/query.js +69 -0
  65. package/dist/query.js.map +1 -0
  66. package/dist/router-bridge-base.d.ts +150 -0
  67. package/dist/router-bridge-base.d.ts.map +1 -0
  68. package/dist/router-bridge-base.js +240 -0
  69. package/dist/router-bridge-base.js.map +1 -0
  70. package/dist/types.d.ts +228 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/validate-routes.d.ts +39 -0
  75. package/dist/validate-routes.d.ts.map +1 -0
  76. package/dist/validate-routes.js +65 -0
  77. package/dist/validate-routes.js.map +1 -0
  78. package/examples/demo/README.md +127 -0
  79. package/examples/demo/index.html +41 -0
  80. package/examples/demo/package.json +27 -0
  81. package/examples/demo/src/main.ts +28 -0
  82. package/examples/demo/src/router.ts +37 -0
  83. package/examples/demo/src/shell.ts +316 -0
  84. package/examples/demo/test/browser/auth-flow.browser.test.ts +60 -0
  85. package/examples/demo/test/browser/startup.browser.test.ts +37 -0
  86. package/examples/demo/test/library-pattern.test.ts +51 -0
  87. package/examples/demo/tsconfig.json +17 -0
  88. package/examples/demo/vite.config.ts +7 -0
  89. package/examples/demo/vitest.browser.config.ts +20 -0
  90. package/examples/demo/vitest.config.ts +9 -0
  91. package/examples/shared/dist/auth-machine.d.ts +20 -0
  92. package/examples/shared/dist/auth-machine.d.ts.map +1 -0
  93. package/examples/shared/dist/auth-machine.js +212 -0
  94. package/examples/shared/dist/auth-machine.js.map +1 -0
  95. package/examples/shared/dist/catalog.d.ts +85 -0
  96. package/examples/shared/dist/catalog.d.ts.map +1 -0
  97. package/examples/shared/dist/catalog.js +86 -0
  98. package/examples/shared/dist/catalog.js.map +1 -0
  99. package/examples/shared/dist/index.d.ts +4 -0
  100. package/examples/shared/dist/index.d.ts.map +1 -0
  101. package/examples/shared/dist/index.js +3 -0
  102. package/examples/shared/dist/index.js.map +1 -0
  103. package/examples/shared/package.json +37 -0
  104. package/examples/shared/src/auth-machine.ts +234 -0
  105. package/examples/shared/src/catalog.ts +95 -0
  106. package/examples/shared/src/index.css +3 -0
  107. package/examples/shared/src/index.ts +3 -0
  108. package/examples/shared/src/styles/layout.css +413 -0
  109. package/examples/shared/src/styles/reset.css +42 -0
  110. package/examples/shared/src/styles/tokens.css +183 -0
  111. package/examples/shared/tsconfig.json +14 -0
  112. package/examples/shared/tsconfig.tsbuildinfo +1 -0
  113. package/package.json +44 -0
  114. package/src/build-tree.ts +77 -0
  115. package/src/connect-router.ts +142 -0
  116. package/src/crawl-machine.ts +100 -0
  117. package/src/create-browser-history.ts +157 -0
  118. package/src/create-route-map.ts +105 -0
  119. package/src/create-router.ts +87 -0
  120. package/src/extract-route.ts +79 -0
  121. package/src/extract-routes.ts +67 -0
  122. package/src/index.ts +175 -0
  123. package/src/query.ts +74 -0
  124. package/src/router-bridge-base.ts +279 -0
  125. package/src/types.ts +234 -0
  126. package/src/validate-routes.ts +76 -0
  127. package/test/connect-route-map.test.ts +320 -0
  128. package/test/crawl-extract.test.js +473 -0
  129. package/test/create-browser-history.test.ts +123 -0
  130. package/test/create-router.test.ts +23 -0
  131. package/test/extract-routes.test.ts +80 -0
  132. package/test/find-route-by-path-patterns.test.ts +69 -0
  133. package/test/integration.test.js +438 -0
  134. package/test/query.test.ts +56 -0
  135. package/test/router-bridge-base-edge.test.ts +165 -0
  136. package/test/router-bridge-base.test.ts +119 -0
  137. package/test/tree-query.test.js +692 -0
  138. package/test/validation.test.js +158 -0
  139. package/tsconfig.json +14 -0
  140. package/tsconfig.tsbuildinfo +1 -0
  141. 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
+ });