From 279b4f10980275022b5250d8f6dec7b2886a18cd Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 25 May 2026 17:50:58 +0200 Subject: [PATCH] feat(themen): ThemenWidget component with compact prop + browser tests Co-Authored-By: Claude Sonnet 4.6 --- .../lib/shared/dashboard/ThemenWidget.svelte | 64 +++++++++++++++++++ .../dashboard/ThemenWidget.svelte.spec.ts | 58 +++++++++++++++++ .../{tag => shared/utils}/tagUtils.test.ts | 0 .../src/lib/{tag => shared/utils}/tagUtils.ts | 0 4 files changed, 122 insertions(+) create mode 100644 frontend/src/lib/shared/dashboard/ThemenWidget.svelte create mode 100644 frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts rename frontend/src/lib/{tag => shared/utils}/tagUtils.test.ts (100%) rename frontend/src/lib/{tag => shared/utils}/tagUtils.ts (100%) diff --git a/frontend/src/lib/shared/dashboard/ThemenWidget.svelte b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte new file mode 100644 index 00000000..b4ca9abf --- /dev/null +++ b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte @@ -0,0 +1,64 @@ + + +
+
+

+ {m.themen_widget_title()} +

+ + {m.themen_alle()} → + +
+ + {#if visibleTags.length === 0} +

{m.themen_leer()}

+ {:else} + + {/if} +
diff --git a/frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts new file mode 100644 index 00000000..521ddeba --- /dev/null +++ b/frontend/src/lib/shared/dashboard/ThemenWidget.svelte.spec.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import ThemenWidget from './ThemenWidget.svelte'; +import type { components } from '$lib/generated/api'; + +afterEach(() => { + cleanup(); +}); + +type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; + +function makeTag( + name: string, + documentCount: number, + children: TagTreeNodeDTO[] = [] +): TagTreeNodeDTO { + return { id: 'id-' + name, name, documentCount, children }; +} + +describe('ThemenWidget', () => { + it('renders a card link per visible tag', async () => { + const tags = [makeTag('Briefe', 5), makeTag('Fotos', 3)]; + const { getByRole } = render(ThemenWidget, { tags }); + await expect.element(getByRole('link', { name: /Briefe/ })).toBeInTheDocument(); + await expect.element(getByRole('link', { name: /Fotos/ })).toBeInTheDocument(); + }); + + it('hides tags where no document exists in the subtree', async () => { + const tags = [makeTag('Briefe', 5), makeTag('Leer', 0)]; + render(ThemenWidget, { tags }); + expect(document.body.textContent).toContain('Briefe'); + expect(document.body.textContent).not.toContain('Leer'); + }); + + it('shows the empty state text when all tags are filtered out', async () => { + render(ThemenWidget, { tags: [makeTag('Leer', 0)] }); + expect(document.body.textContent).toMatch(/Noch keine Themen/); + }); + + it('shows empty state when tags array is empty', async () => { + render(ThemenWidget, { tags: [] }); + expect(document.body.textContent).toMatch(/Noch keine Themen/); + }); + + it('renders in compact single-column mode when compact prop is true', async () => { + const tags = [makeTag('Briefe', 5)]; + const { container } = render(ThemenWidget, { tags, compact: true }); + const grid = container.querySelector('[data-compact="true"]'); + expect(grid).not.toBeNull(); + }); + + it('links to "Alle Themen" page', async () => { + const tags = [makeTag('Briefe', 5)]; + const { getByRole } = render(ThemenWidget, { tags }); + const link = getByRole('link', { name: /Alle Themen/ }); + await expect.element(link).toHaveAttribute('href', '/themen'); + }); +}); diff --git a/frontend/src/lib/tag/tagUtils.test.ts b/frontend/src/lib/shared/utils/tagUtils.test.ts similarity index 100% rename from frontend/src/lib/tag/tagUtils.test.ts rename to frontend/src/lib/shared/utils/tagUtils.test.ts diff --git a/frontend/src/lib/tag/tagUtils.ts b/frontend/src/lib/shared/utils/tagUtils.ts similarity index 100% rename from frontend/src/lib/tag/tagUtils.ts rename to frontend/src/lib/shared/utils/tagUtils.ts