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 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 = `/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; }); test.afterAll(async ({ request }) => { if (docId) await request.delete(`/api/documents/${docId}`); }); 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', exact: true })).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); }); }); // ── Notification bell — J10 ──────────────────────────────────────────────── // // Verifies the notification bell in the global header: clicking it opens the // dropdown and it closes on Escape. Full mark-as-read and navigation flows are // tracked in a follow-up issue. test.describe('Notification bell', () => { let bellDocId: string; test.beforeAll(async ({ request }) => { const stamp = Date.now().toString(36); // Seed a document + comment to ensure the notification list has content to render. const createRes = await request.post('/api/documents', { multipart: { title: `E2E Bell Test Doc ${stamp}`, documentDate: '1930-01-01' } }); if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); const doc = await createRes.json(); bellDocId = doc.id; const commentRes = await request.post(`/api/documents/${bellDocId}/comments`, { data: { content: 'Bell test comment' } }); if (!commentRes.ok()) throw new Error(`Create comment failed: ${commentRes.status()}`); }); test.afterAll(async ({ request }) => { if (bellDocId) await request.delete(`/api/documents/${bellDocId}`); }); test('bell opens dropdown, shows notifications list', async ({ page }) => { test.setTimeout(30_000); await page.goto('/'); await page.waitForSelector('[data-hydrated]'); // Click the notification bell button. const bell = page .locator('button[aria-label*="Benachrichtigungen"]') .or(page.locator('button[aria-label*="benachrichtigung"]')); await expect(bell.first()).toBeVisible({ timeout: 10_000 }); await bell.first().click(); // Dropdown / dialog opens. const dropdown = page .locator('[role="dialog"]') .or(page.locator('[data-testid="notification-dropdown"]')); await expect(dropdown.first()).toBeVisible({ timeout: 8_000 }); await page.screenshot({ path: 'test-results/e2e/notification-bell-open.png' }); // Close the dropdown (press Escape). await page.keyboard.press('Escape'); await expect(dropdown.first()).not.toBeVisible({ timeout: 5_000 }); }); });