feat(geschichten): add DocumentFilterChip component with spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -13,6 +13,9 @@ node_modules
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Leftover directory from branch work
|
||||
/src.main/
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
|
||||
35
frontend/src/routes/geschichten/DocumentFilterChip.svelte
Normal file
35
frontend/src/routes/geschichten/DocumentFilterChip.svelte
Normal 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>
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user