diff --git a/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte new file mode 100644 index 00000000..e3574721 --- /dev/null +++ b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte @@ -0,0 +1,87 @@ + + +
+ +
+ + {timeLabel} + + + {m.dashboard_welcome({ name })} + +
+ + + + + +
+ + {documents ?? '—'} + + {m.dashboard_reader_stats_documents_short()} + + + + {persons ?? '—'} + + {m.dashboard_reader_stats_persons_short()} + + + + {stories ?? '—'} + + {m.dashboard_reader_stats_stories_short()} + +
+
diff --git a/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte.spec.ts new file mode 100644 index 00000000..c9d81c9b --- /dev/null +++ b/frontend/src/lib/shared/dashboard/ReaderHeaderBar.svelte.spec.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +import ReaderHeaderBar from './ReaderHeaderBar.svelte'; + +afterEach(() => { + cleanup(); +}); + +describe('ReaderHeaderBar', () => { + it('renders a link to /documents with document count', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /42/ }); + await expect.element(link).toHaveAttribute('href', '/documents'); + }); + + it('renders a link to /persons with person count', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /7/ }); + await expect.element(link).toHaveAttribute('href', '/persons'); + }); + + it('renders a link to /geschichten with story count', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /3/ }); + await expect.element(link).toHaveAttribute('href', '/geschichten'); + }); + + it('documents stat link has min-h-[44px] for touch target', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /42/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('persons stat link has min-h-[44px] for touch target', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /7/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('stories stat link has min-h-[44px] for touch target', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 42, persons: 7, stories: 3 }); + const link = page.getByRole('link', { name: /3/ }); + const cls = ((await link.element()) as HTMLElement).className; + expect(cls).toMatch(/min-h-\[44px\]/); + }); + + it('shows "—" when counts are null', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: null, persons: null, stories: null }); + const wrapper = page.getByRole('banner'); + const text = ((await wrapper.element()) as HTMLElement).textContent; + expect(text?.match(/—/g)?.length).toBeGreaterThanOrEqual(3); + }); + + it('time label uses text-brand-navy class for morning hour', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 8 }); + const timeLabel = page.getByText(/Morgen/i); + await expect.element(timeLabel).toBeInTheDocument(); + const cls = ((await timeLabel.element()) as HTMLElement).className; + expect(cls).toMatch(/text-brand-navy/); + }); + + it('shows afternoon label for hour 14', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 14 }); + const timeLabel = page.getByText(/Mittag/i); + await expect.element(timeLabel).toBeInTheDocument(); + }); + + it('shows evening label for hour 20', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 20 }); + const timeLabel = page.getByText(/Abend/i); + await expect.element(timeLabel).toBeInTheDocument(); + }); + + it('welcome line contains the user name', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1, hour: 8 }); + const welcome = page.getByText(/Anna/); + await expect.element(welcome).toBeInTheDocument(); + }); + + it('wrapper has dark mode classes', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1 }); + const wrapper = page.getByRole('banner'); + const cls = ((await wrapper.element()) as HTMLElement).className; + expect(cls).toMatch(/dark:bg-surface/); + expect(cls).toMatch(/dark:border-white\/8/); + }); + + it('renders a vertical divider with bg-line class', async () => { + render(ReaderHeaderBar, { name: 'Anna', documents: 1, persons: 1, stories: 1 }); + const wrapper = page.getByRole('banner'); + const el = (await wrapper.element()) as HTMLElement; + const divider = el.querySelector('[aria-hidden="true"]'); + expect(divider).not.toBeNull(); + expect(divider!.className).toMatch(/bg-line/); + }); +});