Some checks failed
CI / Unit & Component Tests (push) Failing after 3m41s
CI / OCR Service Tests (push) Successful in 43s
CI / Backend Unit Tests (push) Failing after 3m30s
CI / Unit & Component Tests (pull_request) Failing after 3m32s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m17s
Three distinct root causes:
1. hilfe/transkription: Wikipedia link test was checking .textContent but
the accessible text had moved to aria-label in a prior commit.
2. documents/[id]/edit: vi.spyOn on a Svelte 5 compiled .svelte.ts service
object does not reliably track calls in vitest-browser mode; replaced
with a plain closure-based mock.
3. GeschichteEditor: TipTap's onMount steals focus and its ProseMirror
view interferes with Playwright CDP event dispatch. Three workarounds:
- blur: dispatchEvent(new FocusEvent('blur')) bypasses focus-state check
- save buttons: dispatchEvent(new MouseEvent('click')) from in-browser JS
context reliably triggers Svelte 5 onclick vs. Playwright CDP click
- trailing-space fill: input.value + dispatchEvent('input') works where
userEvent.fill('value ') silently fails to update bind:value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page, userEvent } from 'vitest/browser';
|
|
import GeschichteEditor from './GeschichteEditor.svelte';
|
|
|
|
const personFactory = (id: string, displayName: string) => ({
|
|
id,
|
|
firstName: displayName.split(' ')[0],
|
|
lastName: displayName.split(' ').slice(1).join(' ') || displayName,
|
|
displayName,
|
|
personType: 'PERSON' as const
|
|
});
|
|
|
|
const docFactory = (id: string, title: string, date = '1882-01-01') => ({
|
|
id,
|
|
title,
|
|
documentDate: date,
|
|
originalFilename: `${title}.pdf`,
|
|
status: 'UPLOADED' as const,
|
|
metadataComplete: false,
|
|
scriptType: 'UNKNOWN' as const,
|
|
createdAt: '2024-01-01T00:00:00',
|
|
updatedAt: '2024-01-01T00:00:00'
|
|
});
|
|
|
|
const draftFactory = (overrides: Record<string, unknown> = {}) => ({
|
|
id: 'g1',
|
|
title: 'Existing draft',
|
|
body: '<p>Hello world</p>',
|
|
status: 'DRAFT' as const,
|
|
persons: [],
|
|
documents: [],
|
|
createdAt: '2024-01-01T00:00:00',
|
|
updatedAt: '2024-01-01T00:00:00',
|
|
...overrides
|
|
});
|
|
|
|
afterEach(() => cleanup());
|
|
|
|
describe('GeschichteEditor — title-required guard', () => {
|
|
it('disables both DRAFT save buttons when the title is empty', async () => {
|
|
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
render(GeschichteEditor, { onSubmit });
|
|
|
|
const draft = await page.getByRole('button', { name: 'Entwurf speichern' }).element();
|
|
const publish = await page.getByRole('button', { name: 'Veröffentlichen' }).element();
|
|
expect(draft).toHaveProperty('disabled', true);
|
|
expect(publish).toHaveProperty('disabled', true);
|
|
});
|
|
|
|
it('shows the inline error after the title field is blurred while empty', async () => {
|
|
const onSubmit = vi.fn();
|
|
render(GeschichteEditor, { onSubmit });
|
|
|
|
await userEvent.click(page.getByPlaceholder('Titel der Geschichte'));
|
|
// userEvent.tab() / keyboard('{Tab}') do not reliably fire the blur event on
|
|
// inputs inside Playwright's test iframe. .blur() is a no-op when the element
|
|
// has lost focus to TipTap's onMount initialisation. Dispatching the FocusEvent
|
|
// directly fires Svelte's onblur listener regardless of the current focus owner.
|
|
const input = await page.getByPlaceholder('Titel der Geschichte').element();
|
|
input.dispatchEvent(new FocusEvent('blur'));
|
|
await expect.element(page.getByText('Bitte gib einen Titel ein.')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('GeschichteEditor — save bar adapts to status', () => {
|
|
it('renders DRAFT mode buttons when no geschichte prop is supplied', async () => {
|
|
render(GeschichteEditor, { onSubmit: vi.fn() });
|
|
await expect
|
|
.element(page.getByRole('button', { name: 'Entwurf speichern' }))
|
|
.toBeInTheDocument();
|
|
await expect.element(page.getByRole('button', { name: 'Veröffentlichen' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders PUBLISHED mode buttons when geschichte.status is PUBLISHED', async () => {
|
|
render(GeschichteEditor, {
|
|
geschichte: draftFactory({ status: 'PUBLISHED', publishedAt: '2024-04-01T12:00:00' }),
|
|
onSubmit: vi.fn()
|
|
});
|
|
await expect.element(page.getByRole('button', { name: 'Speichern' })).toBeInTheDocument();
|
|
await expect
|
|
.element(page.getByRole('button', { name: 'Zurück zu Entwurf' }))
|
|
.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('GeschichteEditor — pre-fill', () => {
|
|
it('renders initial persons as chips', async () => {
|
|
render(GeschichteEditor, {
|
|
initialPersons: [personFactory('p1', 'Franz Raddatz')],
|
|
onSubmit: vi.fn()
|
|
});
|
|
await expect.element(page.getByText('Franz Raddatz')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders initial documents as chips', async () => {
|
|
render(GeschichteEditor, {
|
|
initialDocuments: [docFactory('d1', 'Brief von Eugenie')],
|
|
onSubmit: vi.fn()
|
|
});
|
|
await expect.element(page.getByText(/Brief von Eugenie/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('populates the title input from a geschichte prop', async () => {
|
|
render(GeschichteEditor, {
|
|
geschichte: draftFactory({ title: 'My existing story' }),
|
|
onSubmit: vi.fn()
|
|
});
|
|
const input = await page.getByPlaceholder('Titel der Geschichte').element();
|
|
expect((input as HTMLInputElement).value).toBe('My existing story');
|
|
});
|
|
});
|
|
|
|
describe('GeschichteEditor — onSubmit payload', () => {
|
|
it('passes the trimmed title and DRAFT status when "Entwurf speichern" is clicked', async () => {
|
|
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
render(GeschichteEditor, { onSubmit });
|
|
|
|
// userEvent.fill() with trailing whitespace does not fire the input event chain
|
|
// that Svelte's bind:value requires (CDP limitation). Setting .value directly
|
|
// and dispatching an input event works around this while preserving the trailing
|
|
// space needed to verify the trim() contract.
|
|
const input = (await page
|
|
.getByPlaceholder('Titel der Geschichte')
|
|
.element()) as HTMLInputElement;
|
|
input.value = 'My title ';
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
// userEvent.click() via Playwright CDP does not reliably trigger Svelte 5 onclick
|
|
// handlers when a TipTap editor is mounted in the same component. Dispatching
|
|
// the MouseEvent directly from the browser JS context bypasses this issue.
|
|
const btn = (await page
|
|
.getByRole('button', { name: 'Entwurf speichern' })
|
|
.element()) as HTMLButtonElement;
|
|
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
|
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
const payload = onSubmit.mock.calls[0][0];
|
|
expect(payload.title).toBe('My title');
|
|
expect(payload.status).toBe('DRAFT');
|
|
});
|
|
|
|
it('passes status=PUBLISHED when "Veröffentlichen" is clicked', async () => {
|
|
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
render(GeschichteEditor, { onSubmit });
|
|
|
|
await userEvent.fill(page.getByPlaceholder('Titel der Geschichte'), 'Story');
|
|
const btn = (await page
|
|
.getByRole('button', { name: 'Veröffentlichen' })
|
|
.element()) as HTMLButtonElement;
|
|
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
|
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
expect(onSubmit.mock.calls[0][0].status).toBe('PUBLISHED');
|
|
});
|
|
|
|
it('passes the personIds and documentIds from initial props through onSubmit', async () => {
|
|
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
render(GeschichteEditor, {
|
|
initialPersons: [personFactory('p1', 'Franz Raddatz')],
|
|
initialDocuments: [docFactory('d1', 'Brief A')],
|
|
onSubmit
|
|
});
|
|
|
|
await userEvent.fill(page.getByPlaceholder('Titel der Geschichte'), 'Story');
|
|
const btn = (await page
|
|
.getByRole('button', { name: 'Entwurf speichern' })
|
|
.element()) as HTMLButtonElement;
|
|
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
|
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
const payload = onSubmit.mock.calls[0][0];
|
|
expect(payload.personIds).toEqual(['p1']);
|
|
expect(payload.documentIds).toEqual(['d1']);
|
|
});
|
|
});
|