feat(frontend): simplify homepage to pure dashboard — remove search/filter dual-mode
The homepage now always renders the dashboard. Search and browse moves to the dedicated /documents route (upcoming). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,48 +3,18 @@ import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import Page from './+page.svelte';
|
||||
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
afterEach(cleanup);
|
||||
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn(), invalidateAll: vi.fn() }));
|
||||
|
||||
// Silence fetch calls from PersonTypeahead when advanced filters are open
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
|
||||
);
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// ─── Test data ────────────────────────────────────────────────────────────────
|
||||
|
||||
const emptyData = {
|
||||
user: undefined,
|
||||
const baseData = {
|
||||
user: { firstName: 'Max' },
|
||||
canWrite: true,
|
||||
canAnnotate: false,
|
||||
isDashboard: false,
|
||||
filters: {
|
||||
q: '',
|
||||
from: '',
|
||||
to: '',
|
||||
senderId: '',
|
||||
receiverId: '',
|
||||
tags: [],
|
||||
sort: 'DATE' as const,
|
||||
dir: 'desc' as const,
|
||||
tagQ: '',
|
||||
tagOp: 'AND'
|
||||
},
|
||||
documents: [],
|
||||
total: 0,
|
||||
matchData: {} as Record<
|
||||
string,
|
||||
import('$lib/generated/api').components['schemas']['SearchMatchData']
|
||||
>,
|
||||
resumeDoc: null,
|
||||
pulse: null,
|
||||
activityFeed: [],
|
||||
stats: null,
|
||||
initialValues: { senderName: '', receiverName: '' },
|
||||
segmentationDocs: [],
|
||||
transcriptionDocs: [],
|
||||
readyDocs: [],
|
||||
@@ -52,194 +22,22 @@ const emptyData = {
|
||||
error: null
|
||||
};
|
||||
|
||||
const makeDoc = (overrides = {}) => ({
|
||||
id: '1',
|
||||
title: 'Testbrief',
|
||||
originalFilename: 'testbrief.pdf',
|
||||
status: 'UPLOADED' as const,
|
||||
documentDate: '2024-03-15',
|
||||
location: 'Berlin',
|
||||
sender: {
|
||||
id: 'p1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
personType: 'PERSON' as const
|
||||
},
|
||||
receivers: [
|
||||
{
|
||||
id: 'p2',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Musterfrau',
|
||||
displayName: 'Anna Musterfrau',
|
||||
personType: 'PERSON' as const
|
||||
}
|
||||
],
|
||||
tags: [{ id: 't1', name: 'Familie' }],
|
||||
filePath: '/files/testbrief.pdf',
|
||||
createdAt: '2024-03-15T10:00:00Z',
|
||||
updatedAt: '2024-03-15T10:00:00Z',
|
||||
...overrides
|
||||
});
|
||||
// ─── Dashboard layout ─────────────────────────────────────────────────────────
|
||||
|
||||
const dataWithDocs = { ...emptyData, documents: [makeDoc()] };
|
||||
|
||||
// ─── Search bar ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – search bar', () => {
|
||||
it('renders the full-text search input', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await expect
|
||||
.element(page.getByPlaceholder('Titel, Personen, Tags durchsuchen\u2026'))
|
||||
.toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-default.png' });
|
||||
describe('Home page – dashboard layout', () => {
|
||||
it('does not render a search input', async () => {
|
||||
render(Page, { data: baseData });
|
||||
const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen…');
|
||||
await expect.element(input).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the filter toggle button', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
// Use exact match to avoid collision with the empty-state "Alle Filter löschen" button
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Filter', exact: true }))
|
||||
.toBeInTheDocument();
|
||||
it('renders a greeting for the logged-in user', async () => {
|
||||
render(Page, { data: baseData });
|
||||
await expect.element(page.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the reset link pointing to /', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
const resetLink = page.getByTitle('Filter zurücksetzen');
|
||||
await expect.element(resetLink).toBeInTheDocument();
|
||||
await expect.element(resetLink).toHaveAttribute('href', '/');
|
||||
});
|
||||
|
||||
it('pre-fills the search input from filters.q', async () => {
|
||||
render(Page, { data: { ...emptyData, filters: { ...emptyData.filters, q: 'Urlaub' } } });
|
||||
await expect
|
||||
.element(page.getByPlaceholder('Titel, Personen, Tags durchsuchen\u2026'))
|
||||
.toHaveValue('Urlaub');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Advanced filters ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – advanced filters', () => {
|
||||
it('hides the advanced filters by default', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
// Date inputs are inside the {#if showAdvanced} block → not in DOM
|
||||
await tick();
|
||||
expect(document.querySelector('input[id="from"]')).toBeNull();
|
||||
expect(document.querySelector('input[id="to"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('toggles the advanced filter panel open on button click', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await page.getByRole('button', { name: 'Filter', exact: true }).click();
|
||||
await tick();
|
||||
expect(document.querySelector('input[id="from"]')).not.toBeNull();
|
||||
expect(document.querySelector('input[id="to"]')).not.toBeNull();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-filters-open.png' });
|
||||
});
|
||||
|
||||
it('collapses the advanced filter panel on second click', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
const btn = page.getByRole('button', { name: 'Filter', exact: true });
|
||||
await btn.click();
|
||||
// Wait for the input to appear before clicking again
|
||||
await expect.element(page.getByText('Schlagworte')).toBeInTheDocument();
|
||||
await btn.click();
|
||||
// Wait for slide transition to finish
|
||||
await expect.element(page.getByText('Schlagworte')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the tag filter section when filters are open', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await page.getByRole('button', { name: 'Filter', exact: true }).click();
|
||||
await expect.element(page.getByText('Schlagworte')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Document list ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – document list', () => {
|
||||
it('shows empty state when there are no documents', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await expect.element(page.getByText('Keine Dokumente gefunden')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-empty-state.png' });
|
||||
});
|
||||
|
||||
it('renders a document with title, date, location, sender and receiver', async () => {
|
||||
render(Page, { data: dataWithDocs });
|
||||
await expect.element(page.getByText('Testbrief')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('15. März 2024')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Berlin')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-with-documents.png' });
|
||||
});
|
||||
|
||||
it('renders a tag chip for each document tag', async () => {
|
||||
render(Page, { data: dataWithDocs });
|
||||
await expect.element(page.getByRole('button', { name: 'Familie' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Unbekannt" for sender when sender is null', async () => {
|
||||
const data = { ...emptyData, documents: [makeDoc({ sender: null })] };
|
||||
render(Page, { data });
|
||||
await expect.element(page.getByText('Unbekannt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders original filename when title is empty', async () => {
|
||||
const data = { ...emptyData, documents: [makeDoc({ title: null })] };
|
||||
render(Page, { data });
|
||||
await expect.element(page.getByText('testbrief.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links each document to its detail page', async () => {
|
||||
render(Page, { data: dataWithDocs });
|
||||
const link = page.getByRole('link', { name: /Testbrief/ });
|
||||
await expect.element(link).toHaveAttribute('href', '/documents/1');
|
||||
});
|
||||
|
||||
it('renders the "Neues Dokument" link', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
const link = page.getByRole('link', { name: /Neues Dokument/i });
|
||||
await expect.element(link).toBeInTheDocument();
|
||||
await expect.element(link).toHaveAttribute('href', '/documents/new');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Keystroke preservation (issue #34) ──────────────────────────────────────
|
||||
|
||||
describe('Home page – search input keystroke preservation', () => {
|
||||
it('does not overwrite the search input while the user is focused and stale data arrives', async () => {
|
||||
const { rerender } = render(Page, { data: emptyData });
|
||||
|
||||
const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen\u2026');
|
||||
|
||||
// User types "abc" — input is focused
|
||||
await input.click();
|
||||
await input.fill('abc');
|
||||
|
||||
// Simulate a navigation completing with stale data (q='a') while the user is still typing
|
||||
await rerender({ data: { ...emptyData, filters: { ...emptyData.filters, q: 'a' } } });
|
||||
await tick();
|
||||
|
||||
// Input must still show what the user typed, not the stale URL value
|
||||
await expect.element(input).toHaveValue('abc');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Dashboard mode ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – dashboard mode', () => {
|
||||
const dashboardData = {
|
||||
...emptyData,
|
||||
isDashboard: true,
|
||||
resumeDoc: null,
|
||||
pulse: null,
|
||||
activityFeed: []
|
||||
};
|
||||
|
||||
it('renders empty state when resumeDoc is null', async () => {
|
||||
render(Page, { data: dashboardData });
|
||||
it('renders resume strip empty state when resumeDoc is null', async () => {
|
||||
render(Page, { data: baseData });
|
||||
const empty = page.getByTestId('resume-strip-empty');
|
||||
await expect.element(empty).toBeInTheDocument();
|
||||
});
|
||||
@@ -255,53 +53,18 @@ describe('Home page – dashboard mode', () => {
|
||||
pct: 33,
|
||||
collaborators: []
|
||||
};
|
||||
render(Page, { data: { ...dashboardData, resumeDoc: resume } });
|
||||
render(Page, { data: { ...baseData, resumeDoc: resume } });
|
||||
const strip = page.getByTestId('resume-strip');
|
||||
await expect.element(strip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Error state ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – error state', () => {
|
||||
it('shows the error message when data.error is set', async () => {
|
||||
const data = { ...emptyData, error: 'Daten konnten nicht geladen werden.' };
|
||||
render(Page, { data });
|
||||
await expect.element(page.getByText('Daten konnten nicht geladen werden.')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-error.png' });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Loading spinner ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – loading spinner', () => {
|
||||
it('does not show spinner by default', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
const spinner = page.getByRole('status');
|
||||
await expect.element(spinner).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Sort controls ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – sort controls', () => {
|
||||
it('pre-fills sort from filters.sort', async () => {
|
||||
const data = {
|
||||
...emptyData,
|
||||
filters: { ...emptyData.filters, sort: 'TITLE', dir: 'asc', tagQ: '' }
|
||||
};
|
||||
render(Page, { data });
|
||||
const select = page.getByRole('combobox');
|
||||
await expect.element(select).toHaveValue('TITLE');
|
||||
it('shows drop zone when canWrite is true', async () => {
|
||||
render(Page, { data: { ...baseData, canWrite: true } });
|
||||
await expect.element(page.getByText(/Dateien auf einmal hochladen/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders direction toggle with asc indicator when dir is asc', async () => {
|
||||
const data = {
|
||||
...emptyData,
|
||||
filters: { ...emptyData.filters, sort: 'DATE', dir: 'asc', tagQ: '' }
|
||||
};
|
||||
render(Page, { data });
|
||||
const btn = page.getByRole('button', { name: /aufsteigend/i });
|
||||
await expect.element(btn).toBeInTheDocument();
|
||||
it('hides drop zone when canWrite is false', async () => {
|
||||
render(Page, { data: { ...baseData, canWrite: false } });
|
||||
await expect.element(page.getByText(/Dateien auf einmal hochladen/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user