feat(lesereisen): implement lesereisen
All checks were successful
CI / Unit & Component Tests (push) Successful in 4m34s
CI / OCR Service Tests (push) Successful in 27s
CI / Backend Unit Tests (push) Successful in 5m1s
CI / fail2ban Regex (push) Successful in 47s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m11s
All checks were successful
CI / Unit & Component Tests (push) Successful in 4m34s
CI / OCR Service Tests (push) Successful in 27s
CI / Backend Unit Tests (push) Successful in 5m1s
CI / fail2ban Regex (push) Successful in 47s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m11s
This commit was merged in pull request #787.
This commit is contained in:
@@ -33,6 +33,17 @@ function makeData(overrides: Partial<PageData> = {}): PageData {
|
||||
} 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, {
|
||||
@@ -81,9 +92,12 @@ describe('geschichten page — multi-person filter chips', () => {
|
||||
})
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /Anna A aus Filter entfernen/ }).click();
|
||||
const chipBtn = (await page
|
||||
.getByRole('button', { name: /Anna A aus Filter entfernen/ })
|
||||
.element()) as HTMLElement;
|
||||
chipBtn.click();
|
||||
|
||||
expect(goto).toHaveBeenCalledOnce();
|
||||
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');
|
||||
@@ -91,6 +105,19 @@ describe('geschichten page — multi-person filter chips', () => {
|
||||
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({
|
||||
@@ -100,6 +127,207 @@ describe('geschichten page — multi-person filter chips', () => {
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user