bug: notification deep-link does not scroll to comment on document detail page #299

Merged
marcel merged 8 commits from feat/issue-276-notification-deep-link-scroll into main 2026-04-21 15:06:02 +02:00
Showing only changes of commit 567faee3cc - Show all commits

View File

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