Files
familienarchiv/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
Marcel fb41affd4c
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m22s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m47s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
docs(search): note vitest-browser workaround for + in path
Addresses @Sara review: browser tests in this spec fail silently when
the project path contains '+' (common in git worktrees). The comment
tells developers to copy the frontend directory to a clean path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:58:36 +02:00

215 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// NOTE: vitest-browser fails silently when the project path contains '+' (common in git worktrees
// named 'feat+issue-NNN-slug'). If tests fail with iframe routing errors, copy the frontend
// directory to a path without '+' (e.g. /tmp/fe-copy) and run the suite from there.
import { describe, expect, it, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import InterpretationChipRow from './InterpretationChipRow.svelte';
import type { components } from '$lib/generated/api';
type NlQueryInterpretation = components['schemas']['NlQueryInterpretation'];
type PersonHint = components['schemas']['PersonHint'];
type TagHint = components['schemas']['TagHint'];
afterEach(() => cleanup());
const makePerson = (id: string, displayName: string): PersonHint => ({ id, displayName });
const makeInterpretation = (
overrides: Partial<NlQueryInterpretation> = {}
): NlQueryInterpretation => ({
resolvedPersons: [],
ambiguousPersons: [],
keywords: [],
resolvedTags: [],
rawQuery: 'test',
keywordsApplied: true,
tagsApplied: false,
...overrides
});
describe('InterpretationChipRow', () => {
it('renders type-prefixed labels for sender, date and keyword chips', async () => {
render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedPersons: [makePerson('p1', 'Walter Raddatz')],
dateFrom: '1914-01-01',
dateTo: '1918-12-31',
keywords: ['krieg']
}),
onRemoveChip: vi.fn()
});
await expect.element(page.getByText('Absender: Walter Raddatz')).toBeInTheDocument();
await expect.element(page.getByText('Zeitraum: 19141918')).toBeInTheDocument();
await expect.element(page.getByText('Stichwort: krieg')).toBeInTheDocument();
});
it('calls onRemoveChip with "sender" when the sender chip × is clicked', async () => {
const onRemoveChip = vi.fn();
render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedPersons: [makePerson('p1', 'Walter Raddatz')]
}),
onRemoveChip
});
await page.getByRole('button', { name: /Absender: Walter Raddatz/ }).click();
expect(onRemoveChip).toHaveBeenCalledWith('sender', undefined);
});
it('removes a chip from the DOM but keeps the rest when one × is clicked', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedPersons: [makePerson('p1', 'Walter Raddatz')],
dateFrom: '1914-01-01',
dateTo: '1918-12-31',
keywords: ['krieg']
}),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type]')).toHaveLength(3);
await page.getByRole('button', { name: /Absender/ }).click();
await vi.waitFor(() => expect(container.querySelectorAll('[data-chip-type]')).toHaveLength(2));
});
it('renders a single directional chip with an arrow for a 2-name query', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedPersons: [makePerson('p1', 'Walter Raddatz'), makePerson('p2', 'Emma Raddatz')]
}),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type="directional"]')).toHaveLength(1);
await expect.element(page.getByText(/→/)).toBeInTheDocument();
});
it('calls onRemoveChip with "directional" when the directional chip × is clicked', async () => {
const onRemoveChip = vi.fn();
render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedPersons: [makePerson('p1', 'Walter Raddatz'), makePerson('p2', 'Emma Raddatz')]
}),
onRemoveChip
});
await page.getByRole('button', { name: /Walter Raddatz/ }).click();
expect(onRemoveChip).toHaveBeenCalledWith('directional', undefined);
});
it('does not render keyword chips when keywordsApplied is false', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
keywordsApplied: false,
keywords: ['krieg', 'brief']
}),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type="keyword"]')).toHaveLength(0);
});
it('renders no keyword chips when keywords is empty', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({ keywordsApplied: true, keywords: [] }),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type="keyword"]')).toHaveLength(0);
});
it('renders exactly one keyword chip per keyword', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
keywordsApplied: true,
keywords: ['krieg', 'brief', 'front']
}),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type="keyword"]')).toHaveLength(3);
});
it('keeps the × button in the DOM when a display name is 100 characters', async () => {
const longName = 'W'.repeat(100);
render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedPersons: [makePerson('p1', longName)]
}),
onRemoveChip: vi.fn()
});
await expect
.element(page.getByRole('button', { name: new RegExp('Absender') }))
.toBeInTheDocument();
});
// ── theme chips ─────────────────────────────────────────────────────────────
const makeTag = (id: string, name: string, color?: string): TagHint => ({ id, name, color });
it('renders theme chips when tagsApplied is true', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedTags: [makeTag('t1', 'Hochzeit')],
tagsApplied: true
}),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(1);
await expect.element(page.getByText(/Thema: Hochzeit/)).toBeInTheDocument();
});
it('renders no theme chips when tagsApplied is false', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedTags: [makeTag('t1', 'Hochzeit')],
tagsApplied: false
}),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(0);
});
it('renders exactly N theme chips for N resolved tags', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedTags: [makeTag('t1', 'Krieg'), makeTag('t2', 'Hochzeit'), makeTag('t3', 'Familie')],
tagsApplied: true
}),
onRemoveChip: vi.fn()
});
expect(container.querySelectorAll('[data-chip-type="theme"]')).toHaveLength(3);
});
it('calls onRemoveChip with "theme" and tag name when × is clicked', async () => {
const onRemoveChip = vi.fn();
render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedTags: [makeTag('t1', 'Hochzeit')],
tagsApplied: true
}),
onRemoveChip
});
await page.getByRole('button', { name: /Thema: Hochzeit/ }).click();
expect(onRemoveChip).toHaveBeenCalledWith('theme', 'Hochzeit');
});
it('applies inline color style for a tag with a color', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedTags: [makeTag('t1', 'Hochzeit', 'sage')],
tagsApplied: true
}),
onRemoveChip: vi.fn()
});
const chip = container.querySelector('[data-chip-type="theme"]') as HTMLElement;
expect(chip.style.backgroundColor).toBeTruthy();
});
it('omits color style for a tag with no color', async () => {
const { container } = render(InterpretationChipRow, {
interpretation: makeInterpretation({
resolvedTags: [makeTag('t1', 'Hochzeit')],
tagsApplied: true
}),
onRemoveChip: vi.fn()
});
const chip = container.querySelector('[data-chip-type="theme"]') as HTMLElement;
expect(chip.getAttribute('style')).toBeFalsy();
});
});