@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.
Files changed (203) hide show
  1. package/.oxfmtrc.json +3 -0
  2. package/.oxlintrc.json +3 -0
  3. package/README.md +177 -0
  4. package/dist/extract-params.d.ts +45 -0
  5. package/dist/extract-params.d.ts.map +1 -0
  6. package/dist/extract-params.js +70 -0
  7. package/dist/extract-params.js.map +1 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +17 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/play-router-provider.d.ts +33 -0
  13. package/dist/play-router-provider.d.ts.map +1 -0
  14. package/dist/play-router-provider.js +31 -0
  15. package/dist/play-router-provider.js.map +1 -0
  16. package/dist/route-map.d.ts +101 -0
  17. package/dist/route-map.d.ts.map +1 -0
  18. package/dist/route-map.js +139 -0
  19. package/dist/route-map.js.map +1 -0
  20. package/dist/tanstack-router-bridge.d.ts +115 -0
  21. package/dist/tanstack-router-bridge.d.ts.map +1 -0
  22. package/dist/tanstack-router-bridge.js +112 -0
  23. package/dist/tanstack-router-bridge.js.map +1 -0
  24. package/dist/types.d.ts +26 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/types.js +7 -0
  27. package/dist/types.js.map +1 -0
  28. package/dist/utils.d.ts +9 -0
  29. package/dist/utils.d.ts.map +1 -0
  30. package/dist/utils.js +10 -0
  31. package/dist/utils.js.map +1 -0
  32. package/examples/demo/README.md +100 -0
  33. package/examples/demo/docs/ARCHITECTURE.md +643 -0
  34. package/examples/demo/docs/INVARIANTS.md +461 -0
  35. package/examples/demo/docs/SWAP-REACT.md +635 -0
  36. package/examples/demo/index.html +16 -0
  37. package/examples/demo/package.json +39 -0
  38. package/examples/demo/src/App.tsx +148 -0
  39. package/examples/demo/src/components/About.tsx +49 -0
  40. package/examples/demo/src/components/Contact.tsx +43 -0
  41. package/examples/demo/src/components/Dashboard.tsx +46 -0
  42. package/examples/demo/src/components/DebugPanel.tsx +68 -0
  43. package/examples/demo/src/components/HeaderNav.tsx +103 -0
  44. package/examples/demo/src/components/Home.tsx +41 -0
  45. package/examples/demo/src/components/Login.tsx +82 -0
  46. package/examples/demo/src/components/Navigation.tsx +262 -0
  47. package/examples/demo/src/components/Profile.tsx +46 -0
  48. package/examples/demo/src/components/Register.tsx +109 -0
  49. package/examples/demo/src/components/Settings.tsx +92 -0
  50. package/examples/demo/src/components/index.ts +16 -0
  51. package/examples/demo/src/main.tsx +20 -0
  52. package/examples/demo/test/actor-authority.test.ts +50 -0
  53. 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
  54. 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
  55. 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
  56. package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--After-authentication-flow-1.png +0 -0
  57. package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-1.png +0 -0
  58. package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-2.png +0 -0
  59. 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
  60. 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
  61. package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-1.png +0 -0
  62. package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-2.png +0 -0
  63. package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--about-loads-about-page-1.png +0 -0
  64. package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--contact-loads-contact-page-1.png +0 -0
  65. package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--home-loads-home-page-1.png +0 -0
  66. package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-authenticated-loads-dashboard-1.png +0 -0
  67. 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
  68. package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-1.png +0 -0
  69. package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-2.png +0 -0
  70. 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
  71. 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
  72. package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-1.png +0 -0
  73. package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-2.png +0 -0
  74. package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-1.png +0 -0
  75. package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-2.png +0 -0
  76. package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--1.png +0 -0
  77. package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--2.png +0 -0
  78. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-1.png +0 -0
  79. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-2.png +0 -0
  80. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-1.png +0 -0
  81. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-2.png +0 -0
  82. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-1.png +0 -0
  83. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-2.png +0 -0
  84. 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
  85. 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
  86. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-1.png +0 -0
  87. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-2.png +0 -0
  88. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-1.png +0 -0
  89. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-2.png +0 -0
  90. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-1.png +0 -0
  91. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-2.png +0 -0
  92. 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
  93. 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
  94. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-1.png +0 -0
  95. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-2.png +0 -0
  96. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-1.png +0 -0
  97. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-2.png +0 -0
  98. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-1.png +0 -0
  99. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-2.png +0 -0
  100. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-1.png +0 -0
  101. package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-2.png +0 -0
  102. package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-1.png +0 -0
  103. package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-2.png +0 -0
  104. package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-1.png +0 -0
  105. package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-2.png +0 -0
  106. package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
  107. package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-2.png +0 -0
  108. 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
  109. 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
  110. package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
  111. 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
  112. 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
  113. 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
  114. 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
  115. 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
  116. 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
  117. 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
  118. 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
  119. 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
  120. 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
  121. package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-query-parameters-works-correctly-1.png +0 -0
  122. package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-section-parameter-works-correctly-1.png +0 -0
  123. 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
  124. 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
  125. package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-1.png +0 -0
  126. package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-2.png +0 -0
  127. package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-1.png +0 -0
  128. package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-2.png +0 -0
  129. package/examples/demo/test/browser/__screenshots__/test-multiple-back.browser.test.tsx/Multiple-back--Navigate-forward-3x-then-back-3x-1.png +0 -0
  130. 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
  131. 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
  132. 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
  133. 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
  134. 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
  135. 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
  136. 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
  137. 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
  138. 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
  139. 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
  140. 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
  141. 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
  142. 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
  143. 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
  144. 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
  145. 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
  146. 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
  147. package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-play-route-event-1.png +0 -0
  148. package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-xstate-route-event-1.png +0 -0
  149. package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-1.png +0 -0
  150. package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-2.png +0 -0
  151. package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-xstate-route-event-1.png +0 -0
  152. 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
  153. 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
  154. package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Protected-route-sends-xstate-route-with-authentication-guard-1.png +0 -0
  155. package/examples/demo/test/browser/back-button-duplicate.browser.test.tsx +148 -0
  156. package/examples/demo/test/browser/back-forward-sync.browser.test.tsx +149 -0
  157. package/examples/demo/test/browser/direct-navigation.browser.test.ts +146 -0
  158. package/examples/demo/test/browser/exact-user-scenario.browser.test.tsx +207 -0
  159. package/examples/demo/test/browser/guard-rejection.browser.test.tsx +52 -0
  160. package/examples/demo/test/browser/history-investigation.browser.test.tsx +82 -0
  161. package/examples/demo/test/browser/history-navigation.browser.test.ts +351 -0
  162. package/examples/demo/test/browser/login-flow.browser.test.tsx +34 -0
  163. package/examples/demo/test/browser/navigation.browser.test.tsx +34 -0
  164. package/examples/demo/test/browser/protected-route-navigation.browser.test.tsx +161 -0
  165. package/examples/demo/test/browser/redirect-url-update.browser.test.tsx +140 -0
  166. package/examples/demo/test/browser/settings-parameter.browser.test.tsx +164 -0
  167. package/examples/demo/test/browser/settings-query-freeze.browser.test.ts +141 -0
  168. package/examples/demo/test/browser/state-driven.browser.test.ts +112 -0
  169. package/examples/demo/test/browser/tanstack-integration.browser.test.tsx +61 -0
  170. package/examples/demo/test/browser/uat-xstate-route-regression.browser.test.ts +58 -0
  171. package/examples/demo/test/browser/xstate-route-events.browser.test.ts +293 -0
  172. package/examples/demo/test/browser-back-view-rendering.test.ts +104 -0
  173. package/examples/demo/test/browser-e2e/auth-flow.browser.test.tsx +49 -0
  174. package/examples/demo/test/invalid-route-redirect.test.ts +40 -0
  175. package/examples/demo/test/passive-infra.test.ts +35 -0
  176. package/examples/demo/test/route-parameters.test.ts +539 -0
  177. package/examples/demo/test/signal-only.test.ts +54 -0
  178. package/examples/demo/test/strict-separation.test.ts +37 -0
  179. package/examples/demo/test/test-utils.ts +49 -0
  180. package/examples/demo/tsconfig.json +21 -0
  181. package/examples/demo/tsconfig.tsbuildinfo +1 -0
  182. package/examples/demo/vite.config.ts +13 -0
  183. package/examples/demo/vitest.browser.config.ts +72 -0
  184. package/examples/demo/vitest.config.e2e.browser.ts +28 -0
  185. package/examples/demo/vitest.config.ts +35 -0
  186. package/package.json +51 -0
  187. package/src/extract-params.ts +75 -0
  188. package/src/index.ts +31 -0
  189. package/src/play-router-provider.tsx +46 -0
  190. package/src/route-map.ts +158 -0
  191. package/src/tanstack-router-bridge.ts +135 -0
  192. package/src/types.ts +26 -0
  193. package/src/utils.ts +12 -0
  194. package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/Browser-back-button-sends-route-navigate-event-to-actor-1.png +0 -0
  195. package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-prevents-circular-updates-1.png +0 -0
  196. package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-syncs-actor-route-to-browser-URL-1.png +0 -0
  197. package/test/browser/signal-synced-history.browser.test.ts +95 -0
  198. package/test/route-map.test.ts +107 -0
  199. package/test/tanstack-router-bridge.test.ts +318 -0
  200. package/test/urlpattern-integration.test.ts +145 -0
  201. package/tsconfig.json +16 -0
  202. package/tsconfig.tsbuildinfo +1 -0
  203. 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
+ }