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} + + {#each visibleTags as tag (tag.id)} + + + + {tag.name} + {#if tag.documentCount > 0} + + {m.themen_dokumente({ count: tag.documentCount })} + + {/if} + + + {/each} + + {/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
{m.themen_leer()}