test(frontend): add Vitest specs for DocumentMetadataDrawer and TranscriptionBlock
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 53s
CI / Backend Unit Tests (pull_request) Failing after 58s
CI / E2E Tests (pull_request) Failing after 26s
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 53s
CI / Backend Unit Tests (pull_request) Failing after 58s
CI / E2E Tests (pull_request) Failing after 26s
DocumentMetadataDrawer (10 tests):
- Renders formatted date, dash for null date
- Renders location, dash for null location
- Renders translated status label
- Person cards as links to /persons/{id}
- Receiver links, empty state for no persons
- Tag chips as links, empty state for no tags
TranscriptionBlock (12 tests):
- Renders block number, text, optional label
- Save states: idle (nothing), saving (pulse), saved (checkmark), error (retry)
- Active turquoise border, error red border
- onTextChange fires on typing, onFocus fires on click
Fixes @Felix/@Sara: "Frontend component tests still missing"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import DocumentMetadataDrawer from './DocumentMetadataDrawer.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const sender = { id: 's1', firstName: 'Karl', lastName: 'Müller' };
|
||||
const receivers = [
|
||||
{ id: 'r1', firstName: 'Anna', lastName: 'Schmidt' },
|
||||
{ id: 'r2', firstName: 'Hans', lastName: 'Weber' }
|
||||
];
|
||||
const tags = [
|
||||
{ id: 't1', name: 'Familienbrief' },
|
||||
{ id: 't2', name: 'Kriegszeit' }
|
||||
];
|
||||
|
||||
function renderDrawer(overrides: Record<string, unknown> = {}) {
|
||||
return render(DocumentMetadataDrawer, {
|
||||
documentDate: '1942-03-15',
|
||||
location: 'Berlin',
|
||||
status: 'UPLOADED',
|
||||
sender,
|
||||
receivers,
|
||||
tags,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Details column ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('DocumentMetadataDrawer — details column', () => {
|
||||
it('renders formatted date', async () => {
|
||||
renderDrawer();
|
||||
await expect.element(page.getByText('15. März 1942')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dash when date is null', async () => {
|
||||
renderDrawer({ documentDate: null });
|
||||
const dds = page.getByText('—');
|
||||
await expect.element(dds.first()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders location', async () => {
|
||||
renderDrawer();
|
||||
await expect.element(page.getByText('Berlin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dash when location is null', async () => {
|
||||
renderDrawer({ location: null });
|
||||
const dashes = page.getByText('—');
|
||||
await expect.element(dashes.first()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders translated status label', async () => {
|
||||
renderDrawer();
|
||||
// "Hochgeladen" is the German translation of UPLOADED
|
||||
await expect.element(page.getByText('Hochgeladen')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Persons column ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('DocumentMetadataDrawer — persons column', () => {
|
||||
it('renders sender name as link to person detail', async () => {
|
||||
renderDrawer();
|
||||
const link = page.getByRole('link', { name: /Karl Müller/ });
|
||||
await expect.element(link).toBeInTheDocument();
|
||||
await expect.element(link).toHaveAttribute('href', '/persons/s1');
|
||||
});
|
||||
|
||||
it('renders receiver names as links', async () => {
|
||||
renderDrawer();
|
||||
const anna = page.getByRole('link', { name: /Anna Schmidt/ });
|
||||
await expect.element(anna).toHaveAttribute('href', '/persons/r1');
|
||||
const hans = page.getByRole('link', { name: /Hans Weber/ });
|
||||
await expect.element(hans).toHaveAttribute('href', '/persons/r2');
|
||||
});
|
||||
|
||||
it('shows empty state when no sender and no receivers', async () => {
|
||||
renderDrawer({ sender: null, receivers: [] });
|
||||
await expect.element(page.getByText('Keine Personen zugeordnet')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tags column ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('DocumentMetadataDrawer — tags column', () => {
|
||||
it('renders tag chips as links', async () => {
|
||||
renderDrawer();
|
||||
const fb = page.getByRole('link', { name: 'Familienbrief' });
|
||||
await expect.element(fb).toBeInTheDocument();
|
||||
await expect.element(fb).toHaveAttribute('href', '/?tag=Familienbrief');
|
||||
});
|
||||
|
||||
it('shows empty state when no tags', async () => {
|
||||
renderDrawer({ tags: [] });
|
||||
await expect.element(page.getByText('Keine Schlagwörter zugeordnet')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
114
frontend/src/lib/components/TranscriptionBlock.svelte.spec.ts
Normal file
114
frontend/src/lib/components/TranscriptionBlock.svelte.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import TranscriptionBlock from './TranscriptionBlock.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function renderBlock(overrides: Record<string, unknown> = {}) {
|
||||
return render(TranscriptionBlock, {
|
||||
blockId: 'block-1',
|
||||
blockNumber: 3,
|
||||
text: 'Liebe Mutter,',
|
||||
label: null,
|
||||
active: false,
|
||||
saveState: 'idle' as const,
|
||||
onTextChange: vi.fn(),
|
||||
onFocus: vi.fn(),
|
||||
onDeleteClick: vi.fn(),
|
||||
onRetry: vi.fn(),
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Rendering ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('TranscriptionBlock — rendering', () => {
|
||||
it('renders block number in turquoise badge', async () => {
|
||||
renderBlock();
|
||||
await expect.element(page.getByText('3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders text in textarea', async () => {
|
||||
renderBlock();
|
||||
const textarea = page.getByRole('textbox');
|
||||
await expect.element(textarea).toHaveValue('Liebe Mutter,');
|
||||
});
|
||||
|
||||
it('renders optional label when provided', async () => {
|
||||
renderBlock({ label: 'Anrede' });
|
||||
await expect.element(page.getByText('Anrede')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render label when null', async () => {
|
||||
renderBlock({ label: null });
|
||||
const label = page.getByText('Anrede');
|
||||
await expect.element(label).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Save states ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('TranscriptionBlock — save states', () => {
|
||||
it('shows nothing in idle state', async () => {
|
||||
renderBlock({ saveState: 'idle' });
|
||||
const saving = page.getByText('Speichere...');
|
||||
await expect.element(saving).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Speichere..." in saving state', async () => {
|
||||
renderBlock({ saveState: 'saving' });
|
||||
await expect.element(page.getByText('Speichere...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Gespeichert" in saved state', async () => {
|
||||
renderBlock({ saveState: 'saved' });
|
||||
await expect.element(page.getByText(/Gespeichert/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error with retry button in error state', async () => {
|
||||
const onRetry = vi.fn();
|
||||
renderBlock({ saveState: 'error', onRetry });
|
||||
await expect.element(page.getByText('Nicht gespeichert')).toBeInTheDocument();
|
||||
const retryBtn = page.getByText('Erneut versuchen');
|
||||
await expect.element(retryBtn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Active state ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('TranscriptionBlock — active border', () => {
|
||||
it('has turquoise left border when active', async () => {
|
||||
renderBlock({ active: true });
|
||||
await expect.element(page.getByRole('textbox')).toBeInTheDocument();
|
||||
const block = document.querySelector('[data-block-id="block-1"]')!;
|
||||
expect(block.className).toContain('border-turquoise');
|
||||
});
|
||||
|
||||
it('has error left border when save failed', async () => {
|
||||
renderBlock({ saveState: 'error' });
|
||||
await expect.element(page.getByRole('textbox')).toBeInTheDocument();
|
||||
const block = document.querySelector('[data-block-id="block-1"]')!;
|
||||
expect(block.className).toContain('border-error');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Interactions ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('TranscriptionBlock — interactions', () => {
|
||||
it('calls onTextChange when typing in textarea', async () => {
|
||||
const onTextChange = vi.fn();
|
||||
renderBlock({ onTextChange });
|
||||
const textarea = page.getByRole('textbox');
|
||||
await textarea.fill('Neue Zeile');
|
||||
expect(onTextChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onFocus when textarea is focused', async () => {
|
||||
const onFocus = vi.fn();
|
||||
renderBlock({ onFocus });
|
||||
const textarea = page.getByRole('textbox');
|
||||
await textarea.click();
|
||||
expect(onFocus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user