@xmachines/play-tanstack-react-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 +177 -0
- package/dist/extract-params.d.ts +45 -0
- package/dist/extract-params.d.ts.map +1 -0
- package/dist/extract-params.js +70 -0
- package/dist/extract-params.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/play-router-provider.d.ts +33 -0
- package/dist/play-router-provider.d.ts.map +1 -0
- package/dist/play-router-provider.js +31 -0
- package/dist/play-router-provider.js.map +1 -0
- package/dist/route-map.d.ts +101 -0
- package/dist/route-map.d.ts.map +1 -0
- package/dist/route-map.js +139 -0
- package/dist/route-map.js.map +1 -0
- package/dist/tanstack-router-bridge.d.ts +115 -0
- package/dist/tanstack-router-bridge.d.ts.map +1 -0
- package/dist/tanstack-router-bridge.js +112 -0
- package/dist/tanstack-router-bridge.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/utils.js.map +1 -0
- package/examples/demo/README.md +100 -0
- package/examples/demo/docs/ARCHITECTURE.md +643 -0
- package/examples/demo/docs/INVARIANTS.md +461 -0
- package/examples/demo/docs/SWAP-REACT.md +635 -0
- package/examples/demo/index.html +16 -0
- package/examples/demo/package.json +39 -0
- package/examples/demo/src/App.tsx +148 -0
- package/examples/demo/src/components/About.tsx +49 -0
- package/examples/demo/src/components/Contact.tsx +43 -0
- package/examples/demo/src/components/Dashboard.tsx +46 -0
- package/examples/demo/src/components/DebugPanel.tsx +68 -0
- package/examples/demo/src/components/HeaderNav.tsx +103 -0
- package/examples/demo/src/components/Home.tsx +41 -0
- package/examples/demo/src/components/Login.tsx +82 -0
- package/examples/demo/src/components/Navigation.tsx +262 -0
- package/examples/demo/src/components/Profile.tsx +46 -0
- package/examples/demo/src/components/Register.tsx +109 -0
- package/examples/demo/src/components/Settings.tsx +92 -0
- package/examples/demo/src/components/index.ts +16 -0
- package/examples/demo/src/main.tsx +20 -0
- package/examples/demo/test/actor-authority.test.ts +50 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/Browser-back-button-navigates-through-unique-history--no-duplicates--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--After-authentication-flow-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--about-loads-about-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--contact-loads-contact-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--home-loads-home-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-authenticated-loads-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-unauthenticated-redirects-to-login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-query-parameters-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-section-parameter-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/test-multiple-back.browser.test.tsx/Multiple-back--Navigate-forward-3x-then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-1--Opening-with--someinvalidstate-stays-at-current-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2--Opening-with--about-renders-About-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2b--Opening-with--home-renders-Home-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2c--Opening-with--contact-renders-Contact-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--login---home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-xstate-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Protected-route-sends-xstate-route-with-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/back-button-duplicate.browser.test.tsx +148 -0
- package/examples/demo/test/browser/back-forward-sync.browser.test.tsx +149 -0
- package/examples/demo/test/browser/direct-navigation.browser.test.ts +146 -0
- package/examples/demo/test/browser/exact-user-scenario.browser.test.tsx +207 -0
- package/examples/demo/test/browser/guard-rejection.browser.test.tsx +52 -0
- package/examples/demo/test/browser/history-investigation.browser.test.tsx +82 -0
- package/examples/demo/test/browser/history-navigation.browser.test.ts +351 -0
- package/examples/demo/test/browser/login-flow.browser.test.tsx +34 -0
- package/examples/demo/test/browser/navigation.browser.test.tsx +34 -0
- package/examples/demo/test/browser/protected-route-navigation.browser.test.tsx +161 -0
- package/examples/demo/test/browser/redirect-url-update.browser.test.tsx +140 -0
- package/examples/demo/test/browser/settings-parameter.browser.test.tsx +164 -0
- package/examples/demo/test/browser/settings-query-freeze.browser.test.ts +141 -0
- package/examples/demo/test/browser/state-driven.browser.test.ts +112 -0
- package/examples/demo/test/browser/tanstack-integration.browser.test.tsx +61 -0
- package/examples/demo/test/browser/uat-xstate-route-regression.browser.test.ts +58 -0
- package/examples/demo/test/browser/xstate-route-events.browser.test.ts +293 -0
- package/examples/demo/test/browser-back-view-rendering.test.ts +104 -0
- package/examples/demo/test/browser-e2e/auth-flow.browser.test.tsx +49 -0
- package/examples/demo/test/invalid-route-redirect.test.ts +40 -0
- package/examples/demo/test/passive-infra.test.ts +35 -0
- package/examples/demo/test/route-parameters.test.ts +539 -0
- package/examples/demo/test/signal-only.test.ts +54 -0
- package/examples/demo/test/strict-separation.test.ts +37 -0
- package/examples/demo/test/test-utils.ts +49 -0
- package/examples/demo/tsconfig.json +21 -0
- package/examples/demo/tsconfig.tsbuildinfo +1 -0
- package/examples/demo/vite.config.ts +13 -0
- package/examples/demo/vitest.browser.config.ts +72 -0
- package/examples/demo/vitest.config.e2e.browser.ts +28 -0
- package/examples/demo/vitest.config.ts +35 -0
- package/package.json +51 -0
- package/src/extract-params.ts +75 -0
- package/src/index.ts +31 -0
- package/src/play-router-provider.tsx +46 -0
- package/src/route-map.ts +158 -0
- package/src/tanstack-router-bridge.ts +135 -0
- package/src/types.ts +26 -0
- package/src/utils.ts +12 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/Browser-back-button-sends-route-navigate-event-to-actor-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-prevents-circular-updates-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-syncs-actor-route-to-browser-URL-1.png +0 -0
- package/test/browser/signal-synced-history.browser.test.ts +95 -0
- package/test/route-map.test.ts +107 -0
- package/test/tanstack-router-bridge.test.ts +318 -0
- package/test/urlpattern-integration.test.ts +145 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteMap Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests bidirectional state ID ↔ path mapping with pattern matching support.
|
|
5
|
+
* React Router adapter uses same RouteMap as Solid Router.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { RouteMap } from "../src/route-map.js";
|
|
10
|
+
|
|
11
|
+
describe("RouteMap", () => {
|
|
12
|
+
describe("construction", () => {
|
|
13
|
+
it("should create RouteMap with mappings", () => {
|
|
14
|
+
const routeMap = new RouteMap([
|
|
15
|
+
{ stateId: "#home", path: "/" },
|
|
16
|
+
{ stateId: "#profile", path: "/profile/:userId" },
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
expect(routeMap).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should handle empty mappings array", () => {
|
|
23
|
+
const routeMap = new RouteMap([]);
|
|
24
|
+
|
|
25
|
+
expect(routeMap).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("getPathByStateId - reverse lookup", () => {
|
|
30
|
+
it("should return path for valid state ID", () => {
|
|
31
|
+
const routeMap = new RouteMap([
|
|
32
|
+
{ stateId: "#home", path: "/" },
|
|
33
|
+
{ stateId: "#profile", path: "/profile/:userId" },
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
expect(routeMap.getPathByStateId("#home")).toBe("/");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return path pattern for dynamic routes", () => {
|
|
40
|
+
const routeMap = new RouteMap([{ stateId: "#profile", path: "/profile/:userId" }]);
|
|
41
|
+
|
|
42
|
+
expect(routeMap.getPathByStateId("#profile")).toBe("/profile/:userId");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return null for non-existent state ID", () => {
|
|
46
|
+
const routeMap = new RouteMap([{ stateId: "#home", path: "/" }]);
|
|
47
|
+
|
|
48
|
+
expect(routeMap.getPathByStateId("#nonexistent")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("getStateIdByPath - exact match", () => {
|
|
53
|
+
it("should return state ID for exact path match", () => {
|
|
54
|
+
const routeMap = new RouteMap([
|
|
55
|
+
{ stateId: "#home", path: "/" },
|
|
56
|
+
{ stateId: "#about", path: "/about" },
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
expect(routeMap.getStateIdByPath("/")).toBe("#home");
|
|
60
|
+
expect(routeMap.getStateIdByPath("/about")).toBe("#about");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return null for non-existent path", () => {
|
|
64
|
+
const routeMap = new RouteMap([{ stateId: "#home", path: "/" }]);
|
|
65
|
+
|
|
66
|
+
expect(routeMap.getStateIdByPath("/nonexistent")).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("getStateIdByPath - pattern matching", () => {
|
|
71
|
+
it("should match path with required parameter", () => {
|
|
72
|
+
const routeMap = new RouteMap([{ stateId: "#profile", path: "/profile/:userId" }]);
|
|
73
|
+
|
|
74
|
+
expect(routeMap.getStateIdByPath("/profile/123")).toBe("#profile");
|
|
75
|
+
expect(routeMap.getStateIdByPath("/profile/abc")).toBe("#profile");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should match path with optional parameter present", () => {
|
|
79
|
+
const routeMap = new RouteMap([{ stateId: "#settings", path: "/settings/:section?" }]);
|
|
80
|
+
|
|
81
|
+
expect(routeMap.getStateIdByPath("/settings/account")).toBe("#settings");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should match path with optional parameter absent", () => {
|
|
85
|
+
const routeMap = new RouteMap([{ stateId: "#settings", path: "/settings/:section?" }]);
|
|
86
|
+
|
|
87
|
+
expect(routeMap.getStateIdByPath("/settings")).toBe("#settings");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should match nested dynamic routes", () => {
|
|
91
|
+
const routeMap = new RouteMap([
|
|
92
|
+
{ stateId: "#post", path: "/users/:userId/posts/:postId" },
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
expect(routeMap.getStateIdByPath("/users/123/posts/456")).toBe("#post");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should prefer exact match over pattern match", () => {
|
|
99
|
+
const routeMap = new RouteMap([
|
|
100
|
+
{ stateId: "#settings-profile", path: "/settings/profile" },
|
|
101
|
+
{ stateId: "#settings", path: "/settings/:section?" },
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
expect(routeMap.getStateIdByPath("/settings/profile")).toBe("#settings-profile");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStackReactRouterBridge Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests RouterBridge protocol compliance, bidirectional sync with Signal.subtle.Watcher pattern,
|
|
5
|
+
* and circular update prevention.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
9
|
+
import { Signal } from "@xmachines/play-signals";
|
|
10
|
+
import { TanStackReactRouterBridge } from "../src/tanstack-router-bridge.js";
|
|
11
|
+
import { RouteMap } from "../src/route-map.js";
|
|
12
|
+
|
|
13
|
+
describe("TanStackReactRouterBridge", () => {
|
|
14
|
+
let mockRouter: any;
|
|
15
|
+
let mockActor: any;
|
|
16
|
+
let routeMap: RouteMap;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Mock TanStack React Router with router.history.subscribe
|
|
20
|
+
const historySubscribers: Set<
|
|
21
|
+
(event: { location: { pathname: string; search: string }; action: string }) => void
|
|
22
|
+
> = new Set();
|
|
23
|
+
|
|
24
|
+
mockRouter = {
|
|
25
|
+
state: {
|
|
26
|
+
location: {
|
|
27
|
+
pathname: "/",
|
|
28
|
+
search: "",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
navigate: vi.fn(),
|
|
32
|
+
history: {
|
|
33
|
+
subscribe: vi.fn((fn: (event: any) => void) => {
|
|
34
|
+
historySubscribers.add(fn);
|
|
35
|
+
return () => historySubscribers.delete(fn);
|
|
36
|
+
}),
|
|
37
|
+
location: { pathname: "/", search: "" },
|
|
38
|
+
},
|
|
39
|
+
// Helper: trigger a navigation event (simulates PUSH, POP, BACK, FORWARD)
|
|
40
|
+
_triggerNavigation: (pathname: string, search = "", action = "PUSH") => {
|
|
41
|
+
mockRouter.state.location.pathname = pathname;
|
|
42
|
+
mockRouter.state.location.search = search;
|
|
43
|
+
mockRouter.history.location.pathname = pathname;
|
|
44
|
+
mockRouter.history.location.search = search;
|
|
45
|
+
historySubscribers.forEach((fn) => fn({ location: { pathname, search }, action }));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Mock actor with Signal.State
|
|
50
|
+
const currentRoute = new Signal.State<string | null>("/");
|
|
51
|
+
mockActor = {
|
|
52
|
+
currentRoute,
|
|
53
|
+
send: vi.fn(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Simple RouteMap
|
|
57
|
+
routeMap = new RouteMap([
|
|
58
|
+
{ stateId: "#home", path: "/" },
|
|
59
|
+
{ stateId: "#dashboard", path: "/dashboard" },
|
|
60
|
+
{ stateId: "#settings", path: "/settings/:section?" },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("RouterBridge protocol compliance", () => {
|
|
65
|
+
it("should implement connect() method", () => {
|
|
66
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
67
|
+
expect(bridge.connect).toBeDefined();
|
|
68
|
+
expect(typeof bridge.connect).toBe("function");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should implement disconnect() method", () => {
|
|
72
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
73
|
+
expect(bridge.disconnect).toBeDefined();
|
|
74
|
+
expect(typeof bridge.disconnect).toBe("function");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should not throw when calling connect()", () => {
|
|
78
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
79
|
+
expect(() => bridge.connect()).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should not throw when calling disconnect()", () => {
|
|
83
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
84
|
+
bridge.connect();
|
|
85
|
+
expect(() => bridge.disconnect()).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("actor → router sync", () => {
|
|
90
|
+
it("should navigate router when actor state changes", async () => {
|
|
91
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
92
|
+
bridge.connect();
|
|
93
|
+
|
|
94
|
+
mockRouter.navigate.mockClear();
|
|
95
|
+
|
|
96
|
+
// Trigger signal change
|
|
97
|
+
mockActor.currentRoute.set("/dashboard");
|
|
98
|
+
|
|
99
|
+
// Wait for watcher callback and its internal microtask
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
101
|
+
|
|
102
|
+
expect(mockRouter.navigate).toHaveBeenCalledWith({ to: "/dashboard" });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should skip navigation if path unchanged (lastSyncedPath guard)", async () => {
|
|
106
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
107
|
+
bridge.connect();
|
|
108
|
+
|
|
109
|
+
mockActor.currentRoute.set("/dashboard");
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
111
|
+
|
|
112
|
+
mockRouter.navigate.mockClear();
|
|
113
|
+
|
|
114
|
+
// Set same path again
|
|
115
|
+
mockActor.currentRoute.set("/dashboard");
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
117
|
+
|
|
118
|
+
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("router → actor sync", () => {
|
|
123
|
+
it("should send play.route event when router navigates (PUSH)", () => {
|
|
124
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
125
|
+
bridge.connect();
|
|
126
|
+
|
|
127
|
+
mockActor.send.mockClear();
|
|
128
|
+
|
|
129
|
+
mockRouter._triggerNavigation("/dashboard", "", "PUSH");
|
|
130
|
+
|
|
131
|
+
expect(mockActor.send).toHaveBeenCalledWith(
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
type: "play.route",
|
|
134
|
+
to: "#dashboard",
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should send play.route event for BACK navigation", () => {
|
|
140
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
141
|
+
bridge.connect();
|
|
142
|
+
|
|
143
|
+
// First navigate forward
|
|
144
|
+
mockRouter._triggerNavigation("/dashboard", "", "PUSH");
|
|
145
|
+
mockActor.send.mockClear();
|
|
146
|
+
|
|
147
|
+
// Then go back
|
|
148
|
+
mockRouter._triggerNavigation("/", "", "BACK");
|
|
149
|
+
|
|
150
|
+
expect(mockActor.send).toHaveBeenCalledWith(
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
type: "play.route",
|
|
153
|
+
to: "#home",
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should send play.route event for FORWARD navigation", () => {
|
|
159
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
160
|
+
bridge.connect();
|
|
161
|
+
|
|
162
|
+
// Navigate to /dashboard first so lastSyncedPath is "/"
|
|
163
|
+
mockRouter._triggerNavigation("/dashboard", "", "PUSH");
|
|
164
|
+
mockActor.send.mockClear();
|
|
165
|
+
|
|
166
|
+
// Then back to /, then forward to /dashboard
|
|
167
|
+
mockRouter._triggerNavigation("/", "", "BACK");
|
|
168
|
+
mockActor.send.mockClear();
|
|
169
|
+
|
|
170
|
+
mockRouter._triggerNavigation("/dashboard", "", "FORWARD");
|
|
171
|
+
|
|
172
|
+
expect(mockActor.send).toHaveBeenCalledWith(
|
|
173
|
+
expect.objectContaining({
|
|
174
|
+
type: "play.route",
|
|
175
|
+
to: "#dashboard",
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should extract parameters from URL", () => {
|
|
181
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
182
|
+
bridge.connect();
|
|
183
|
+
|
|
184
|
+
mockActor.send.mockClear();
|
|
185
|
+
|
|
186
|
+
mockRouter._triggerNavigation("/settings/account", "");
|
|
187
|
+
|
|
188
|
+
expect(mockActor.send).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
type: "play.route",
|
|
191
|
+
to: "#settings",
|
|
192
|
+
params: { section: "account" },
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should extract query parameters", () => {
|
|
198
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
199
|
+
bridge.connect();
|
|
200
|
+
|
|
201
|
+
// Navigate to /dashboard first so "/" is not lastSyncedPath
|
|
202
|
+
mockRouter._triggerNavigation("/dashboard", "");
|
|
203
|
+
mockActor.send.mockClear();
|
|
204
|
+
|
|
205
|
+
mockRouter._triggerNavigation("/", "?tab=home&view=grid");
|
|
206
|
+
|
|
207
|
+
expect(mockActor.send).toHaveBeenCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
type: "play.route",
|
|
210
|
+
to: "#home",
|
|
211
|
+
query: { tab: "home", view: "grid" },
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should skip send if state ID not found", () => {
|
|
217
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
218
|
+
bridge.connect();
|
|
219
|
+
|
|
220
|
+
mockActor.send.mockClear();
|
|
221
|
+
|
|
222
|
+
mockRouter._triggerNavigation("/unknown", "");
|
|
223
|
+
|
|
224
|
+
expect(mockActor.send).not.toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("getInitialRouterPath", () => {
|
|
229
|
+
it("should return current router pathname for initial sync", () => {
|
|
230
|
+
mockRouter.state.location.pathname = "/dashboard";
|
|
231
|
+
|
|
232
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
233
|
+
const initialPath = (bridge as any).getInitialRouterPath();
|
|
234
|
+
expect(initialPath).toBe("/dashboard");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should return null if router.state is missing", () => {
|
|
238
|
+
mockRouter.state = undefined;
|
|
239
|
+
|
|
240
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
241
|
+
const initialPath = (bridge as any).getInitialRouterPath();
|
|
242
|
+
expect(initialPath).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("circular update prevention", () => {
|
|
247
|
+
it("should prevent actor → router → actor loops", async () => {
|
|
248
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
249
|
+
bridge.connect();
|
|
250
|
+
|
|
251
|
+
mockActor.send.mockClear();
|
|
252
|
+
mockRouter.navigate.mockClear();
|
|
253
|
+
|
|
254
|
+
// Actor changes route → should navigate router
|
|
255
|
+
mockActor.currentRoute.set("/dashboard");
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
257
|
+
|
|
258
|
+
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
|
259
|
+
|
|
260
|
+
// Simulate router firing history event for that navigation
|
|
261
|
+
// (this is the round-trip that should be suppressed)
|
|
262
|
+
mockRouter._triggerNavigation("/dashboard", "", "PUSH");
|
|
263
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
264
|
+
|
|
265
|
+
// Should still be 1 — no circular navigation
|
|
266
|
+
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should skip redundant updates (same path)", () => {
|
|
270
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
271
|
+
bridge.connect();
|
|
272
|
+
|
|
273
|
+
mockRouter._triggerNavigation("/dashboard", "");
|
|
274
|
+
mockActor.send.mockClear();
|
|
275
|
+
|
|
276
|
+
// Same path again
|
|
277
|
+
mockRouter._triggerNavigation("/dashboard", "");
|
|
278
|
+
|
|
279
|
+
expect(mockActor.send).not.toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("cleanup", () => {
|
|
284
|
+
it("should subscribe to router.history on connect", () => {
|
|
285
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
286
|
+
bridge.connect();
|
|
287
|
+
|
|
288
|
+
expect(mockRouter.history.subscribe).toHaveBeenCalledWith(expect.any(Function));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should unsubscribe from router.history on disconnect", () => {
|
|
292
|
+
const unsubscribeSpy = vi.fn();
|
|
293
|
+
mockRouter.history.subscribe.mockReturnValue(unsubscribeSpy);
|
|
294
|
+
|
|
295
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
296
|
+
bridge.connect();
|
|
297
|
+
bridge.disconnect();
|
|
298
|
+
|
|
299
|
+
expect(unsubscribeSpy).toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should clear routerUnsubscribe after disconnect", () => {
|
|
303
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
304
|
+
bridge.connect();
|
|
305
|
+
bridge.disconnect();
|
|
306
|
+
|
|
307
|
+
expect((bridge as any).routerUnsubscribe).toBeNull();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("Signal.subtle.Watcher integration", () => {
|
|
312
|
+
it("should create watcher on connect (routeWatcher from base class)", () => {
|
|
313
|
+
const bridge = new TanStackReactRouterBridge(mockRouter, mockActor, routeMap);
|
|
314
|
+
bridge.connect();
|
|
315
|
+
expect((bridge as any).routeWatcher).toBeDefined();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URLPattern integration tests for extractParams()
|
|
3
|
+
* Tests URLPattern.exec() implementation for parameter extraction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
// Import the actual extractParams implementation by creating a history instance
|
|
9
|
+
// and extracting the function. This tests the real implementation.
|
|
10
|
+
// Note: extractParams is a private function in signal-synced-history.ts
|
|
11
|
+
// For testing, we'll import the module and test via the class usage
|
|
12
|
+
|
|
13
|
+
// We can't directly import extractParams (it's private), so we'll test it
|
|
14
|
+
// indirectly via the signal-synced-history.ts module.
|
|
15
|
+
// Create a test version that matches the implementation signature.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* extractParams implementation from signal-synced-history.ts
|
|
19
|
+
* Uses URLPattern API for robust pattern matching
|
|
20
|
+
*/
|
|
21
|
+
function extractParams(
|
|
22
|
+
path: string,
|
|
23
|
+
pattern: string | undefined,
|
|
24
|
+
): { params: Record<string, string>; match?: URLPatternResult } {
|
|
25
|
+
if (!pattern) {
|
|
26
|
+
return { params: {} };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Normalize path by removing trailing slash (except for root "/")
|
|
31
|
+
// URLPattern is strict about trailing slashes
|
|
32
|
+
const normalizedPath = path === "/" || !path.endsWith("/") ? path : path.slice(0, -1);
|
|
33
|
+
|
|
34
|
+
// Create URLPattern for pathname matching
|
|
35
|
+
const urlPattern = new URLPattern({ pathname: pattern });
|
|
36
|
+
|
|
37
|
+
// Execute pattern match against path
|
|
38
|
+
const match = urlPattern.exec({ pathname: normalizedPath });
|
|
39
|
+
|
|
40
|
+
if (!match) {
|
|
41
|
+
return { params: {} };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Extract named groups from pathname component
|
|
45
|
+
const groups = match.pathname.groups || {};
|
|
46
|
+
const params: Record<string, string> = {};
|
|
47
|
+
|
|
48
|
+
Object.keys(groups).forEach((key) => {
|
|
49
|
+
const value = groups[key];
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
// Decode URI component to handle URL encoding (%20 → space, etc.)
|
|
52
|
+
params[key] = decodeURIComponent(value);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
params,
|
|
58
|
+
match, // Full result for observability
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Invalid pattern (e.g., malformed regex) - log and return empty
|
|
62
|
+
console.error(`[extractParams] Invalid pattern: ${pattern}`, error);
|
|
63
|
+
return { params: {} };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("URLPattern integration - extractParams()", () => {
|
|
68
|
+
test("extracts simple parameter from /profile/:userId pattern", () => {
|
|
69
|
+
const result = extractParams("/profile/alice", "/profile/:userId");
|
|
70
|
+
|
|
71
|
+
expect(result).toHaveProperty("params");
|
|
72
|
+
expect(result.params).toEqual({ userId: "alice" });
|
|
73
|
+
expect(result).toHaveProperty("match");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("handles optional parameter /settings/:section? with and without value", () => {
|
|
77
|
+
// With section parameter
|
|
78
|
+
const withSection = extractParams("/settings/account", "/settings/:section?");
|
|
79
|
+
expect(withSection.params).toEqual({ section: "account" });
|
|
80
|
+
|
|
81
|
+
// Without section parameter
|
|
82
|
+
const withoutSection = extractParams("/settings", "/settings/:section?");
|
|
83
|
+
expect(withoutSection.params).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("validates regex constraint /api/:id(\\d+) for numeric IDs only", () => {
|
|
87
|
+
// Should match numeric ID
|
|
88
|
+
const numericResult = extractParams("/api/123", "/api/:id(\\d+)");
|
|
89
|
+
expect(numericResult.params).toEqual({ id: "123" });
|
|
90
|
+
|
|
91
|
+
// Should NOT match alphabetic ID
|
|
92
|
+
const alphaResult = extractParams("/api/abc", "/api/:id(\\d+)");
|
|
93
|
+
expect(alphaResult.params).toEqual({});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("matches wildcard pattern /docs/* for nested paths", () => {
|
|
97
|
+
// Single level
|
|
98
|
+
const singleLevel = extractParams("/docs/intro", "/docs/*");
|
|
99
|
+
expect(singleLevel.params).toBeDefined();
|
|
100
|
+
|
|
101
|
+
// Multiple levels
|
|
102
|
+
const multiLevel = extractParams("/docs/guides/advanced", "/docs/*");
|
|
103
|
+
expect(multiLevel.params).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("automatically decodes URL-encoded parameters", () => {
|
|
107
|
+
// %20 should decode to space
|
|
108
|
+
const result = extractParams("/search/hello%20world", "/search/:q");
|
|
109
|
+
expect(result.params).toEqual({ q: "hello world" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("handles invalid pattern gracefully with empty params", () => {
|
|
113
|
+
const errorSpy = vi
|
|
114
|
+
.spyOn(console, "error")
|
|
115
|
+
.mockImplementation((..._args: unknown[]) => undefined);
|
|
116
|
+
// Malformed pattern should return empty params
|
|
117
|
+
const result = extractParams("/profile/alice", "/:invalid(()");
|
|
118
|
+
expect(result.params).toEqual({});
|
|
119
|
+
errorSpy.mockRestore();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("returns URLPatternResult in match field for observability", () => {
|
|
123
|
+
const result = extractParams("/profile/alice", "/profile/:userId");
|
|
124
|
+
|
|
125
|
+
expect(result).toHaveProperty("match");
|
|
126
|
+
expect(result.match).toBeDefined();
|
|
127
|
+
// URLPatternResult has pathname.groups
|
|
128
|
+
expect(result.match).toHaveProperty("pathname");
|
|
129
|
+
expect(result.match?.pathname).toHaveProperty("groups");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("handles edge cases: empty pattern, no params, trailing slashes", () => {
|
|
133
|
+
// Empty/undefined pattern
|
|
134
|
+
const emptyResult = extractParams("/profile/alice", undefined);
|
|
135
|
+
expect(emptyResult.params).toEqual({});
|
|
136
|
+
|
|
137
|
+
// Pattern with no parameters
|
|
138
|
+
const noParamsResult = extractParams("/about", "/about");
|
|
139
|
+
expect(noParamsResult.params).toEqual({});
|
|
140
|
+
|
|
141
|
+
// Trailing slash handling
|
|
142
|
+
const trailingResult = extractParams("/profile/alice/", "/profile/:userId");
|
|
143
|
+
expect(trailingResult.params).toEqual({ userId: "alice" });
|
|
144
|
+
});
|
|
145
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@xmachines/shared/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"lib": ["ESNext", "DOM"],
|
|
8
|
+
"jsx": "react-jsx"
|
|
9
|
+
},
|
|
10
|
+
"references": [
|
|
11
|
+
{ "path": "../play-actor" },
|
|
12
|
+
{ "path": "../play-router" },
|
|
13
|
+
{ "path": "../play-signals" }
|
|
14
|
+
],
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|