@xmachines/play-tanstack-react-router 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +3 -0
- package/README.md +177 -0
- package/dist/extract-params.d.ts +45 -0
- package/dist/extract-params.d.ts.map +1 -0
- package/dist/extract-params.js +70 -0
- package/dist/extract-params.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/play-router-provider.d.ts +33 -0
- package/dist/play-router-provider.d.ts.map +1 -0
- package/dist/play-router-provider.js +31 -0
- package/dist/play-router-provider.js.map +1 -0
- package/dist/route-map.d.ts +101 -0
- package/dist/route-map.d.ts.map +1 -0
- package/dist/route-map.js +139 -0
- package/dist/route-map.js.map +1 -0
- package/dist/tanstack-router-bridge.d.ts +115 -0
- package/dist/tanstack-router-bridge.d.ts.map +1 -0
- package/dist/tanstack-router-bridge.js +112 -0
- package/dist/tanstack-router-bridge.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/utils.js.map +1 -0
- package/examples/demo/README.md +100 -0
- package/examples/demo/docs/ARCHITECTURE.md +643 -0
- package/examples/demo/docs/INVARIANTS.md +461 -0
- package/examples/demo/docs/SWAP-REACT.md +635 -0
- package/examples/demo/index.html +16 -0
- package/examples/demo/package.json +39 -0
- package/examples/demo/src/App.tsx +148 -0
- package/examples/demo/src/components/About.tsx +49 -0
- package/examples/demo/src/components/Contact.tsx +43 -0
- package/examples/demo/src/components/Dashboard.tsx +46 -0
- package/examples/demo/src/components/DebugPanel.tsx +68 -0
- package/examples/demo/src/components/HeaderNav.tsx +103 -0
- package/examples/demo/src/components/Home.tsx +41 -0
- package/examples/demo/src/components/Login.tsx +82 -0
- package/examples/demo/src/components/Navigation.tsx +262 -0
- package/examples/demo/src/components/Profile.tsx +46 -0
- package/examples/demo/src/components/Register.tsx +109 -0
- package/examples/demo/src/components/Settings.tsx +92 -0
- package/examples/demo/src/components/index.ts +16 -0
- package/examples/demo/src/main.tsx +20 -0
- package/examples/demo/test/actor-authority.test.ts +50 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/Browser-back-button-navigates-through-unique-history--no-duplicates--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--After-authentication-flow-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--about-loads-about-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--contact-loads-contact-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--home-loads-home-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-authenticated-loads-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-unauthenticated-redirects-to-login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-query-parameters-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-section-parameter-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/test-multiple-back.browser.test.tsx/Multiple-back--Navigate-forward-3x-then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-1--Opening-with--someinvalidstate-stays-at-current-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2--Opening-with--about-renders-About-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2b--Opening-with--home-renders-Home-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2c--Opening-with--contact-renders-Contact-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--login---home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-xstate-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Protected-route-sends-xstate-route-with-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/back-button-duplicate.browser.test.tsx +148 -0
- package/examples/demo/test/browser/back-forward-sync.browser.test.tsx +149 -0
- package/examples/demo/test/browser/direct-navigation.browser.test.ts +146 -0
- package/examples/demo/test/browser/exact-user-scenario.browser.test.tsx +207 -0
- package/examples/demo/test/browser/guard-rejection.browser.test.tsx +52 -0
- package/examples/demo/test/browser/history-investigation.browser.test.tsx +82 -0
- package/examples/demo/test/browser/history-navigation.browser.test.ts +351 -0
- package/examples/demo/test/browser/login-flow.browser.test.tsx +34 -0
- package/examples/demo/test/browser/navigation.browser.test.tsx +34 -0
- package/examples/demo/test/browser/protected-route-navigation.browser.test.tsx +161 -0
- package/examples/demo/test/browser/redirect-url-update.browser.test.tsx +140 -0
- package/examples/demo/test/browser/settings-parameter.browser.test.tsx +164 -0
- package/examples/demo/test/browser/settings-query-freeze.browser.test.ts +141 -0
- package/examples/demo/test/browser/state-driven.browser.test.ts +112 -0
- package/examples/demo/test/browser/tanstack-integration.browser.test.tsx +61 -0
- package/examples/demo/test/browser/uat-xstate-route-regression.browser.test.ts +58 -0
- package/examples/demo/test/browser/xstate-route-events.browser.test.ts +293 -0
- package/examples/demo/test/browser-back-view-rendering.test.ts +104 -0
- package/examples/demo/test/browser-e2e/auth-flow.browser.test.tsx +49 -0
- package/examples/demo/test/invalid-route-redirect.test.ts +40 -0
- package/examples/demo/test/passive-infra.test.ts +35 -0
- package/examples/demo/test/route-parameters.test.ts +539 -0
- package/examples/demo/test/signal-only.test.ts +54 -0
- package/examples/demo/test/strict-separation.test.ts +37 -0
- package/examples/demo/test/test-utils.ts +49 -0
- package/examples/demo/tsconfig.json +21 -0
- package/examples/demo/tsconfig.tsbuildinfo +1 -0
- package/examples/demo/vite.config.ts +13 -0
- package/examples/demo/vitest.browser.config.ts +72 -0
- package/examples/demo/vitest.config.e2e.browser.ts +28 -0
- package/examples/demo/vitest.config.ts +35 -0
- package/package.json +51 -0
- package/src/extract-params.ts +75 -0
- package/src/index.ts +31 -0
- package/src/play-router-provider.tsx +46 -0
- package/src/route-map.ts +158 -0
- package/src/tanstack-router-bridge.ts +135 -0
- package/src/types.ts +26 -0
- package/src/utils.ts +12 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/Browser-back-button-sends-route-navigate-event-to-actor-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-prevents-circular-updates-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-syncs-actor-route-to-browser-URL-1.png +0 -0
- package/test/browser/signal-synced-history.browser.test.ts +95 -0
- package/test/route-map.test.ts +107 -0
- package/test/tanstack-router-bridge.test.ts +318 -0
- package/test/urlpattern-integration.test.ts +145 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,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
|
+
});
|