diff --git a/frontend/src/routes/documents/theme-chip-removal.spec.ts b/frontend/src/routes/documents/theme-chip-removal.spec.ts deleted file mode 100644 index f39edec0..00000000 --- a/frontend/src/routes/documents/theme-chip-removal.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { buildThemeRemovalUrl } from './theme-chip-removal.js'; -import type { components } from '$lib/generated/api'; - -type NlQueryInterpretation = components['schemas']['NlQueryInterpretation']; - -function makeInterp(overrides: Partial = {}): NlQueryInterpretation { - return { - resolvedPersons: [], - ambiguousPersons: [], - keywords: [], - resolvedTags: [], - rawQuery: '', - keywordsApplied: false, - tagsApplied: true, - ...overrides - }; -} - -function makeTag(id: string, name: string, color?: string) { - return color ? { id, name, color } : { id, name }; -} - -describe('buildThemeRemovalUrl', () => { - it('N remaining tags → N tag params + tagOp=OR', () => { - const interp = makeInterp({ - resolvedTags: [ - makeTag('aaa', 'Hochzeit'), - makeTag('bbb', 'Weltkrieg'), - makeTag('ccc', 'Familie') - ] - }); - const url = buildThemeRemovalUrl(interp, 'Hochzeit'); - const params = new URL(url, 'http://x').searchParams; - expect(params.getAll('tag')).toEqual(['Weltkrieg', 'Familie']); - expect(params.get('tagOp')).toBe('OR'); - }); - - it('last tag removed → no tag or tagOp params in URL', () => { - const interp = makeInterp({ - resolvedTags: [makeTag('aaa', 'Hochzeit')] - }); - const url = buildThemeRemovalUrl(interp, 'Hochzeit'); - const params = new URL(url, 'http://x').searchParams; - expect(params.getAll('tag')).toEqual([]); - expect(params.get('tagOp')).toBeNull(); - }); - - it('last tag removed with resolved sender person → sender param intact', () => { - const interp = makeInterp({ - resolvedPersons: [{ id: '11111111-1111-1111-1111-111111111111', displayName: 'Walter' }], - resolvedTags: [makeTag('aaa', 'Hochzeit')] - }); - const url = buildThemeRemovalUrl(interp, 'Hochzeit'); - const params = new URL(url, 'http://x').searchParams; - expect(params.get('senderId')).toBe('11111111-1111-1111-1111-111111111111'); - expect(params.getAll('tag')).toEqual([]); - expect(params.get('tagOp')).toBeNull(); - }); - - it('null-color tag → tag name emitted correctly; color does not affect params', () => { - const interp = makeInterp({ - resolvedTags: [makeTag('aaa', 'Erbschaft'), makeTag('bbb', 'Migration')] - }); - const url = buildThemeRemovalUrl(interp, 'Erbschaft'); - const params = new URL(url, 'http://x').searchParams; - expect(params.getAll('tag')).toEqual(['Migration']); - expect(params.get('tagOp')).toBe('OR'); - }); - - it('directional pair → senderId and receiverId both emitted', () => { - const interp = makeInterp({ - resolvedPersons: [ - { id: '11111111-1111-1111-1111-111111111111', displayName: 'Walter' }, - { id: '22222222-2222-2222-2222-222222222222', displayName: 'Emma' } - ], - resolvedTags: [makeTag('aaa', 'Krieg'), makeTag('bbb', 'Heimat')] - }); - const url = buildThemeRemovalUrl(interp, 'Krieg'); - const params = new URL(url, 'http://x').searchParams; - expect(params.get('senderId')).toBe('11111111-1111-1111-1111-111111111111'); - expect(params.get('receiverId')).toBe('22222222-2222-2222-2222-222222222222'); - expect(params.getAll('tag')).toEqual(['Heimat']); - }); -}); diff --git a/frontend/src/routes/documents/theme-chip-removal.ts b/frontend/src/routes/documents/theme-chip-removal.ts deleted file mode 100644 index 21e7a511..00000000 --- a/frontend/src/routes/documents/theme-chip-removal.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { components } from '$lib/generated/api'; - -type NlQueryInterpretation = components['schemas']['NlQueryInterpretation']; - -export function buildThemeRemovalUrl( - interp: NlQueryInterpretation, - removedTagName: string -): string { - const remaining = interp.resolvedTags.filter((t) => t.name !== removedTagName); - const params = new URLSearchParams(); - - const resolved = interp.resolvedPersons; - if (resolved.length >= 1) params.set('senderId', resolved[0].id); - if (resolved.length >= 2) params.set('receiverId', resolved[1].id); - if (interp.dateFrom) params.set('from', interp.dateFrom); - if (interp.dateTo) params.set('to', interp.dateTo); - if (interp.keywordsApplied && interp.keywords.length > 0) { - params.set('q', interp.keywords.join(' ')); - } - - remaining.forEach((tag) => params.append('tag', tag.name)); - if (remaining.length > 0) params.set('tagOp', 'OR'); - - const qs = params.toString(); - return qs ? `/documents?${qs}` : '/documents'; -} diff --git a/frontend/src/routes/search/DisambiguationPicker.svelte b/frontend/src/routes/search/DisambiguationPicker.svelte deleted file mode 100644 index c99619c9..00000000 --- a/frontend/src/routes/search/DisambiguationPicker.svelte +++ /dev/null @@ -1,102 +0,0 @@ - - - - -
open && closePicker()}> - - - {#if open} -
-

{heading}

-
    - {#each persons as person (person.id)} -
  • - -
  • - {/each} -
-
- {/if} -
diff --git a/frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts b/frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts deleted file mode 100644 index 04eac3dd..00000000 --- a/frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { describe, expect, it, vi, afterEach } from 'vitest'; -import { cleanup, render } from 'vitest-browser-svelte'; -import { page } from 'vitest/browser'; -import DisambiguationPicker from './DisambiguationPicker.svelte'; -import type { components } from '$lib/generated/api'; - -type PersonHint = components['schemas']['PersonHint']; - -afterEach(() => cleanup()); - -const persons: PersonHint[] = [ - { id: 'w1', displayName: 'Walter Raddatz' }, - { id: 'w2', displayName: 'Walter Müller' } -]; - -const multiProps = { persons, heading: 'Person auswählen', showCue: true }; - -function pressEscape() { - (document.activeElement as HTMLElement).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }) - ); -} - -describe('DisambiguationPicker', () => { - it('opens the picker and shows a select option per ambiguous person', async () => { - render(DisambiguationPicker, { ...multiProps, onSelect: vi.fn() }); - await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click(); - await expect - .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' })) - .toBeInTheDocument(); - await expect - .element(page.getByRole('button', { name: 'Walter Müller auswählen' })) - .toBeInTheDocument(); - }); - - it('moves focus into the picker list on open', async () => { - render(DisambiguationPicker, { ...multiProps, onSelect: vi.fn() }); - await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click(); - await expect - .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' })) - .toHaveFocus(); - }); - - it('returns focus to the trigger when closed with Escape', async () => { - render(DisambiguationPicker, { ...multiProps, onSelect: vi.fn() }); - const trigger = page.getByRole('button', { name: /Mehrere Personen gefunden/ }); - await trigger.click(); - await expect - .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' })) - .toHaveFocus(); - pressEscape(); - await expect.element(trigger).toHaveFocus(); - }); - - it('does not call onSelect when dismissed without choosing', async () => { - const onSelect = vi.fn(); - render(DisambiguationPicker, { ...multiProps, onSelect }); - await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click(); - await expect - .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' })) - .toHaveFocus(); - pressEscape(); - expect(onSelect).not.toHaveBeenCalled(); - }); - - it('calls onSelect with the chosen person', async () => { - const onSelect = vi.fn(); - render(DisambiguationPicker, { ...multiProps, onSelect }); - await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click(); - await page.getByRole('button', { name: 'Walter Müller auswählen' }).click(); - expect(onSelect).toHaveBeenCalledWith(persons[1]); - }); - - it('renders the supplied heading as a visible panel heading', async () => { - render(DisambiguationPicker, { - persons: [{ id: 'c1', displayName: 'Clara Cramer' }], - heading: 'Meintest du Clara Cramer?', - showCue: false, - onSelect: vi.fn() - }); - await page.getByRole('button', { name: 'Meintest du Clara Cramer?' }).click(); - await expect.element(page.getByText('Meintest du Clara Cramer?')).toBeVisible(); - }); - - it('suppresses the cue when showCue is false', async () => { - render(DisambiguationPicker, { - persons: [{ id: 'c1', displayName: 'Clara Cramer' }], - heading: 'Meintest du Clara Cramer?', - showCue: false, - onSelect: vi.fn() - }); - await expect.element(page.getByText('(auswählen…)')).not.toBeInTheDocument(); - }); - - it('shows the cue when showCue is true', async () => { - render(DisambiguationPicker, { ...multiProps, onSelect: vi.fn() }); - await expect.element(page.getByText('(auswählen…)')).toBeVisible(); - }); - - it('announces the did-you-mean heading as the trigger accessible name for a single suggestion', async () => { - render(DisambiguationPicker, { - persons: [{ id: 'c1', displayName: 'Clara Cramer' }], - heading: 'Meintest du Clara Cramer?', - showCue: false, - onSelect: vi.fn() - }); - await expect - .element(page.getByRole('button', { name: 'Meintest du Clara Cramer?' })) - .toBeInTheDocument(); - }); - - it('keeps the multiple-people trigger accessible name for two or more suggestions', async () => { - render(DisambiguationPicker, { ...multiProps, onSelect: vi.fn() }); - await expect - .element(page.getByRole('button', { name: /Mehrere Personen gefunden/ })) - .toBeInTheDocument(); - }); -}); diff --git a/frontend/src/routes/search/InterpretationChipRow.svelte b/frontend/src/routes/search/InterpretationChipRow.svelte deleted file mode 100644 index 2356a774..00000000 --- a/frontend/src/routes/search/InterpretationChipRow.svelte +++ /dev/null @@ -1,181 +0,0 @@ - - -
- {#each chips as chip (chip.key)} - {#if chip.type === 'directional'} - - {chip.from} - - {chip.to} - - - {:else if chip.type === 'theme'} - - {m.search_chip_theme_prefix()}: - {chip.tag.name} - - - {: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 deleted file mode 100644 index 6242d16b..00000000 --- a/frontend/src/routes/search/InterpretationChipRow.svelte.spec.ts +++ /dev/null @@ -1,214 +0,0 @@ -// 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 => ({ - 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: 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(); - }); - - // ── 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(); - }); -}); diff --git a/frontend/src/routes/search/SmartModeToggle.svelte b/frontend/src/routes/search/SmartModeToggle.svelte deleted file mode 100644 index a7155bcc..00000000 --- a/frontend/src/routes/search/SmartModeToggle.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/frontend/src/routes/search/SmartModeToggle.svelte.spec.ts b/frontend/src/routes/search/SmartModeToggle.svelte.spec.ts deleted file mode 100644 index 1661a50e..00000000 --- a/frontend/src/routes/search/SmartModeToggle.svelte.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expect, it, vi, afterEach } from 'vitest'; -import { cleanup, render } from 'vitest-browser-svelte'; -import { page } from 'vitest/browser'; -import SmartModeToggle from './SmartModeToggle.svelte'; -import SearchFilterBar from '../SearchFilterBar.svelte'; - -afterEach(() => cleanup()); - -const SEARCH_PLACEHOLDER = 'Titel, Personen, Tags durchsuchen…'; - -describe('SmartModeToggle', () => { - it('renders aria-pressed="false" by default and toggles on click', async () => { - render(SmartModeToggle, { smartMode: false }); - const btn = page.getByRole('button'); - await expect.element(btn).toHaveAttribute('aria-pressed', 'false'); - await btn.click(); - await expect.element(btn).toHaveAttribute('aria-pressed', 'true'); - await btn.click(); - await expect.element(btn).toHaveAttribute('aria-pressed', 'false'); - }); - - it('shows the smart label when smartMode is true', async () => { - render(SmartModeToggle, { smartMode: true }); - const btn = page.getByRole('button'); - await expect.element(btn).toHaveTextContent('Smart'); - }); - - it('shows the keyword label when smartMode is false', async () => { - render(SmartModeToggle, { smartMode: false }); - const btn = page.getByRole('button'); - await expect.element(btn).toHaveTextContent('Text'); - }); - - it('applies the active pill style only in smart mode', async () => { - render(SmartModeToggle, { smartMode: true }); - const btn = page.getByRole('button'); - await expect.element(btn).toHaveClass(/bg-primary/); - }); -}); - -describe('SmartModeToggle inside SearchFilterBar', () => { - it('adds maxlength="500" to the search input only in smart mode', async () => { - render(SearchFilterBar, { onSearch: vi.fn(), sort: 'DATE', dir: 'desc', smartMode: true }); - await expect - .element(page.getByPlaceholder(SEARCH_PLACEHOLDER)) - .toHaveAttribute('maxlength', '500'); - }); - - it('omits maxlength from the search input in keyword mode', async () => { - render(SearchFilterBar, { onSearch: vi.fn(), sort: 'DATE', dir: 'desc', smartMode: false }); - await expect - .element(page.getByPlaceholder(SEARCH_PLACEHOLDER)) - .not.toHaveAttribute('maxlength'); - }); - - it('does not fire the keyword search on input while in smart mode', async () => { - const onSearch = vi.fn(); - render(SearchFilterBar, { onSearch, sort: 'DATE', dir: 'desc', smartMode: true }); - await page.getByPlaceholder(SEARCH_PLACEHOLDER).fill('Walter im Krieg'); - expect(onSearch).not.toHaveBeenCalled(); - }); - - it('fires the smart search callback on Enter in smart mode', async () => { - const onSmartSearch = vi.fn(); - render(SearchFilterBar, { - onSearch: vi.fn(), - onSmartSearch, - sort: 'DATE', - dir: 'desc', - smartMode: true - }); - const input = page.getByPlaceholder(SEARCH_PLACEHOLDER); - await input.fill('Walter im Krieg'); - await input.click(); - // Enter submits the NL query in smart mode - (document.activeElement as HTMLElement).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) - ); - await vi.waitFor(() => expect(onSmartSearch).toHaveBeenCalled()); - }); -}); diff --git a/frontend/src/routes/search/SmartSearchStatus.svelte b/frontend/src/routes/search/SmartSearchStatus.svelte deleted file mode 100644 index d6444a7e..00000000 --- a/frontend/src/routes/search/SmartSearchStatus.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - -{#if status === 'loading'} -
- -

{m.search_loading_nl()}

-

- {m.search_loading_nl_sub()} -

-
-{:else if status === 'error'} - -{/if} diff --git a/frontend/src/routes/search/SmartSearchStatus.svelte.spec.ts b/frontend/src/routes/search/SmartSearchStatus.svelte.spec.ts deleted file mode 100644 index 605576d0..00000000 --- a/frontend/src/routes/search/SmartSearchStatus.svelte.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, it, vi, afterEach } from 'vitest'; -import { cleanup, render } from 'vitest-browser-svelte'; -import { page } from 'vitest/browser'; -import SmartSearchStatus from './SmartSearchStatus.svelte'; - -afterEach(() => { - cleanup(); - vi.restoreAllMocks(); -}); - -describe('SmartSearchStatus', () => { - it('renders a role="status" loading panel with the loading title', async () => { - render(SmartSearchStatus, { status: 'loading' }); - const status = page.getByRole('status'); - await expect.element(status).toBeInTheDocument(); - await expect.element(status).toHaveTextContent('Archiv wird befragt'); - }); - - it('hides the loading panel once the status changes away from loading', async () => { - const { rerender } = render(SmartSearchStatus, { status: 'loading' }); - await expect.element(page.getByRole('status')).toBeInTheDocument(); - await rerender({ status: 'error', errorCode: 'SMART_SEARCH_UNAVAILABLE' }); - await expect.element(page.getByRole('status')).not.toBeInTheDocument(); - }); - - it('renders the 503 panel with title, body and a switch-to-keyword button', async () => { - render(SmartSearchStatus, { - status: 'error', - errorCode: 'SMART_SEARCH_UNAVAILABLE', - onSwitchToKeyword: vi.fn() - }); - await expect.element(page.getByText('Intelligente Suche nicht verfügbar')).toBeInTheDocument(); - await expect - .element(page.getByRole('button', { name: /Volltextsuche wechseln/ })) - .toBeInTheDocument(); - }); - - it('invokes onSwitchToKeyword when the 503 fallback button is clicked', async () => { - const onSwitchToKeyword = vi.fn(); - render(SmartSearchStatus, { - status: 'error', - errorCode: 'SMART_SEARCH_UNAVAILABLE', - onSwitchToKeyword - }); - await page.getByRole('button', { name: /Volltextsuche wechseln/ }).click(); - expect(onSwitchToKeyword).toHaveBeenCalledOnce(); - }); - - it('renders the 429 panel with title and body but no switch-to-keyword button', async () => { - render(SmartSearchStatus, { - status: 'error', - errorCode: 'SMART_SEARCH_RATE_LIMITED', - onSwitchToKeyword: vi.fn() - }); - await expect.element(page.getByText('Zu viele Anfragen')).toBeInTheDocument(); - await expect - .element(page.getByRole('button', { name: /Volltextsuche wechseln/ })) - .not.toBeInTheDocument(); - }); -}); diff --git a/frontend/src/routes/search/chip-types.ts b/frontend/src/routes/search/chip-types.ts deleted file mode 100644 index d78c58b8..00000000 --- a/frontend/src/routes/search/chip-types.ts +++ /dev/null @@ -1 +0,0 @@ -export type ChipType = 'sender' | 'directional' | 'date' | 'keyword' | 'theme';