Files
familienarchiv/frontend/src/routes/DocumentList.svelte
Marcel 0c765d8112
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m54s
CI / OCR Service Tests (pull_request) Successful in 33s
CI / Backend Unit Tests (pull_request) Failing after 3m13s
CI / Unit & Component Tests (push) Failing after 3m48s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m24s
fix(tests): fix 13 pre-existing vitest-browser spec failures
Fixes all remaining failing tests in the browser project. Root cause in
every case: Playwright CDP-based clicks/keyboard events do not reliably
trigger Svelte 5 onclick/onkeydown handlers. Pattern applied throughout:

- Buttons / result items: native `.element().click()` or
  `dispatchEvent(new MouseEvent('click', { bubbles: true }))`
- Keyboard events: `dispatchEvent(new KeyboardEvent('keydown', { key }))`
  on the target DOM element
- TipTap selection: `element.focus()` + Selection API +
  `document.dispatchEvent(new Event('selectionchange'))`
- ProseMirror focus for onFocus: `dispatchEvent(new FocusEvent('focus'))`

Also fixes pre-existing content/logic issues found during analysis:
- ChronikErrorCard, BulkDropZone, CorrespondenzHero: stale i18n strings
  and wrong ARIA role (combobox not textbox)
- RichtlinienRuleCard: beide beispielInput + beispielOutput required for
  arrow to render; querySelectorAll to get last code element
- admin/system/page: vi.unstubAllGlobals() in afterEach; strict-mode
  heading selector; per-call mockResolvedValueOnce for dual-card page
- DocumentList: add total prop + result count paragraph (test relied on it)
- PersonTypeahead keyboard navigation: pressKey() helper with native
  KeyboardEvent dispatch replaces userEvent.keyboard()
- PersonMultiSelect: native element clicks for result selection and
  chip removal; keydown dispatch on result div for Enter key test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 13:15:54 +02:00

133 lines
4.0 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { m } from '$lib/paraglide/messages.js';
import DocumentRow from '$lib/document/DocumentRow.svelte';
import { SvelteMap } from 'svelte/reactivity';
import type { components } from '$lib/generated/api';
type DocumentSearchItem = components['schemas']['DocumentSearchItem'];
type SortMode = 'DATE' | 'TITLE' | 'SENDER' | 'RECEIVER' | 'UPLOAD_DATE' | 'RELEVANCE';
let {
items,
canWrite,
error,
total = 0,
q = '',
sort = 'DATE'
}: {
items: DocumentSearchItem[];
canWrite: boolean;
error?: string | null;
total?: number;
q?: string;
sort?: SortMode;
} = $props();
const groups = $derived.by(() => {
if (sort === 'SENDER') return groupBySender(items);
if (sort === 'RECEIVER') return groupByReceiver(items);
return groupByYear(items);
});
function groupByYear(docItems: DocumentSearchItem[]) {
const map = new SvelteMap<string, DocumentSearchItem[]>();
for (const item of docItems) {
const label = item.document.documentDate?.substring(0, 4) ?? m.docs_group_undated();
const bucket = map.get(label);
if (bucket) bucket.push(item);
else map.set(label, [item]);
}
return Array.from(map.entries()).map(([label, groupItems]) => ({ label, items: groupItems }));
}
function groupBySender(docItems: DocumentSearchItem[]) {
const map = new SvelteMap<string, DocumentSearchItem[]>();
for (const item of docItems) {
const label = item.document.sender?.displayName ?? m.docs_group_unknown_sender();
const bucket = map.get(label);
if (bucket) bucket.push(item);
else map.set(label, [item]);
}
return Array.from(map.entries()).map(([label, groupItems]) => ({ label, items: groupItems }));
}
function groupByReceiver(docItems: DocumentSearchItem[]) {
const map = new SvelteMap<string, DocumentSearchItem[]>();
for (const item of docItems) {
const receivers = item.document.receivers ?? [];
const labels =
receivers.length > 0
? receivers.map((r) => r.displayName)
: [m.docs_group_unknown_receiver()];
for (const label of labels) {
const bucket = map.get(label);
if (bucket) bucket.push(item);
else map.set(label, [item]);
}
}
return Array.from(map.entries()).map(([label, groupItems]) => ({ label, items: groupItems }));
}
</script>
<!-- ERROR -->
{#if error}
<div class="border border-line bg-surface shadow-sm">
<div class="bg-red-50 p-8 text-center text-red-600">
{error}
</div>
</div>
{:else if items.length > 0}
<!-- RESULT COUNT -->
{#if total > 0}
<p class="mb-2 text-sm text-ink-2">{total} Dokumente</p>
{/if}
<!-- GROUP CARDS -->
{#each groups as group (group.label)}
<div
data-testid="group-card"
class="mb-4 overflow-hidden border border-line bg-surface shadow-sm"
>
<div class="border-b border-line bg-muted px-5 py-2">
<span
data-testid="group-header"
class="font-sans text-xs font-bold text-ink-3"
class:uppercase={sort !== 'SENDER' && sort !== 'RECEIVER'}
class:tracking-widest={sort !== 'SENDER' && sort !== 'RECEIVER'}
class:tracking-wide={sort === 'SENDER' || sort === 'RECEIVER'}>{group.label}</span
>
</div>
<ul class="divide-y divide-line">
{#each group.items as item (group.label + '-' + item.document.id)}
<DocumentRow item={item} canWrite={canWrite} />
{/each}
</ul>
</div>
{/each}
{:else}
<!-- EMPTY STATE -->
<div class="border border-line bg-surface shadow-sm">
<div class="p-16 text-center">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Mag-Glass-MD.svg"
alt=""
aria-hidden="true"
class="h-6 w-6"
/>
</div>
<h3 class="font-serif text-lg font-medium text-ink">{m.docs_empty_heading()}</h3>
<p class="mt-1 font-sans text-sm text-ink-2">
{q ? m.docs_empty_for_term({ term: q }) : m.docs_empty_text()}
</p>
<button
onclick={() => goto('/documents')}
class="mt-6 text-sm font-bold tracking-wide text-primary uppercase transition hover:text-ink-2"
>
{m.docs_empty_btn_clear()}
</button>
</div>
</div>
{/if}