@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,34 @@
1
+ import { test, expect, afterEach } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import App from "../../src/App.js";
5
+
6
+ afterEach(async () => {
7
+ cleanup();
8
+ });
9
+
10
+ test("E2E: User can log in and see dashboard", async () => {
11
+ render(<App />);
12
+
13
+ // App starts at home page, navigate to login
14
+ const user = userEvent.setup();
15
+ await user.click(screen.getByRole("button", { name: /login/i }));
16
+ await new Promise((resolve) => setTimeout(resolve, 50));
17
+
18
+ // Should now be at login page
19
+ expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
20
+ expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
21
+
22
+ // Fill in login form
23
+ await user.type(screen.getByLabelText(/username/i), "testuser");
24
+ await user.type(screen.getByLabelText(/password/i), "password123");
25
+
26
+ // Submit login
27
+ await user.click(screen.getByRole("button", { name: /log in/i }));
28
+
29
+ // Wait for navigation
30
+ await new Promise((resolve) => setTimeout(resolve, 100));
31
+
32
+ // Should see dashboard heading (more specific than button or description)
33
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
34
+ });
@@ -0,0 +1,34 @@
1
+ import { test, expect, afterEach } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import App from "../../src/App.js";
5
+
6
+ afterEach(async () => {
7
+ cleanup();
8
+ });
9
+
10
+ test("E2E: Navigation reflects actor state transitions", async () => {
11
+ render(<App />);
12
+
13
+ // App starts at home page, navigate to login
14
+ const user = userEvent.setup();
15
+ await user.click(screen.getByRole("button", { name: /login/i }));
16
+ await new Promise((resolve) => setTimeout(resolve, 50));
17
+
18
+ // Login first
19
+ await user.type(screen.getByLabelText(/username/i), "testuser");
20
+ await user.type(screen.getByLabelText(/password/i), "password123");
21
+ await user.click(screen.getByRole("button", { name: /log in/i }));
22
+ await new Promise((resolve) => setTimeout(resolve, 100));
23
+
24
+ // Verify we're at dashboard heading (actor state transitioned)
25
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
26
+
27
+ // Logout
28
+ const logoutButton = screen.getByRole("button", { name: /logout/i });
29
+ await user.click(logoutButton);
30
+ await new Promise((resolve) => setTimeout(resolve, 100));
31
+
32
+ // Should be back at home page (logout transitions to home state)
33
+ expect(screen.getByRole("heading", { name: /Welcome/i })).toBeInTheDocument();
34
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * TDD: Browser Back/Forward Navigation on Protected Routes
3
+ *
4
+ * Test Case: Login → Dashboard → Settings → Back to Dashboard → Browser Back
5
+ * Expected: Browser back should navigate to /settings
6
+ * Actual: Stays on / (dashboard)
7
+ *
8
+ * This reproduces the reported issue: "I can't use forward/back on protected routes"
9
+ */
10
+
11
+ import { test, expect, afterEach } from "vitest";
12
+ import { render, screen, cleanup, waitFor } from "@testing-library/react";
13
+ import userEvent from "@testing-library/user-event";
14
+ import App from "../../src/App.js";
15
+
16
+ afterEach(async () => {
17
+ cleanup();
18
+ });
19
+
20
+ test("Browser back navigates from dashboard to settings (protected route)", async () => {
21
+ // Reset URL BEFORE rendering
22
+ window.history.pushState({}, "", "/");
23
+
24
+ render(<App />);
25
+ const user = userEvent.setup();
26
+
27
+ console.log("\n=== Step 1: Login ===");
28
+ // Navigate to login if needed
29
+ if (!screen.queryByLabelText(/username/i)) {
30
+ await user.click(screen.getByRole("button", { name: /login/i }));
31
+ await new Promise((resolve) => setTimeout(resolve, 100));
32
+ }
33
+
34
+ // Fill and submit login form
35
+ await user.type(screen.getByLabelText(/username/i), "testuser");
36
+ await user.type(screen.getByLabelText(/password/i), "password123");
37
+ await user.click(screen.getByRole("button", { name: /log in/i }));
38
+
39
+ console.log("1. After login, URL:", window.location.pathname);
40
+ await waitFor(
41
+ () => {
42
+ expect(window.location.pathname).toBe("/"); // Dashboard
43
+ },
44
+ { timeout: 2000 },
45
+ );
46
+
47
+ console.log("\n=== Step 2: Navigate to Settings ===");
48
+ const settingsButton = screen
49
+ .getAllByRole("button")
50
+ .find((btn) => btn.textContent?.includes("Settings") && !btn.textContent?.includes("›"));
51
+ expect(settingsButton).toBeDefined();
52
+
53
+ await user.click(settingsButton!);
54
+
55
+ console.log("2. After clicking Settings, URL:", window.location.pathname);
56
+ await waitFor(
57
+ () => {
58
+ expect(window.location.pathname).toMatch(/\/settings/);
59
+ },
60
+ { timeout: 2000 },
61
+ );
62
+
63
+ console.log("\n=== Step 3: Click Back to Home ===");
64
+ const backButton = screen
65
+ .getAllByRole("button")
66
+ .find((btn) => btn.textContent?.includes("Back to Home"));
67
+ expect(backButton).toBeDefined();
68
+
69
+ await user.click(backButton!);
70
+
71
+ console.log("3. After Back to Home, URL:", window.location.pathname);
72
+ await waitFor(
73
+ () => {
74
+ expect(window.location.pathname).toBe("/");
75
+ },
76
+ { timeout: 2000 },
77
+ );
78
+
79
+ console.log("\n=== Step 4: Browser Back (FIXED) ===");
80
+ window.history.back();
81
+
82
+ console.log("4. After browser back(), URL:", window.location.pathname);
83
+ console.log(" EXPECTED: /settings");
84
+ await waitFor(
85
+ () => {
86
+ console.log(" ACTUAL:", window.location.pathname);
87
+ expect(window.location.pathname).toMatch(/\/settings/);
88
+ },
89
+ { timeout: 2000 },
90
+ );
91
+
92
+ // This should work now with pattern matching
93
+ });
94
+
95
+ test("Browser back/forward through multiple protected routes", async () => {
96
+ // Reset URL BEFORE rendering
97
+ window.history.pushState({}, "", "/");
98
+
99
+ render(<App />);
100
+ const user = userEvent.setup();
101
+
102
+ // Login
103
+ if (!screen.queryByLabelText(/username/i)) {
104
+ await user.click(screen.getByRole("button", { name: /login/i }));
105
+ await new Promise((resolve) => setTimeout(resolve, 100));
106
+ }
107
+
108
+ await user.type(screen.getByLabelText(/username/i), "testuser");
109
+ await user.type(screen.getByLabelText(/password/i), "password123");
110
+ await user.click(screen.getByRole("button", { name: /log in/i }));
111
+
112
+ await waitFor(
113
+ () => {
114
+ expect(window.location.pathname).toBe("/");
115
+ },
116
+ { timeout: 2000 },
117
+ );
118
+
119
+ // Navigate: Dashboard → Settings → Dashboard
120
+ const settingsButton = screen
121
+ .getAllByRole("button")
122
+ .find((btn) => btn.textContent?.includes("Settings") && !btn.textContent?.includes("›"));
123
+ await user.click(settingsButton!);
124
+ await waitFor(
125
+ () => {
126
+ expect(window.location.pathname).toMatch(/\/settings/);
127
+ },
128
+ { timeout: 2000 },
129
+ );
130
+
131
+ const backButton = screen
132
+ .getAllByRole("button")
133
+ .find((btn) => btn.textContent?.includes("Back to Home"));
134
+ await user.click(backButton!);
135
+ await waitFor(
136
+ () => {
137
+ expect(window.location.pathname).toBe("/");
138
+ },
139
+ { timeout: 2000 },
140
+ );
141
+
142
+ // Browser back should go to /settings
143
+ window.history.back();
144
+ console.log("After back(), URL:", window.location.pathname);
145
+ await waitFor(
146
+ () => {
147
+ expect(window.location.pathname).toMatch(/\/settings/);
148
+ },
149
+ { timeout: 2000 },
150
+ );
151
+
152
+ // Browser forward should return to /
153
+ window.history.forward();
154
+ console.log("After forward(), URL:", window.location.pathname);
155
+ await waitFor(
156
+ () => {
157
+ expect(window.location.pathname).toBe("/");
158
+ },
159
+ { timeout: 2000 },
160
+ );
161
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Test: URL updates when browser back triggers always-guard redirect
3
+ *
4
+ * Bug: When logged in, pressing back to /login causes a redirect to / (dashboard).
5
+ * The screen updates correctly, but the URL stays at /login instead of updating to /.
6
+ *
7
+ * Root cause: The queueMicrotask in syncActorFromUrl() was calling baseHistory.replace()
8
+ * without the recursion guard (isInsideSyncActorFromUrl), and the isProcessingBrowserNavigation
9
+ * flag was cleared too early. This caused syncUrlFromActor() (from signal watcher) to race
10
+ * with the queueMicrotask and interfere with the URL update.
11
+ *
12
+ * Fix:
13
+ * 1. Add recursion guard around replace() in queueMicrotask
14
+ * 2. Clear isProcessingBrowserNavigation flag AFTER queueMicrotask completes
15
+ * This ensures the queueMicrotask handles the URL update, not syncUrlFromActor().
16
+ */
17
+
18
+ import { test, expect, afterEach } from "vitest";
19
+ import { render, screen, cleanup } from "@testing-library/react";
20
+ import userEvent from "@testing-library/user-event";
21
+ import App from "../../src/App.js";
22
+
23
+ afterEach(async () => {
24
+ cleanup();
25
+ });
26
+
27
+ test("URL updates when back button triggers always-guard redirect from /login to /", async () => {
28
+ render(<App />);
29
+ const user = userEvent.setup();
30
+
31
+ // Step 1: Go to /login
32
+ await user.click(screen.getByRole("button", { name: /login/i }));
33
+ await new Promise((resolve) => setTimeout(resolve, 50));
34
+ expect(window.location.pathname).toBe("/login");
35
+
36
+ // Step 2: Log in (redirects to /)
37
+ await user.type(screen.getByLabelText(/username/i), "testuser");
38
+ await user.type(screen.getByLabelText(/password/i), "password123");
39
+ await user.click(screen.getByRole("button", { name: /log in/i }));
40
+ await new Promise((resolve) => setTimeout(resolve, 100));
41
+
42
+ // Verify we're at dashboard
43
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
44
+ expect(window.location.pathname).toBe("/");
45
+
46
+ // Step 3: Press back (browser goes back to /login)
47
+ // This should trigger: /login → always-guard → /dashboard (/)
48
+ // BOTH the screen AND URL should show /
49
+ window.history.back();
50
+ await new Promise((resolve) => setTimeout(resolve, 150));
51
+
52
+ // Step 4: Verify URL was updated to / (not stuck at /login)
53
+ expect(window.location.pathname).toBe("/");
54
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
55
+ });
56
+
57
+ test("URL updates when back button triggers always-guard redirect from /register to /", async () => {
58
+ render(<App />);
59
+ const user = userEvent.setup();
60
+
61
+ // Step 1: Go to /register
62
+ // (Navigate via URL since there's no register button)
63
+ window.history.pushState({}, "", "/register");
64
+ window.dispatchEvent(new PopStateEvent("popstate"));
65
+ await new Promise((resolve) => setTimeout(resolve, 100));
66
+
67
+ // Step 2: Register (redirects to /)
68
+ await user.type(screen.getByLabelText(/username/i), "testuser");
69
+ await user.type(screen.getByLabelText(/email/i), "test@example.com");
70
+ await user.type(screen.getByLabelText(/password/i), "password123");
71
+ await user.click(screen.getByRole("button", { name: /create account/i }));
72
+ await new Promise((resolve) => setTimeout(resolve, 100));
73
+
74
+ // Verify we're at dashboard
75
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
76
+ expect(window.location.pathname).toBe("/");
77
+
78
+ // Step 3: Press back (browser goes back to /register)
79
+ window.history.back();
80
+ await new Promise((resolve) => setTimeout(resolve, 150));
81
+
82
+ // Step 4: Verify URL was updated to / (not stuck at /register)
83
+ expect(window.location.pathname).toBe("/");
84
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
85
+ });
86
+
87
+ test("URL updates when redirecting from /login to / via always-guard", async () => {
88
+ render(<App />);
89
+ const user = userEvent.setup();
90
+
91
+ // 1. Log in first
92
+ await user.click(screen.getByRole("button", { name: /login/i }));
93
+ await new Promise((resolve) => setTimeout(resolve, 50));
94
+
95
+ await user.type(screen.getByLabelText(/username/i), "testuser");
96
+ await user.type(screen.getByLabelText(/password/i), "password123");
97
+ await user.click(screen.getByRole("button", { name: /log in/i }));
98
+ await new Promise((resolve) => setTimeout(resolve, 100));
99
+
100
+ // 2. Verify we're at dashboard
101
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
102
+ expect(window.location.pathname).toBe("/");
103
+
104
+ // 3. Try to navigate to /login while authenticated
105
+ // This should trigger: /login → always-guard → /dashboard (/)
106
+ // The screen AND URL should both update to /
107
+ window.history.pushState({}, "", "/login");
108
+ window.dispatchEvent(new PopStateEvent("popstate"));
109
+ await new Promise((resolve) => setTimeout(resolve, 100));
110
+
111
+ // 4. Verify URL was corrected to / (not stuck at /login)
112
+ expect(window.location.pathname).toBe("/");
113
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
114
+ });
115
+
116
+ test("URL updates when redirecting from /register to / via always-guard", async () => {
117
+ render(<App />);
118
+ const user = userEvent.setup();
119
+
120
+ // 1. Log in first
121
+ await user.click(screen.getByRole("button", { name: /login/i }));
122
+ await new Promise((resolve) => setTimeout(resolve, 50));
123
+
124
+ await user.type(screen.getByLabelText(/username/i), "testuser");
125
+ await user.type(screen.getByLabelText(/password/i), "password123");
126
+ await user.click(screen.getByRole("button", { name: /log in/i }));
127
+ await new Promise((resolve) => setTimeout(resolve, 100));
128
+
129
+ // 2. Verify we're at dashboard
130
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
131
+
132
+ // 3. Try to navigate to /register while authenticated
133
+ window.history.pushState({}, "", "/register");
134
+ window.dispatchEvent(new PopStateEvent("popstate"));
135
+ await new Promise((resolve) => setTimeout(resolve, 100));
136
+
137
+ // 4. Verify URL was corrected to / (not stuck at /register)
138
+ expect(window.location.pathname).toBe("/");
139
+ expect(screen.getByRole("heading", { name: /Welcome to the Dashboard/i })).toBeInTheDocument();
140
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Settings Parameter Browser Test
3
+ *
4
+ * Verifies that settings route displays correct section parameter from URL.
5
+ * This is a regression test for the bug where /settings/profile showed "general"
6
+ * instead of "profile".
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
10
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
11
+ import userEvent from "@testing-library/user-event";
12
+ import App from "../../src/App.js";
13
+
14
+ describe("Settings Parameter Display", () => {
15
+ beforeEach(() => {
16
+ // Clear any auth state between tests
17
+ window.history.pushState({}, "", "/");
18
+ });
19
+
20
+ afterEach(() => {
21
+ cleanup();
22
+ });
23
+
24
+ it("should display 'profile' section when navigating to /settings/profile", async () => {
25
+ const user = userEvent.setup();
26
+
27
+ // Start at login page
28
+ window.history.pushState({}, "", "/login");
29
+ render(<App />);
30
+
31
+ // Wait for login form to appear
32
+ await waitFor(() => {
33
+ expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
34
+ });
35
+
36
+ // Log in
37
+ const usernameInput = screen.getByLabelText(/username/i);
38
+ const passwordInput = screen.getByLabelText(/password/i);
39
+ const loginButton = screen.getByRole("button", { name: /log in/i });
40
+
41
+ await user.type(usernameInput, "testuser");
42
+ await user.type(passwordInput, "password123");
43
+ await user.click(loginButton);
44
+
45
+ // Wait for dashboard
46
+ await waitFor(() => {
47
+ expect(screen.getByText(/welcome to the dashboard/i)).toBeInTheDocument();
48
+ });
49
+
50
+ // Navigate to settings/profile by clicking the Settings › Profile button
51
+ const settingsProfileButton = screen.getByRole("button", { name: /settings › profile/i });
52
+ await user.click(settingsProfileButton);
53
+
54
+ // Small delay to allow routing to process
55
+ await new Promise((resolve) => setTimeout(resolve, 100));
56
+
57
+ await waitFor(
58
+ () => {
59
+ const sectionText = screen.getByText(/current section:/i);
60
+ expect(sectionText.textContent).toContain("profile");
61
+ expect(sectionText.textContent).not.toContain("general");
62
+ },
63
+ { timeout: 3000 },
64
+ );
65
+ });
66
+
67
+ it("should display 'general' section when navigating to /settings (no parameter)", async () => {
68
+ const user = userEvent.setup();
69
+
70
+ // Start at login page
71
+ window.history.pushState({}, "", "/login");
72
+ render(<App />);
73
+
74
+ // Wait for login form
75
+ await waitFor(() => {
76
+ expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
77
+ });
78
+
79
+ const usernameInput = screen.getByLabelText(/username/i);
80
+ const passwordInput = screen.getByLabelText(/password/i);
81
+ const loginButton = screen.getByRole("button", { name: /log in/i });
82
+
83
+ await user.type(usernameInput, "testuser");
84
+ await user.type(passwordInput, "password123");
85
+ await user.click(loginButton);
86
+
87
+ // Wait for dashboard
88
+ await waitFor(() => {
89
+ expect(screen.getByText(/welcome to the dashboard/i)).toBeInTheDocument();
90
+ });
91
+
92
+ // Navigate to settings (no section) by clicking the Settings button
93
+ const settingsButton = screen.getByRole("button", { name: /^settings$/i });
94
+ await user.click(settingsButton);
95
+
96
+ // Small delay to allow routing to process
97
+ await new Promise((resolve) => setTimeout(resolve, 100));
98
+
99
+ await waitFor(() => {
100
+ const sectionText = screen.getByText(/current section:/i);
101
+ expect(sectionText.textContent).toContain("general");
102
+ });
103
+ });
104
+
105
+ it("should update section display when clicking section navigation buttons", async () => {
106
+ const user = userEvent.setup();
107
+
108
+ // Start at login page
109
+ window.history.pushState({}, "", "/login");
110
+ render(<App />);
111
+
112
+ // Wait for login form
113
+ await waitFor(() => {
114
+ expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
115
+ });
116
+
117
+ // Log in
118
+ const usernameInput = screen.getByLabelText(/username/i);
119
+ const passwordInput = screen.getByLabelText(/password/i);
120
+ const loginButton = screen.getByRole("button", { name: /log in/i });
121
+
122
+ await user.type(usernameInput, "testuser");
123
+ await user.type(passwordInput, "password123");
124
+ await user.click(loginButton);
125
+
126
+ // Navigate to settings
127
+ await waitFor(() => {
128
+ expect(screen.getByText(/welcome to the dashboard/i)).toBeInTheDocument();
129
+ });
130
+
131
+ // Navigate to settings by clicking the Settings button
132
+ const settingsButton = screen.getByRole("button", { name: /^settings$/i });
133
+ await user.click(settingsButton);
134
+
135
+ // Small delay to allow routing to process
136
+ await new Promise((resolve) => setTimeout(resolve, 100));
137
+
138
+ // Wait for settings page
139
+ await waitFor(() => {
140
+ expect(screen.getByText(/current section:/i)).toBeInTheDocument();
141
+ });
142
+
143
+ // Initial section should be "general"
144
+ expect(screen.getByText(/current section: general/i)).toBeInTheDocument();
145
+
146
+ // Click "Account" button
147
+ const accountButton = screen.getByRole("button", { name: /account/i });
148
+ await user.click(accountButton);
149
+
150
+ // Should update to show "account"
151
+ await waitFor(() => {
152
+ expect(screen.getByText(/current section: account/i)).toBeInTheDocument();
153
+ });
154
+
155
+ // Click "Privacy" button
156
+ const privacyButton = screen.getByRole("button", { name: /privacy/i });
157
+ await user.click(privacyButton);
158
+
159
+ // Should update to show "privacy"
160
+ await waitFor(() => {
161
+ expect(screen.getByText(/current section: privacy/i)).toBeInTheDocument();
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * TDD Test: Settings with Query String Freeze
3
+ *
4
+ * GAP: Navigating to /settings/? causes infinite loop and app freeze
5
+ *
6
+ * This test validates that the app handles settings URLs with:
7
+ * - Trailing slashes (/settings/)
8
+ * - Empty query strings (/settings/?)
9
+ * - Query parameters (/settings?tab=profile)
10
+ *
11
+ * Without causing infinite render loops or freezing.
12
+ */
13
+
14
+ import { test, expect } from "vitest";
15
+ import { definePlayer } from "@xmachines/play-xstate";
16
+ import { authMachine } from "../../src/machines/auth-machine.js";
17
+ import { catalog } from "../../src/machines/catalog.js";
18
+ import { SignalSyncedHistory } from "@xmachines/play-tanstack-react-router";
19
+ import { extractMachineRoutes } from "@xmachines/play-router";
20
+
21
+ test("Settings with trailing slash and empty query string does not freeze", async () => {
22
+ // Create authenticated actor
23
+ const createPlayer = definePlayer({ machine: authMachine, catalog });
24
+ const actor = createPlayer();
25
+ actor.start();
26
+
27
+ // Login to access protected routes
28
+ actor.send({ type: "auth.login", username: "test", password: "pass" } as any);
29
+ await new Promise((resolve) => setTimeout(resolve, 50));
30
+ expect(actor.getSnapshot().matches("dashboard")).toBe(true);
31
+
32
+ // Extract route tree for proper route matching
33
+ const routeTree = extractMachineRoutes(authMachine);
34
+
35
+ // Initialize history with route tree
36
+ const history = new SignalSyncedHistory(actor, routeTree, "/dashboard");
37
+ await new Promise((resolve) => setTimeout(resolve, 50));
38
+
39
+ // Navigate to settings with trailing slash and empty query
40
+ // This is the problematic URL that causes the freeze
41
+ actor.send({ type: "play.route", to: "#settings" } as any);
42
+ await new Promise((resolve) => setTimeout(resolve, 100));
43
+
44
+ // Should be in settings state
45
+ expect(actor.getSnapshot().matches("settings")).toBe(true);
46
+
47
+ // Current route should be normalized (no trailing slash, no empty query)
48
+ const currentRoute = actor.currentRoute.get();
49
+ expect(currentRoute).toBe("/settings");
50
+
51
+ // Should NOT have caused infinite loop (if we got here, test passed)
52
+ expect(actor.getSnapshot().context.isAuthenticated).toBe(true);
53
+
54
+ history.dispose();
55
+ actor.stop();
56
+ });
57
+
58
+ test("Settings with query parameters works correctly", async () => {
59
+ const createPlayer = definePlayer({ machine: authMachine, catalog });
60
+ const actor = createPlayer();
61
+ actor.start();
62
+
63
+ actor.send({ type: "auth.login", username: "test", password: "pass" } as any);
64
+ await new Promise((resolve) => setTimeout(resolve, 50));
65
+
66
+ const routeTree = extractMachineRoutes(authMachine);
67
+ const history = new SignalSyncedHistory(actor, routeTree, "/dashboard");
68
+ await new Promise((resolve) => setTimeout(resolve, 50));
69
+
70
+ // Navigate to settings with query parameter
71
+ actor.send({
72
+ type: "play.route",
73
+ to: "#settings",
74
+ query: { tab: "profile" },
75
+ } as any);
76
+ await new Promise((resolve) => setTimeout(resolve, 100));
77
+
78
+ expect(actor.getSnapshot().matches("settings")).toBe(true);
79
+ expect(actor.currentRoute.get()).toBe("/settings");
80
+ expect(actor.getSnapshot().context.queryParams).toEqual({ tab: "profile" });
81
+
82
+ history.dispose();
83
+ actor.stop();
84
+ });
85
+
86
+ test("Settings with section parameter works correctly", async () => {
87
+ const createPlayer = definePlayer({ machine: authMachine, catalog });
88
+ const actor = createPlayer();
89
+ actor.start();
90
+
91
+ actor.send({ type: "auth.login", username: "test", password: "pass" } as any);
92
+ await new Promise((resolve) => setTimeout(resolve, 50));
93
+
94
+ const routeTree = extractMachineRoutes(authMachine);
95
+ const history = new SignalSyncedHistory(actor, routeTree, "/dashboard");
96
+ await new Promise((resolve) => setTimeout(resolve, 50));
97
+
98
+ // Navigate to settings with section parameter
99
+ actor.send({
100
+ type: "play.route",
101
+ to: "#settings",
102
+ params: { section: "profile" },
103
+ } as any);
104
+ await new Promise((resolve) => setTimeout(resolve, 100));
105
+
106
+ expect(actor.getSnapshot().matches("settings")).toBe(true);
107
+ expect(actor.currentRoute.get()).toBe("/settings/profile");
108
+ expect(actor.getSnapshot().context.routeParams).toEqual({ section: "profile" });
109
+
110
+ history.dispose();
111
+ actor.stop();
112
+ });
113
+
114
+ test("Direct navigation to /settings/? URL normalizes correctly", async () => {
115
+ const createPlayer = definePlayer({ machine: authMachine, catalog });
116
+ const actor = createPlayer();
117
+ actor.start();
118
+
119
+ actor.send({ type: "auth.login", username: "test", password: "pass" } as any);
120
+ await new Promise((resolve) => setTimeout(resolve, 50));
121
+
122
+ const routeTree = extractMachineRoutes(authMachine);
123
+ const history = new SignalSyncedHistory(actor, routeTree, "/dashboard");
124
+ const baseHistory = history.getHistory();
125
+ await new Promise((resolve) => setTimeout(resolve, 50));
126
+
127
+ // Simulate browser direct navigation to problematic URL
128
+ // This is what happens when user types /settings/? in address bar
129
+ baseHistory.push("/settings/?");
130
+ await new Promise((resolve) => setTimeout(resolve, 150));
131
+
132
+ // Should navigate to settings state
133
+ expect(actor.getSnapshot().matches("settings")).toBe(true);
134
+
135
+ // URL should be normalized to /settings (no trailing slash, no empty query)
136
+ const currentRoute = actor.currentRoute.get();
137
+ expect(currentRoute).toBe("/settings");
138
+
139
+ history.dispose();
140
+ actor.stop();
141
+ });