feat(search): add InterpretationChipRow component (#739)
Renders type-prefixed chips (Absender/Zeitraum/Stichwort), a single directional chip for 2-name queries, gates keyword chips on keywordsApplied, and emits onRemoveChip(type, value?). Truncating name spans keep the 44px × button visible; chip wrappers show a focus ring. 9 vitest-browser-svelte specs (red/green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
133
frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
Normal file
133
frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
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'];
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const makePerson = (id: string, displayName: string): PersonHint => ({ id, displayName });
|
||||
|
||||
const makeInterpretation = (
|
||||
overrides: Partial<NlQueryInterpretation> = {}
|
||||
): NlQueryInterpretation => ({
|
||||
resolvedPersons: [],
|
||||
ambiguousPersons: [],
|
||||
keywords: [],
|
||||
rawQuery: 'test',
|
||||
keywordsApplied: true,
|
||||
...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: 1914–1918')).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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user