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); }); });