Skip to content

Commit 4f34856

Browse files
e2e: fix navigation + resources playwright specs (admin-bypass unneeded)
Three failures on master forcing every dashboard PR to merge with gh pr merge --admin. Root causes: 1. navigation.spec.ts asserted /Stacks/ and /Team/ sidebar links — Stacks retired in b13b8ee (duplicate of Deployments), Team has no sidebar entry. Spec dropped to chrome reality. 2. fixtures.ts mocked GET /api/v1/resources with the glob `**/api/v1/resources`, which does not match the env-query variant `/api/v1/resources?env=production` that ResourcesPage emits. Glob replaced with a regex anchored on path + optional query string. 3. App.tsx <Navigate to="/app/resources/:id"> sent the literal string ":id" instead of the captured route param — Navigate is dumb and doesn't interpolate. Wrapped both /resources/:id and /deployments/:id in tiny components that read useParams() and construct the real target. This is a real product bug (legacy deep-links to resource/deploy detail pages 404'd into an empty skeleton); the spec just happened to catch it. Also dropped the dangling `/stacks → /app/stacks` redirect — target was deleted with the route. Playwright chromium: 10/10 pass. Vitest unit: 365 pass, 3 skipped (unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9d6609a commit 4f34856

3 files changed

Lines changed: 32 additions & 9 deletions

File tree

e2e/fixtures.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ export async function installAPIFake(page: Page) {
7070
}),
7171
)
7272

73-
// GET /api/v1/resources
74-
await page.route('**/api/v1/resources', (route: Route) => {
73+
// GET /api/v1/resources (optional ?env=... query string — ResourcesPage
74+
// calls listResources(ctx.env) which produces /api/v1/resources?env=production).
75+
// A bare-glob `**/api/v1/resources` only matches the no-query path, so we
76+
// use a regex anchored on the path + optional query string.
77+
await page.route(/\/api\/v1\/resources(\?[^/]*)?$/, (route: Route) => {
7578
if (route.request().method() === 'GET') {
7679
return route.fulfill({
7780
status: 200,

e2e/navigation.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ test.describe('Navigation', () => {
1010
test('every nav link reaches its page', async ({ page }) => {
1111
// The authenticated app is mounted under /app/*; the sidebar nav links
1212
// point to /app/<section>. The Overview is just /app (or /app/).
13+
//
14+
// Tracks the live AppShell sidebar — Stacks was retired (b13b8ee:
15+
// "/app/stacks duplicate route + StacksPage.tsx deleted, same data as
16+
// Deployments") and Team has no sidebar nav link in the user-facing
17+
// sidebar (the route exists but is no longer linked from chrome).
1318
await page.goto('/app')
1419
const targets: { name: RegExp; pathFragment?: string }[] = [
1520
{ name: /Resources/i, pathFragment: '/app/resources' },
1621
{ name: /Deployments/i, pathFragment: '/app/deployments' },
17-
{ name: /Stacks/i, pathFragment: '/app/stacks' },
1822
{ name: /Vault/i, pathFragment: '/app/vault' },
19-
{ name: /Team/i, pathFragment: '/app/team' },
2023
{ name: /Billing/i, pathFragment: '/app/billing' },
2124
{ name: /Settings/i, pathFragment: '/app/settings' },
2225
{ name: /Overview/i, pathFragment: '/app' },

src/App.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { lazy, Suspense } from 'react'
2-
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom'
2+
import { BrowserRouter, Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom'
33

44
// Homepage — eagerly imported. It's the cold-load path and the most-visited
55
// public surface, so it stays in the main entry chunk.
@@ -115,6 +115,19 @@ function AuthGate({ children }: { children: JSX.Element }) {
115115
return children
116116
}
117117

118+
// LegacyRedirect — <Navigate to="/app/resources/:id"> sends a literal ":id"
119+
// string instead of the captured route param (Navigate is dumb — it doesn't
120+
// interpolate). This wrapper reads the param and constructs the real
121+
// destination so /resources/<token> → /app/resources/<token> works.
122+
function LegacyResourceRedirect() {
123+
const { id = '' } = useParams()
124+
return <Navigate to={`/app/resources/${id}`} replace />
125+
}
126+
function LegacyDeploymentRedirect() {
127+
const { id = '' } = useParams()
128+
return <Navigate to={`/app/deployments/${id}`} replace />
129+
}
130+
118131
// AppLoadingFallback — shown while a lazy-loaded /app/* chunk is in flight.
119132
// Tiny inline style so it renders even before the page's own CSS resolves.
120133
// In practice this fallback is on screen for ~50-150ms on a warm cache.
@@ -209,12 +222,16 @@ export function AppRoutes() {
209222
</Route>
210223

211224
{/* Back-compat: every legacy unprefixed path that used to be a
212-
dashboard route now redirects under /app. */}
225+
dashboard route now redirects under /app. Parameterized routes
226+
use a wrapper that interpolates the captured param — see
227+
LegacyResourceRedirect / LegacyDeploymentRedirect above
228+
(Navigate's `to` is literal, not parameterized). */}
213229
<Route path="/resources" element={<Navigate to="/app/resources" replace />} />
214-
<Route path="/resources/:id" element={<Navigate to="/app/resources/:id" replace />} />
230+
<Route path="/resources/:id" element={<LegacyResourceRedirect />} />
215231
<Route path="/deployments" element={<Navigate to="/app/deployments" replace />} />
216-
<Route path="/deployments/:id" element={<Navigate to="/app/deployments/:id" replace />} />
217-
<Route path="/stacks" element={<Navigate to="/app/stacks" replace />} />
232+
<Route path="/deployments/:id" element={<LegacyDeploymentRedirect />} />
233+
{/* /stacks legacy path retired with the route (b13b8ee). Falls
234+
through to the catch-all → /. */}
218235
<Route path="/vault" element={<Navigate to="/app/vault" replace />} />
219236
<Route path="/team" element={<Navigate to="/app/team" replace />} />
220237
<Route path="/billing" element={<Navigate to="/app/billing" replace />} />

0 commit comments

Comments
 (0)