From ff19e7da35896c716c5cbd4ad8399bc9a3e20620 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 21:27:57 +0200 Subject: [PATCH] test: cover DocumentThumbnail, UnsavedWarningBanner, PersonsStatsBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DocumentThumbnail: thumbnailUrl→img branch, no-thumbnail→placeholder icon branch, sm vs lg size container class, lazy/async loading attrs. UnsavedWarningBanner: warning text, discard button, callback wiring. PersonsStatsBar: count rendering, singular/plural label switching for both persons and documents (4 branches), zero-count plural fallback. 14 tests across three small primitive files. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../document/DocumentThumbnail.svelte.test.ts | 61 +++++++++++++++++++ .../UnsavedWarningBanner.svelte.test.ts | 29 +++++++++ .../persons/PersonsStatsBar.svelte.test.ts | 46 ++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 frontend/src/lib/document/DocumentThumbnail.svelte.test.ts create mode 100644 frontend/src/lib/shared/primitives/UnsavedWarningBanner.svelte.test.ts create mode 100644 frontend/src/routes/persons/PersonsStatsBar.svelte.test.ts diff --git a/frontend/src/lib/document/DocumentThumbnail.svelte.test.ts b/frontend/src/lib/document/DocumentThumbnail.svelte.test.ts new file mode 100644 index 00000000..41562aa6 --- /dev/null +++ b/frontend/src/lib/document/DocumentThumbnail.svelte.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import DocumentThumbnail from './DocumentThumbnail.svelte'; + +afterEach(cleanup); + +describe('DocumentThumbnail', () => { + it('renders the supplied thumbnail image when thumbnailUrl is set', async () => { + render(DocumentThumbnail, { + props: { + doc: { id: 'd1', thumbnailUrl: '/api/d1/thumb', contentType: 'application/pdf' } + } + }); + + const img = document.querySelector('img') as HTMLImageElement; + expect(img).not.toBeNull(); + expect(img.src).toContain('/api/d1/thumb'); + }); + + it('renders the placeholder icon when thumbnailUrl is missing', async () => { + render(DocumentThumbnail, { + props: { doc: { id: 'd1', thumbnailUrl: null, contentType: 'application/pdf' } } + }); + + const svg = document.querySelector('svg'); + expect(svg).not.toBeNull(); + }); + + it('uses the small container size by default', async () => { + render(DocumentThumbnail, { + props: { doc: { id: 'd1', thumbnailUrl: null, contentType: 'application/pdf' } } + }); + + const container = document.querySelector('.h-\\[84px\\]'); + expect(container).not.toBeNull(); + }); + + it('uses the large container size when size="lg"', async () => { + render(DocumentThumbnail, { + props: { + doc: { id: 'd1', thumbnailUrl: null, contentType: 'application/pdf' }, + size: 'lg' + } + }); + + const container = document.querySelector('.h-\\[168px\\]'); + expect(container).not.toBeNull(); + }); + + it('uses lazy loading attributes on the thumbnail image', async () => { + render(DocumentThumbnail, { + props: { + doc: { id: 'd1', thumbnailUrl: '/api/d1/thumb', contentType: 'application/pdf' } + } + }); + + const img = document.querySelector('img') as HTMLImageElement; + expect(img.loading).toBe('lazy'); + expect(img.decoding).toBe('async'); + }); +}); diff --git a/frontend/src/lib/shared/primitives/UnsavedWarningBanner.svelte.test.ts b/frontend/src/lib/shared/primitives/UnsavedWarningBanner.svelte.test.ts new file mode 100644 index 00000000..bcbbd0f3 --- /dev/null +++ b/frontend/src/lib/shared/primitives/UnsavedWarningBanner.svelte.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import UnsavedWarningBanner from './UnsavedWarningBanner.svelte'; + +afterEach(cleanup); + +describe('UnsavedWarningBanner', () => { + it('renders the warning text', async () => { + render(UnsavedWarningBanner, { props: { onDiscard: () => {} } }); + + await expect.element(page.getByText(/ungespeicherte änderungen/i)).toBeVisible(); + }); + + it('renders the discard action button', async () => { + render(UnsavedWarningBanner, { props: { onDiscard: () => {} } }); + + await expect.element(page.getByRole('button', { name: /verwerfen/i })).toBeVisible(); + }); + + it('calls onDiscard when the discard button is clicked', async () => { + const onDiscard = vi.fn(); + render(UnsavedWarningBanner, { props: { onDiscard } }); + + await page.getByRole('button', { name: /verwerfen/i }).click(); + + expect(onDiscard).toHaveBeenCalledOnce(); + }); +}); diff --git a/frontend/src/routes/persons/PersonsStatsBar.svelte.test.ts b/frontend/src/routes/persons/PersonsStatsBar.svelte.test.ts new file mode 100644 index 00000000..8b819fbe --- /dev/null +++ b/frontend/src/routes/persons/PersonsStatsBar.svelte.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import PersonsStatsBar from './PersonsStatsBar.svelte'; + +afterEach(cleanup); + +describe('PersonsStatsBar', () => { + it('renders both counts', async () => { + render(PersonsStatsBar, { props: { totalPersons: 42, totalDocuments: 99 } }); + + await expect.element(page.getByText('42')).toBeVisible(); + await expect.element(page.getByText('99')).toBeVisible(); + }); + + it('uses the singular person label when totalPersons is 1', async () => { + render(PersonsStatsBar, { props: { totalPersons: 1, totalDocuments: 5 } }); + + await expect.element(page.getByText('Person', { exact: true })).toBeVisible(); + }); + + it('uses the plural person label when totalPersons is not 1', async () => { + render(PersonsStatsBar, { props: { totalPersons: 5, totalDocuments: 5 } }); + + await expect.element(page.getByText('Personen', { exact: true })).toBeVisible(); + }); + + it('uses the singular document label when totalDocuments is 1', async () => { + render(PersonsStatsBar, { props: { totalPersons: 5, totalDocuments: 1 } }); + + await expect.element(page.getByText('Dokument', { exact: true })).toBeVisible(); + }); + + it('uses the plural document label when totalDocuments is not 1', async () => { + render(PersonsStatsBar, { props: { totalPersons: 5, totalDocuments: 5 } }); + + await expect.element(page.getByText('Dokumente', { exact: true })).toBeVisible(); + }); + + it('handles zero counts with plural labels', async () => { + render(PersonsStatsBar, { props: { totalPersons: 0, totalDocuments: 0 } }); + + await expect.element(page.getByText('Personen', { exact: true })).toBeVisible(); + await expect.element(page.getByText('Dokumente', { exact: true })).toBeVisible(); + }); +});