feat(geschichten): add DocumentFilterChip component with spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-12 09:12:37 +02:00
parent 03e0dae5aa
commit dcc9a25fdc
3 changed files with 125 additions and 0 deletions

3
frontend/.gitignore vendored
View File

@@ -13,6 +13,9 @@ node_modules
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Leftover directory from branch work
/src.main/
# Env # Env
.env .env
.env.* .env.*

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
let {
id,
title,
onremove
}: {
id: string;
title: string | null;
onremove: () => void;
} = $props();
const chipLabel = $derived(title ?? id.slice(0, 8));
</script>
<div
class="inline-flex min-h-11 items-center gap-1.5 rounded-full border border-primary bg-primary px-3 text-primary-fg"
aria-live="polite"
>
<span class="font-sans text-xs tracking-wider whitespace-nowrap uppercase">
{m.geschichten_filter_document_chip()}
</span>
<span class="line-clamp-2 font-serif italic sm:max-w-[16rem] sm:truncate" title={chipLabel}>
{chipLabel}
</span>
<button
type="button"
onclick={onremove}
aria-label={m.geschichten_filter_remove_document_chip({ title: chipLabel })}
class="ml-0.5 flex min-h-[44px] min-w-[44px] items-center justify-center rounded-full focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:outline-none"
>
<span aria-hidden="true">×</span>
</button>
</div>

View File

@@ -0,0 +1,87 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
import DocumentFilterChip from './DocumentFilterChip.svelte';
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const VALID_UUID = '11111111-2222-3333-4444-555555555555';
describe('DocumentFilterChip', () => {
it('renders the resolved document title inside the chip', async () => {
render(DocumentFilterChip, {
props: {
id: VALID_UUID,
title: 'Brief an Oma',
onremove: vi.fn()
}
});
await expect.element(page.getByText(/Brief an Oma/)).toBeVisible();
});
it('renders the prefix label', async () => {
render(DocumentFilterChip, {
props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
});
await expect.element(page.getByText(/Gefiltert nach Brief/)).toBeVisible();
});
it('falls back to short UUID when title is null', async () => {
render(DocumentFilterChip, {
props: { id: VALID_UUID, title: null, onremove: vi.fn() }
});
await expect.element(page.getByText(/11111111/)).toBeVisible();
});
it('fires onremove when the remove button is clicked', async () => {
const onremove = vi.fn();
render(DocumentFilterChip, {
props: { id: VALID_UUID, title: 'Brief an Oma', onremove }
});
const btn = (await page
.getByRole('button', { name: /Brief an Oma aus Filter entfernen/ })
.element()) as HTMLElement;
btn.click();
await vi.waitFor(() => expect(onremove).toHaveBeenCalledOnce());
});
it('remove button aria-label references the resolved title', async () => {
render(DocumentFilterChip, {
props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
});
const btn = page.getByRole('button', { name: /Brief an Oma aus Filter entfernen/ });
await expect.element(btn).toBeVisible();
});
it('title= attribute equals the validated id, not a raw query string', async () => {
render(DocumentFilterChip, {
props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
});
const chip = document.querySelector('[title]');
expect(chip?.getAttribute('title')).toBe('Brief an Oma');
});
it('remove button has a minimum 44px touch target', async () => {
render(DocumentFilterChip, {
props: { id: VALID_UUID, title: 'Brief an Oma', onremove: vi.fn() }
});
const btn = (await page
.getByRole('button', { name: /Brief an Oma aus Filter entfernen/ })
.element()) as HTMLElement;
expect(btn.className).toMatch(/min-h-\[44px\]|min-h-11/);
});
});