The /geschichten list page now renders one removable chip per active person filter and lets users add more via the existing typeahead. The URL uses repeated ?personId= params (matching the documents tag filter), which the regenerated API client passes straight through to the backend's new array-bound endpoint. New translation keys cover the chip remove aria-label, the AND hint shown while picking, and the multi-person empty state.
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
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: [],
|
||
personFilters: [],
|
||
documentFilter: null,
|
||
canBlogWrite: false,
|
||
...overrides
|
||
} as unknown as PageData;
|
||
}
|
||
|
||
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']
|
||
})
|
||
});
|
||
|
||
await page.getByRole('button', { name: /Anna A aus Filter entfernen/ }).click();
|
||
|
||
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('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();
|
||
});
|
||
});
|