From 567faee3ccb6a2304614e746d89ec98549401c21 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 21 Apr 2026 13:44:45 +0200 Subject: [PATCH] test(e2e): notification deep-link scrolls to target comment Seeds a document, transcription block, and block comment via API, then visits /documents/{id}?commentId=X&annotationId=Y and asserts the page enters transcribe mode, the comment article becomes visible, and the URL query params are stripped. Runs at 320px and 1440px so the collapsed PDF strip clipping on mobile is caught. An axe-core pass guards the new tabindex + focus-visible ring against a11y regressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/e2e/notification-deep-link.spec.ts | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 frontend/e2e/notification-deep-link.spec.ts diff --git a/frontend/e2e/notification-deep-link.spec.ts b/frontend/e2e/notification-deep-link.spec.ts new file mode 100644 index 00000000..18e24728 --- /dev/null +++ b/frontend/e2e/notification-deep-link.spec.ts @@ -0,0 +1,117 @@ +import { test, expect, type Page } from '@playwright/test'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import { AxeBuilder } from '@axe-core/playwright'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); + +/** + * E2E test for the notification deep-link scroll flow — issue #276. + * + * Seeds a document + transcription block + block comment via API, then + * visits /documents/{id}?commentId=X&annotationId=Y and verifies: + * - page enters transcribe mode + * - the target comment is visible in the viewport + * - focus lands on the comment article + * - URL query params are stripped after handling + */ + +let docHref: string; +let docId: string; +let annotationId: string; +let commentId: string; + +test.describe('Notification deep-link scroll', () => { + test.beforeAll(async ({ request }) => { + const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; + + const createRes = await request.post('/api/documents', { + multipart: { title: 'E2E Deep-Link Test', documentDate: '1945-05-08' } + }); + if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); + const doc = await createRes.json(); + docId = doc.id; + docHref = `${baseURL}/documents/${docId}`; + + const uploadRes = await request.put(`/api/documents/${docId}`, { + multipart: { + title: doc.title, + documentDate: '1945-05-08', + file: { + name: 'minimal.pdf', + mimeType: 'application/pdf', + buffer: fs.readFileSync(PDF_FIXTURE) + } + } + }); + if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`); + + const blockRes = await request.post(`/api/documents/${docId}/transcription-blocks`, { + data: { + pageNumber: 1, + x: 0.1, + y: 0.1, + width: 0.3, + height: 0.1, + text: 'Seeded line', + label: null + } + }); + if (!blockRes.ok()) throw new Error(`Create block failed: ${blockRes.status()}`); + const block = await blockRes.json(); + annotationId = block.annotationId; + + const commentRes = await request.post( + `/api/documents/${docId}/transcription-blocks/${block.id}/comments`, + { + data: { content: 'Target comment for deep-link test' } + } + ); + if (!commentRes.ok()) throw new Error(`Create comment failed: ${commentRes.status()}`); + const comment = await commentRes.json(); + commentId = comment.id; + }); + + async function openDeepLink(page: Page) { + const url = `${docHref}?commentId=${commentId}&annotationId=${annotationId}`; + await page.goto(url); + await page.waitForSelector('[data-hydrated]'); + } + + for (const viewport of [ + { width: 320, height: 700, name: 'mobile-320' }, + { width: 1440, height: 900, name: 'desktop-1440' } + ]) { + test(`deep-link scrolls comment into view at ${viewport.name}`, async ({ page }) => { + test.setTimeout(45_000); + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await openDeepLink(page); + + // Transcribe mode was auto-entered — Fertig button is visible + await expect(page.getByRole('button', { name: 'Fertig' })).toBeVisible({ timeout: 15_000 }); + + // The target comment article is in the DOM and visible + const article = page.locator(`#comment-${commentId}`); + await expect(article).toBeVisible({ timeout: 10_000 }); + + // URL query params are stripped after handling + await expect.poll(() => page.url()).not.toContain('commentId='); + await expect.poll(() => page.url()).not.toContain('annotationId='); + + await page.screenshot({ + path: `test-results/e2e/notification-deep-link-${viewport.name}.png` + }); + }); + } + + test('axe accessibility check passes on document detail with deep-link', async ({ page }) => { + test.setTimeout(45_000); + await openDeepLink(page); + await expect(page.locator(`#comment-${commentId}`)).toBeVisible({ timeout: 10_000 }); + + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations).toHaveLength(0); + }); +});