Files
familienarchiv/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
Marcel 5387bc9247 feat(search): render removable theme chips in InterpretationChipRow
When tagsApplied is true, each resolvedTag renders as a 'Thema: Name'
chip with optional inline color style from the tag's resolved color.
Clicking × calls onRemoveChip('theme', tag.name).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:33:53 +02:00

212 lines
7.5 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.
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();
});
});