@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,165 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Signal } from "@xmachines/play-signals";
|
|
3
|
+
import { RouterBridgeBase } from "../src/router-bridge-base.js";
|
|
4
|
+
|
|
5
|
+
class TestBridge extends RouterBridgeBase {
|
|
6
|
+
protected navigateRouter(_path: string): void {}
|
|
7
|
+
protected watchRouterChanges(): void {}
|
|
8
|
+
protected unwatchRouterChanges(): void {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class InitialPathBridge extends RouterBridgeBase {
|
|
12
|
+
protected navigateRouter(_path: string): void {}
|
|
13
|
+
protected watchRouterChanges(): void {}
|
|
14
|
+
protected unwatchRouterChanges(): void {}
|
|
15
|
+
protected getInitialRouterPath(): string | null {
|
|
16
|
+
return "/dashboard/main";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeBridge(overrides?: {
|
|
21
|
+
getStateIdByPath?: (path: string) => string | undefined;
|
|
22
|
+
getPathByStateId?: (id: string) => string | undefined;
|
|
23
|
+
actorSend?: (event: unknown) => void;
|
|
24
|
+
}) {
|
|
25
|
+
const actor = {
|
|
26
|
+
currentRoute: new Signal.State<string | null>("/"),
|
|
27
|
+
send: overrides?.actorSend ?? vi.fn(),
|
|
28
|
+
};
|
|
29
|
+
const routeMap = {
|
|
30
|
+
getStateIdByPath:
|
|
31
|
+
overrides?.getStateIdByPath ??
|
|
32
|
+
((path: string) => (path === "/dashboard" ? "dashboard" : undefined)),
|
|
33
|
+
getPathByStateId:
|
|
34
|
+
overrides?.getPathByStateId ??
|
|
35
|
+
((id: string) => (id === "dashboard" ? "/dashboard/:tab" : undefined)),
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
bridge: new TestBridge(actor as any, routeMap),
|
|
39
|
+
actor,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("RouterBridgeBase edge branches", () => {
|
|
44
|
+
it("initializes lastSyncedPath to empty string when actor route is null", () => {
|
|
45
|
+
const actor = {
|
|
46
|
+
currentRoute: new Signal.State<string | null>(null),
|
|
47
|
+
send: vi.fn(),
|
|
48
|
+
};
|
|
49
|
+
const routeMap = {
|
|
50
|
+
getStateIdByPath: vi.fn(),
|
|
51
|
+
getPathByStateId: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const bridge = new TestBridge(actor as any, routeMap);
|
|
55
|
+
expect((bridge as any).lastSyncedPath).toBe("");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("connect prefers initial router path over actor route", () => {
|
|
59
|
+
const actor = {
|
|
60
|
+
currentRoute: new Signal.State<string | null>("/"),
|
|
61
|
+
send: vi.fn(),
|
|
62
|
+
};
|
|
63
|
+
const routeMap = {
|
|
64
|
+
getStateIdByPath: (path: string) =>
|
|
65
|
+
path === "/dashboard/main" ? "dashboard" : undefined,
|
|
66
|
+
getPathByStateId: () => "/dashboard/:tab",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const bridge = new InitialPathBridge(actor as any, routeMap);
|
|
70
|
+
bridge.connect();
|
|
71
|
+
|
|
72
|
+
expect(actor.send).toHaveBeenCalledWith(
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
type: "play.route",
|
|
75
|
+
to: "#dashboard",
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
bridge.disconnect();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("prefixes event target when routeMap stateId has no #", () => {
|
|
83
|
+
const { bridge, actor } = makeBridge({
|
|
84
|
+
getStateIdByPath: (path: string) =>
|
|
85
|
+
path === "/dashboard/main" ? "dashboard" : undefined,
|
|
86
|
+
getPathByStateId: () => "/dashboard/:tab",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
(bridge as any).syncActorFromRouter("/dashboard/main", "?tab=profile");
|
|
90
|
+
|
|
91
|
+
expect(actor.send).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
type: "play.route",
|
|
94
|
+
to: "#dashboard",
|
|
95
|
+
query: { tab: "profile" },
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("extractParams returns empty object when no pattern or invalid pattern", () => {
|
|
101
|
+
const noPattern = makeBridge({ getPathByStateId: () => undefined }).bridge;
|
|
102
|
+
expect((noPattern as any).extractParams("/x", "x")).toEqual({});
|
|
103
|
+
|
|
104
|
+
const invalidPattern = makeBridge({ getPathByStateId: () => "(" }).bridge;
|
|
105
|
+
expect((invalidPattern as any).extractParams("/x", "x")).toEqual({});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("extractQuery returns {} when URLSearchParams throws", () => {
|
|
109
|
+
const { bridge } = makeBridge();
|
|
110
|
+
expect((bridge as any).extractQuery(Symbol("bad") as any)).toEqual({});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("syncRouterFromActor returns early for invalid or blocked routes", () => {
|
|
114
|
+
const { bridge } = makeBridge();
|
|
115
|
+
|
|
116
|
+
(bridge as any).syncRouterFromActor(null);
|
|
117
|
+
expect((bridge as any).isProcessingNavigation).toBe(false);
|
|
118
|
+
|
|
119
|
+
(bridge as any).lastSyncedPath = "/same";
|
|
120
|
+
(bridge as any).syncRouterFromActor("/same");
|
|
121
|
+
expect((bridge as any).isProcessingNavigation).toBe(false);
|
|
122
|
+
|
|
123
|
+
(bridge as any).isProcessingNavigation = true;
|
|
124
|
+
(bridge as any).syncRouterFromActor("/dashboard");
|
|
125
|
+
expect((bridge as any).isProcessingNavigation).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("syncActorFromRouter returns early for invalid pathname or processing flag", () => {
|
|
129
|
+
const { bridge, actor } = makeBridge();
|
|
130
|
+
|
|
131
|
+
(bridge as any).syncActorFromRouter(123 as any, "");
|
|
132
|
+
expect(actor.send).not.toHaveBeenCalled();
|
|
133
|
+
|
|
134
|
+
(bridge as any).isProcessingNavigation = true;
|
|
135
|
+
(bridge as any).syncActorFromRouter("/dashboard/main", "");
|
|
136
|
+
expect(actor.send).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("extractParams returns {} when URLPattern has no match", () => {
|
|
140
|
+
const { bridge } = makeBridge({ getPathByStateId: () => "/dashboard/:tab" });
|
|
141
|
+
expect((bridge as any).extractParams("/other/path", "dashboard")).toEqual({});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("syncActorFromRouter catches actor.send errors and clears processing flag", () => {
|
|
145
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
146
|
+
const throwingSend = vi.fn(() => {
|
|
147
|
+
throw new Error("send failed");
|
|
148
|
+
});
|
|
149
|
+
const { bridge } = makeBridge({
|
|
150
|
+
getStateIdByPath: () => "dashboard",
|
|
151
|
+
getPathByStateId: () => "/dashboard/:tab",
|
|
152
|
+
actorSend: throwingSend,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
(bridge as any).syncActorFromRouter("/dashboard", "");
|
|
156
|
+
|
|
157
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
158
|
+
"[RouterBridgeBase] Error sending event:",
|
|
159
|
+
expect.any(Error),
|
|
160
|
+
);
|
|
161
|
+
expect((bridge as any).isProcessingNavigation).toBe(false);
|
|
162
|
+
|
|
163
|
+
errorSpy.mockRestore();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouterBridgeBase Unit Tests — Wave 0 Stub (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* These tests MUST FAIL until Task 2 creates RouterBridgeBase.
|
|
5
|
+
* They define the full contract for the base class.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
import { Signal } from "@xmachines/play-signals";
|
|
9
|
+
import { RouterBridgeBase } from "../src/router-bridge-base.js";
|
|
10
|
+
|
|
11
|
+
// Minimal concrete implementation for testing (only abstract methods)
|
|
12
|
+
class TestBridge extends RouterBridgeBase {
|
|
13
|
+
public navigateCalls: string[] = [];
|
|
14
|
+
public watchCalled = false;
|
|
15
|
+
public unwatchCalled = false;
|
|
16
|
+
|
|
17
|
+
protected navigateRouter(path: string): void {
|
|
18
|
+
this.navigateCalls.push(path);
|
|
19
|
+
}
|
|
20
|
+
protected watchRouterChanges(): void {
|
|
21
|
+
this.watchCalled = true;
|
|
22
|
+
}
|
|
23
|
+
protected unwatchRouterChanges(): void {
|
|
24
|
+
this.unwatchCalled = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("RouterBridgeBase", () => {
|
|
29
|
+
let mockActor: any;
|
|
30
|
+
let mockRouteMap: any;
|
|
31
|
+
let bridge: TestBridge;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
const currentRoute = new Signal.State<string | null>("/");
|
|
35
|
+
mockActor = { currentRoute, send: vi.fn() };
|
|
36
|
+
mockRouteMap = {
|
|
37
|
+
getStateIdByPath: vi.fn((path: string) =>
|
|
38
|
+
path === "/" ? "#home" : path === "/dashboard" ? "#dashboard" : undefined,
|
|
39
|
+
),
|
|
40
|
+
getPathByStateId: vi.fn((id: string) =>
|
|
41
|
+
id === "#home" ? "/" : id === "#dashboard" ? "/dashboard" : undefined,
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
bridge = new TestBridge(mockActor, mockRouteMap);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("RouterBridge protocol compliance", () => {
|
|
48
|
+
it("should implement connect() method", () => {
|
|
49
|
+
expect(typeof bridge.connect).toBe("function");
|
|
50
|
+
});
|
|
51
|
+
it("should implement disconnect() method", () => {
|
|
52
|
+
expect(typeof bridge.disconnect).toBe("function");
|
|
53
|
+
});
|
|
54
|
+
it("should not throw when calling connect()", () => {
|
|
55
|
+
expect(() => bridge.connect()).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
it("should not throw when calling disconnect()", () => {
|
|
58
|
+
bridge.connect();
|
|
59
|
+
expect(() => bridge.disconnect()).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("connect() sets up framework-specific watcher", () => {
|
|
64
|
+
it("should call watchRouterChanges() on connect", () => {
|
|
65
|
+
bridge.connect();
|
|
66
|
+
expect(bridge.watchCalled).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it("should call unwatchRouterChanges() on disconnect", () => {
|
|
69
|
+
bridge.connect();
|
|
70
|
+
bridge.disconnect();
|
|
71
|
+
expect(bridge.unwatchCalled).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("actor → router sync", () => {
|
|
76
|
+
it("should call navigateRouter when actor route changes", async () => {
|
|
77
|
+
bridge.connect();
|
|
78
|
+
bridge.navigateCalls = []; // Clear initial sync
|
|
79
|
+
mockActor.currentRoute.set("/dashboard");
|
|
80
|
+
await new Promise<void>((r) => queueMicrotask(r));
|
|
81
|
+
expect(bridge.navigateCalls).toContain("/dashboard");
|
|
82
|
+
});
|
|
83
|
+
it("should not navigate when route is unchanged (circular prevention)", async () => {
|
|
84
|
+
bridge.connect();
|
|
85
|
+
bridge.navigateCalls = [];
|
|
86
|
+
mockActor.currentRoute.set("/"); // Same as initial
|
|
87
|
+
await new Promise<void>((r) => queueMicrotask(r));
|
|
88
|
+
expect(bridge.navigateCalls).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("router → actor sync", () => {
|
|
93
|
+
it("should send play.route event when router navigates to known path", () => {
|
|
94
|
+
bridge.connect();
|
|
95
|
+
// Access protected method via type cast for testing
|
|
96
|
+
(bridge as any).syncActorFromRouter("/dashboard", "");
|
|
97
|
+
expect(mockActor.send).toHaveBeenCalledWith(
|
|
98
|
+
expect.objectContaining({ type: "play.route", to: "#dashboard" }),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
it("should not send event for unknown path", () => {
|
|
102
|
+
bridge.connect();
|
|
103
|
+
(bridge as any).syncActorFromRouter("/unknown", "");
|
|
104
|
+
expect(mockActor.send).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("circular update prevention", () => {
|
|
109
|
+
it("should not double-navigate when both syncs fire", async () => {
|
|
110
|
+
bridge.connect();
|
|
111
|
+
bridge.navigateCalls = [];
|
|
112
|
+
// Simulate actor→router direction; flag prevents router→actor echo
|
|
113
|
+
(bridge as any).syncRouterFromActor("/dashboard");
|
|
114
|
+
(bridge as any).syncActorFromRouter("/dashboard", ""); // Should be blocked
|
|
115
|
+
expect(mockActor.send).not.toHaveBeenCalled();
|
|
116
|
+
await new Promise<void>((r) => queueMicrotask(r));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|