Files
familienarchiv/frontend/src/routes/geschichten/page.svelte.spec.ts
Marcel cb87695834 test(geschichten/page): add failing tests for Entwürfe section
RED: page does not yet render a drafts section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 18:51:19 +02:00

396 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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() }));
vi.mock('$app/state', () => ({ navigating: { to: null } }));
import Page from './+page.svelte';
import type { PageData } from './$types';
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
function person(id: string, displayName: string) {
return {
id,
firstName: displayName.split(' ')[0] ?? displayName,
lastName: displayName.split(' ').slice(1).join(' ') || 'X',
displayName,
personType: 'PERSON'
};
}
function makeData(overrides: Partial<PageData> = {}): PageData {
return {
geschichten: [],
drafts: [],
personFilters: [],
documentFilter: null,
canBlogWrite: false,
...overrides
} as unknown as PageData;
}
function makeDocumentFilter(overrides: { id?: string; title?: string | null } = {}): {
id: string;
title: string | null;
} {
return {
id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
title: 'Brief an Oma',
...overrides
};
}
describe('geschichten page — multi-person filter chips', () => {
it('renders one chip per person in personFilters', async () => {
render(Page, {
data: makeData({
personFilters: [person('a', 'Anna A'), person('b', 'Bertha B')] as PageData['personFilters']
})
});
await expect
.element(page.getByRole('button', { name: /Anna A aus Filter entfernen/ }))
.toBeVisible();
await expect
.element(page.getByRole('button', { name: /Bertha B aus Filter entfernen/ }))
.toBeVisible();
});
it('renders the "All" pill in pressed state when no filters are active', async () => {
render(Page, { data: makeData() });
await expect
.element(page.getByRole('button', { name: 'Alle' }))
.toHaveAttribute('aria-pressed', 'true');
});
it('renders the "All" pill in unpressed state when at least one filter is active', async () => {
render(Page, {
data: makeData({
personFilters: [person('a', 'Anna A')] as PageData['personFilters']
})
});
await expect
.element(page.getByRole('button', { name: 'Alle' }))
.toHaveAttribute('aria-pressed', 'false');
});
it('clicking × on a chip removes only that person from the URL', async () => {
const { goto } = await import('$app/navigation');
vi.mocked(goto).mockClear();
// Seed window.location so the chip-removal logic builds the new URL deterministically.
const originalHref = window.location.href;
window.history.replaceState({}, '', '/geschichten?personId=a&personId=b');
render(Page, {
data: makeData({
personFilters: [person('a', 'Anna A'), person('b', 'Bertha B')] as PageData['personFilters']
})
});
const chipBtn = (await page
.getByRole('button', { name: /Anna A aus Filter entfernen/ })
.element()) as HTMLElement;
chipBtn.click();
await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce());
const url = vi.mocked(goto).mock.calls[0][0] as string;
expect(url).toContain('personId=b');
expect(url).not.toContain('personId=a');
window.history.replaceState({}, '', originalHref);
});
it('JOURNEY row in the list shows the REISE badge (integration: page passes type through)', async () => {
render(Page, {
data: makeData({
geschichten: [
{ id: 'g1', title: 'Lesereise Berlin', type: 'JOURNEY' }
] as PageData['geschichten']
})
});
const badge = document.querySelector('[data-testid="journey-badge"]');
expect(badge).not.toBeNull();
});
it('shows the "+ Person wählen" button even when filters are already active', async () => {
render(Page, {
data: makeData({
personFilters: [person('a', 'Anna A')] as PageData['personFilters']
})
});
await expect.element(page.getByRole('button', { name: /Person wählen/ })).toBeVisible();
});
describe('document filter chip', () => {
it('renders the document chip when documentFilter is set', async () => {
render(Page, {
data: makeData({ documentFilter: makeDocumentFilter() as PageData['documentFilter'] })
});
await expect.element(page.getByText(/Gefiltert nach Brief/)).toBeVisible();
await expect.element(page.getByText(/Brief an Oma/)).toBeVisible();
});
it('does not render the document chip when documentFilter is null', async () => {
render(Page, { data: makeData() });
await expect.element(page.getByText(/Gefiltert nach Brief/)).not.toBeInTheDocument();
});
it('clicking the document chip remove button navigates without documentId', async () => {
const { goto } = await import('$app/navigation');
vi.mocked(goto).mockClear();
window.history.replaceState(
{},
'',
'/geschichten?documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
);
render(Page, {
data: makeData({ documentFilter: makeDocumentFilter() as PageData['documentFilter'] })
});
const removeBtn = (await page
.getByRole('button', { name: /Brief an Oma aus Filter entfernen/ })
.element()) as HTMLElement;
removeBtn.click();
await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce());
const url = vi.mocked(goto).mock.calls[0][0] as string;
expect(url).not.toContain('documentId');
});
it('document chip removal preserves active person filters', async () => {
const { goto } = await import('$app/navigation');
vi.mocked(goto).mockClear();
window.history.replaceState(
{},
'',
'/geschichten?personId=p1&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
);
render(Page, {
data: makeData({
personFilters: [person('p1', 'Anna A')] as PageData['personFilters'],
documentFilter: makeDocumentFilter() as PageData['documentFilter']
})
});
const removeBtn = (await page
.getByRole('button', { name: /Brief an Oma aus Filter entfernen/ })
.element()) as HTMLElement;
removeBtn.click();
await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce());
const url = vi.mocked(goto).mock.calls[0][0] as string;
expect(url).toContain('personId=p1');
expect(url).not.toContain('documentId');
});
it('marks the "All" pill as unpressed when document filter is active', async () => {
render(Page, {
data: makeData({ documentFilter: makeDocumentFilter() as PageData['documentFilter'] })
});
await expect
.element(page.getByRole('button', { name: 'Alle' }))
.toHaveAttribute('aria-pressed', 'false');
});
});
it('removing a person chip preserves an active document filter in the URL', async () => {
const { goto } = await import('$app/navigation');
vi.mocked(goto).mockClear();
window.history.replaceState(
{},
'',
'/geschichten?personId=a&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
);
render(Page, {
data: makeData({
personFilters: [person('a', 'Anna A')] as PageData['personFilters'],
documentFilter: makeDocumentFilter() as PageData['documentFilter']
})
});
const chipBtn = (await page
.getByRole('button', { name: /Anna A aus Filter entfernen/ })
.element()) as HTMLElement;
chipBtn.click();
await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce());
const url = vi.mocked(goto).mock.calls[0][0] as string;
expect(url).not.toContain('personId=a');
expect(url).toContain('documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee');
window.history.replaceState({}, '', '/');
});
it('clearAll removes both person and document filters from the URL', async () => {
const { goto } = await import('$app/navigation');
vi.mocked(goto).mockClear();
window.history.replaceState(
{},
'',
'/geschichten?personId=a&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
);
render(Page, {
data: makeData({
personFilters: [person('a', 'Anna A')] as PageData['personFilters'],
documentFilter: makeDocumentFilter() as PageData['documentFilter']
})
});
const allBtn = (await page.getByRole('button', { name: 'Alle' }).element()) as HTMLElement;
allBtn.click();
await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce());
const url = vi.mocked(goto).mock.calls[0][0] as string;
expect(url).not.toContain('personId');
expect(url).not.toContain('documentId');
window.history.replaceState({}, '', '/');
});
describe('empty state precedence', () => {
it('shows geschichten_empty_for_document when only document filter is active', async () => {
render(Page, {
data: makeData({
geschichten: [],
documentFilter: makeDocumentFilter() as PageData['documentFilter']
})
});
await expect.element(page.getByText('Noch keine Geschichten zu diesem Brief')).toBeVisible();
});
it('shows geschichten_empty_for_persons when only person filter is active', async () => {
render(Page, {
data: makeData({
geschichten: [],
personFilters: [person('a', 'Anna A')] as PageData['personFilters']
})
});
await expect.element(page.getByText(/Keine Geschichten für Anna A gefunden/)).toBeVisible();
});
it('shows geschichten_empty_no_filter when no filter is active', async () => {
render(Page, { data: makeData({ geschichten: [] }) });
await expect
.element(page.getByText('Es gibt noch keine veröffentlichten Geschichten.'))
.toBeVisible();
});
it('person-wins: shows persons message when both person and document filters are active', async () => {
render(Page, {
data: makeData({
geschichten: [],
personFilters: [person('a', 'Anna A')] as PageData['personFilters'],
documentFilter: makeDocumentFilter() as PageData['documentFilter']
})
});
await expect.element(page.getByText(/Keine Geschichten für Anna A gefunden/)).toBeVisible();
await expect
.element(page.getByText('Noch keine Geschichten zu diesem Brief'))
.not.toBeInTheDocument();
});
it('chip renders alongside results (empty state not shown when results exist)', async () => {
render(Page, {
data: makeData({
geschichten: [
{ id: 'g1', title: 'Lesereise Berlin', type: 'JOURNEY' }
] as PageData['geschichten'],
documentFilter: makeDocumentFilter() as PageData['documentFilter']
})
});
await expect.element(page.getByText(/Gefiltert nach Brief/)).toBeVisible();
await expect.element(page.getByText(/Lesereise Berlin/)).toBeVisible();
await expect
.element(page.getByText('Noch keine Geschichten zu diesem Brief'))
.not.toBeInTheDocument();
});
});
it('renders all filter pills with a 44px touch target (h-11)', async () => {
render(Page, {
data: makeData({
personFilters: [person('a', 'Anna A')] as PageData['personFilters']
})
});
// All three pill variants must use h-11 (44px) per the senior-author touch-target rule
const all = page.getByRole('button', { name: 'Alle' });
const chip = page.getByRole('button', { name: /Anna A aus Filter entfernen/ });
const add = page.getByRole('button', { name: /Person wählen/ });
const allEl = (await all.element()) as HTMLElement;
const chipEl = (await chip.element()) as HTMLElement;
const addEl = (await add.element()) as HTMLElement;
expect(allEl.className).toContain('h-11');
expect(chipEl.className).toContain('h-11');
expect(addEl.className).toContain('h-11');
});
});
// ─── Entwürfe section ─────────────────────────────────────────────────────────
describe('geschichten page — Entwürfe section', () => {
const draft = () =>
({
id: 'draft-1',
title: 'Mein Entwurf',
body: '<p>test</p>',
type: 'STORY',
status: 'DRAFT',
author: { firstName: 'Max', lastName: 'Muster' },
publishedAt: null
}) as unknown as PageData['geschichten'][0];
it('Entwürfe section is hidden when drafts array is empty', async () => {
render(Page, { data: makeData({ drafts: [] }) });
const heading = Array.from(document.querySelectorAll('h2')).find(
(h) => h.textContent?.includes('Entwürfe') || h.textContent?.includes('Drafts')
);
expect(heading).toBeUndefined();
});
it('Entwürfe section is visible when drafts are present', async () => {
render(Page, { data: makeData({ drafts: [draft()] as PageData['geschichten'] }) });
const heading = Array.from(document.querySelectorAll('h2')).find(
(h) => h.textContent?.includes('Entwürfe') || h.textContent?.includes('Drafts')
);
expect(heading).not.toBeUndefined();
});
it('renders a row for each draft story', async () => {
render(Page, { data: makeData({ drafts: [draft()] as PageData['geschichten'] }) });
const link = document.querySelector('a[href="/geschichten/draft-1"]');
expect(link).not.toBeNull();
});
it('draft row shows the draft badge', async () => {
render(Page, { data: makeData({ drafts: [draft()] as PageData['geschichten'] }) });
const badge = document.querySelector('[data-testid="draft-badge"]');
expect(badge).not.toBeNull();
});
});