diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte b/frontend/src/routes/search/InterpretationChipRow.svelte new file mode 100644 index 00000000..8e9cdbc0 --- /dev/null +++ b/frontend/src/routes/search/InterpretationChipRow.svelte @@ -0,0 +1,133 @@ + + +
+ {#each chips as chip (chip.key)} + {#if chip.type === 'directional'} + + {chip.from} + + {chip.to} + + + {:else} + + {chip.label} + + + {/if} + {/each} +
+ +{#if showKeywordsNotApplied} +

{m.smart_search_keywords_not_applied()}

+{/if} diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts new file mode 100644 index 00000000..f3164389 --- /dev/null +++ b/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts @@ -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 => ({ + 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(); + }); +});