test(e2e): person-mention read mode hover (B20) and tap (B21)
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m22s
CI / OCR Service Tests (push) Successful in 49s
CI / Backend Unit Tests (push) Failing after 3m9s
CI / Unit & Component Tests (pull_request) Failing after 3m15s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m0s
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m22s
CI / OCR Service Tests (push) Successful in 49s
CI / Backend Unit Tests (push) Failing after 3m9s
CI / Unit & Component Tests (pull_request) Failing after 3m15s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m0s
Creates a Person, document, annotation, and transcription block with
mentionedPersons sidecar, then exercises the read-mode link in two
contexts:
- Desktop: page.hover() mounts the hover card; mouseleave unmounts.
- Touch (Pixel 7 device): page.tap() navigates to /persons/{id}
without the card ever mounting (tap opens the page directly).
Tests are sequential because they share a single document/person via
beforeAll/afterAll. The touch test spins up a separate browser context
with hasTouch=true reusing the stored auth state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
154
frontend/e2e/person-mention-read.spec.ts
Normal file
154
frontend/e2e/person-mention-read.spec.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { test, expect, devices } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf');
|
||||
const STORAGE_STATE = path.resolve(__dirname, '.auth/user.json');
|
||||
|
||||
/**
|
||||
* E2E for issue #362 — Person @mentions, read-mode rendering + hover card (B20/B21).
|
||||
*
|
||||
* Strategy:
|
||||
* - Create a document, a Person, and a transcription block whose text contains
|
||||
* `@DisplayName` and whose mentionedPersons sidecar links to that person.
|
||||
* - Open the document in read mode.
|
||||
* - B20: page.hover() on the .person-mention link → hover card mounts.
|
||||
* - B21: with context.setHasTouch(true), page.tap() on the link → navigates
|
||||
* to /persons/{id} without ever showing the hover card.
|
||||
*/
|
||||
|
||||
let docId: string;
|
||||
let personId: string;
|
||||
let docHref: string;
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.describe('Person mention — read mode', () => {
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
|
||||
// 1. Person we will mention.
|
||||
const personRes = await request.post('/api/persons', {
|
||||
data: {
|
||||
firstName: 'Auguste',
|
||||
lastName: 'Raddatz',
|
||||
personType: 'PERSON',
|
||||
birthYear: 1882,
|
||||
deathYear: 1944
|
||||
}
|
||||
});
|
||||
if (!personRes.ok()) throw new Error(`Create person failed: ${personRes.status()}`);
|
||||
const person = await personRes.json();
|
||||
personId = person.id;
|
||||
|
||||
// 2. Document with a PDF so the transcription panel is mountable.
|
||||
const docRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E Person Mention Read', documentDate: '1945-05-08' }
|
||||
});
|
||||
if (!docRes.ok()) throw new Error(`Create document failed: ${docRes.status()}`);
|
||||
const doc = await docRes.json();
|
||||
docId = doc.id;
|
||||
docHref = `${baseURL}/documents/${docId}`;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Annotation to anchor the block on the page.
|
||||
const annRes = await request.post(`/api/documents/${docId}/annotations`, {
|
||||
data: { pageNumber: 1, x: 0.1, y: 0.1, width: 0.5, height: 0.1, color: '#00C7B1' }
|
||||
});
|
||||
if (!annRes.ok()) throw new Error(`Create annotation failed: ${annRes.status()}`);
|
||||
|
||||
// 4. Block text contains @Auguste Raddatz; sidecar links it to personId.
|
||||
const blockRes = await request.post(`/api/documents/${docId}/transcription-blocks`, {
|
||||
data: {
|
||||
pageNumber: 1,
|
||||
x: 0.1,
|
||||
y: 0.1,
|
||||
width: 0.5,
|
||||
height: 0.1,
|
||||
text: 'Brief an @Auguste Raddatz vom Mai 1944',
|
||||
label: null,
|
||||
mentionedPersons: [{ personId, displayName: 'Auguste Raddatz' }]
|
||||
}
|
||||
});
|
||||
if (!blockRes.ok()) throw new Error(`Create block failed: ${blockRes.status()}`);
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
if (docId) await request.delete(`/api/documents/${docId}`);
|
||||
if (personId) await request.delete(`/api/persons/${personId}`);
|
||||
});
|
||||
|
||||
test('renders the @mention as an underlined anchor link to /persons/{id}', async ({ page }) => {
|
||||
await page.goto(docHref);
|
||||
await page.getByRole('button', { name: 'Transkription' }).click();
|
||||
|
||||
const link = page.locator(`a.person-mention[data-person-id="${personId}"]`).first();
|
||||
await expect(link).toBeVisible({ timeout: 5000 });
|
||||
await expect(link).toHaveAttribute('href', `/persons/${personId}`);
|
||||
// The @ trigger is stripped from the rendered text per spec
|
||||
await expect(link).toHaveText('Auguste Raddatz');
|
||||
});
|
||||
|
||||
test('B20: desktop hover mounts the hover card with loaded person data', async ({ page }) => {
|
||||
await page.goto(docHref);
|
||||
await page.getByRole('button', { name: 'Transkription' }).click();
|
||||
|
||||
const link = page.locator(`a.person-mention[data-person-id="${personId}"]`).first();
|
||||
await link.hover();
|
||||
|
||||
const card = page.getByTestId('person-hover-card');
|
||||
await expect(card).toBeVisible({ timeout: 5000 });
|
||||
// Loaded state: person displayName is rendered inside the card
|
||||
await expect(page.getByTestId('person-hover-card-name')).toHaveText('Auguste Raddatz');
|
||||
// Footer link points to /persons/{id}
|
||||
await expect(card.locator(`a[href="/persons/${personId}"]`)).toBeVisible();
|
||||
});
|
||||
|
||||
test('B20: hover card unmounts on mouseleave', async ({ page }) => {
|
||||
await page.goto(docHref);
|
||||
await page.getByRole('button', { name: 'Transkription' }).click();
|
||||
|
||||
const link = page.locator(`a.person-mention[data-person-id="${personId}"]`).first();
|
||||
await link.hover();
|
||||
await expect(page.getByTestId('person-hover-card')).toBeVisible();
|
||||
|
||||
// Move pointer away
|
||||
await page.mouse.move(0, 0);
|
||||
await expect(page.getByTestId('person-hover-card')).toBeHidden({ timeout: 2000 });
|
||||
});
|
||||
|
||||
test('B21: touch-device tap navigates without showing the hover card', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
...devices['Pixel 7'],
|
||||
storageState: STORAGE_STATE
|
||||
});
|
||||
const touchPage = await context.newPage();
|
||||
try {
|
||||
await touchPage.goto(docHref);
|
||||
await touchPage.getByRole('button', { name: 'Transkription' }).click();
|
||||
|
||||
const link = touchPage.locator(`a.person-mention[data-person-id="${personId}"]`).first();
|
||||
await expect(link).toBeVisible({ timeout: 5000 });
|
||||
|
||||
await link.tap();
|
||||
// The card never mounted — the tap navigated directly per spec
|
||||
await expect(touchPage).toHaveURL(new RegExp(`/persons/${personId}`));
|
||||
await expect(touchPage.getByTestId('person-hover-card')).toHaveCount(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user