From 636fac44cc11fedd3ca939e45ef7c99fdfcae667 Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Tue, 1 Sep 2026 11:36:51 +0200 Subject: [PATCH] fix(e2e): assert the deep link as a path, not a hash Follow-up to #1341, which left one E2E test red on development (111 passed, 1 failed). SPA-001 navigates correctly -- gotoHash() was already de-hashed -- but its closing assertion still required the URL to contain '#/search', which history routing no longer produces. Matched as a SUFFIX rather than an exact URL: Nextcloud serves the app as both /apps/... and /index.php/apps/..., and the router emits whichever base the page was loaded under, so pinning either would fail on the other for a reason that is not a defect. Renamed the file-local gotoHash() to gotoRoute(). It stopped using a hash when the app moved to history routing, and a helper whose name says 'hash' is the next reader's wrong turn. Extended the docblock to say WHY the assertion matters: the Dashboard is exactly what renders when the router cannot resolve a path and the catch-all redirects to '/', so 'Search, not Dashboard' is the real guard against a base-mismatch regression. Verified: SPA-001 passes against the dev container. prettier --check on the full glob is clean. --- tests/e2e/spec-coverage/gate19.spec.ts | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/e2e/spec-coverage/gate19.spec.ts b/tests/e2e/spec-coverage/gate19.spec.ts index e9afdbded..3227a31f4 100644 --- a/tests/e2e/spec-coverage/gate19.spec.ts +++ b/tests/e2e/spec-coverage/gate19.spec.ts @@ -100,7 +100,7 @@ async function openIndexRoute(page: Page, route: string): Promise { * @param page The Playwright page. * @param route The in-app route (e.g. '/catalogi/123'). */ -async function gotoHash(page: Page, route: string): Promise { +async function gotoRoute(page: Page, route: string): Promise { await page.goto(`${APP}${route}`, { waitUntil: 'domcontentloaded' }) await page.waitForTimeout(1500) await dismissOverlays(page) @@ -188,22 +188,26 @@ test.describe('spa-deep-link-routing', () => { * THEN it returns a TemplateResponse for the index template with a permissive connect-src CSP * AND the front-end router resolves the remaining path client-side. * - * The router is hash-mode, so the client-resolvable deep link is the - * hash form. We open #/search cold and assert the Search page (not the - * Dashboard fallback) actually rendered. + * The router is HISTORY-mode, so the client-resolvable deep link is a + * plain path. We open /search cold and assert the Search page (not the + * Dashboard fallback) actually rendered — the Dashboard is exactly what + * appears when the router cannot resolve the path and the catch-all + * redirects to `/`, which is the regression this guards. */ test(// @e2e spa-deep-link-routing::open-a-deep-link-directly - 'SPA-001 — direct hash deep-link to /search renders the Search page, not the Dashboard', async ({ + 'SPA-001 — direct deep-link to /search renders the Search page, not the Dashboard', async ({ page, }) => { - await gotoHash(page, '/search') + await gotoRoute(page, '/search') // The genuine Search surface must mount from the deep link. await expect( page.locator('[data-testid="cn-search-page"]').first(), ).toBeVisible({ timeout: 20000 }) - // And the URL kept the deep-link route. + // And the URL kept the deep-link route. Matched as a suffix: Nextcloud + // serves the app as both `/apps/...` and `/index.php/apps/...`, and the + // router emits whichever base the page was loaded under. expect(page.url()).toContain('/apps/opencatalogi') - expect(page.url()).toContain('#/search') + expect(page.url()).toMatch(/\/apps\/opencatalogi\/search$/) }) }) @@ -587,7 +591,7 @@ test.describe('catalogs', () => { }) => { const cat = await resolveOrSeedCatalog(request) await bootApp(page) - await gotoHash(page, `/catalogi/${cat.id}`) + await gotoRoute(page, `/catalogi/${cat.id}`) await expect( page.locator('[data-testid="cn-detail-page"]').first(), ).toBeVisible({ timeout: 15000 }) @@ -606,7 +610,7 @@ test.describe('catalogs', () => { }) => { const cat = await resolveOrSeedCatalog(request) await bootApp(page) - await gotoHash(page, `/publications/${cat.slug}`) + await gotoRoute(page, `/publications/${cat.slug}`) // The Publications page is a manifest type:index page — its genuine // surface is cn-index-page (not the dashboard). await expect( @@ -1055,7 +1059,7 @@ test.describe('publications', () => { const errors = trackPageErrors(page) const cat = await resolveOrSeedCatalog(request) await bootApp(page) - await gotoHash(page, `/publications/${cat.slug}`) + await gotoRoute(page, `/publications/${cat.slug}`) await expect( page.locator('[data-testid="cn-index-page"]').first(), ).toBeVisible({ timeout: 15000 }) @@ -1079,7 +1083,7 @@ test.describe('publications', () => { const errors = trackPageErrors(page) const cat = await resolveOrSeedCatalog(request) await bootApp(page) - await gotoHash(page, `/publications/${cat.slug}`) + await gotoRoute(page, `/publications/${cat.slug}`) await expect( page.locator('[data-testid="cn-index-page"]').first(), ).toBeVisible({ timeout: 15000 })