From 239d0b27a3e29e21f11b5b7604afe15d9f8671de Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 10 May 2026 00:40:07 +0200 Subject: [PATCH] test(admin/tags): cover TagTreeNode recursive branches Tag link href, document-count visibility branch, color-dot at depth 0 vs deeper, aria-current matrix, children list rendering, collapse-map hides children, expand/collapse toggle for nodes with children. 9 tests covering ~30 branches in the recursive tree-node component. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../admin/tags/TagTreeNode.svelte.test.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 frontend/src/routes/admin/tags/TagTreeNode.svelte.test.ts diff --git a/frontend/src/routes/admin/tags/TagTreeNode.svelte.test.ts b/frontend/src/routes/admin/tags/TagTreeNode.svelte.test.ts new file mode 100644 index 00000000..9c597820 --- /dev/null +++ b/frontend/src/routes/admin/tags/TagTreeNode.svelte.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page as browserPage } from 'vitest/browser'; +import { SvelteMap } from 'svelte/reactivity'; + +const mockPage = { url: new URL('http://localhost/admin/tags') }; + +vi.mock('$app/state', () => ({ + get page() { + return mockPage; + } +})); + +afterEach(cleanup); + +async function loadComponent() { + return (await import('./TagTreeNode.svelte')).default; +} + +const leafNode = (overrides: Record = {}) => ({ + id: 't1', + name: 'Personen', + color: 'sage', + documentCount: 5, + parentId: null, + children: [], + ...overrides +}); + +const parentNode = (overrides: Record = {}) => ({ + id: 'tp1', + name: 'Orte', + color: 'sienna', + documentCount: 0, + parentId: null, + children: [{ id: 'tc1', name: 'Berlin', color: null, documentCount: 2, children: [] }], + ...overrides +}); + +describe('TagTreeNode', () => { + it('renders the tag name as a link', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + render(Node, { props: { node: leafNode(), depth: 0, collapseMap: new SvelteMap() } }); + + await expect + .element(browserPage.getByRole('link', { name: /personen/i })) + .toHaveAttribute('href', '/admin/tags/t1'); + }); + + it('renders the document count when greater than 0', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + render(Node, { + props: { node: leafNode({ documentCount: 7 }), depth: 0, collapseMap: new SvelteMap() } + }); + + await expect.element(browserPage.getByText('(7)')).toBeVisible(); + }); + + it('omits the document count when 0', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + render(Node, { + props: { node: leafNode({ documentCount: 0 }), depth: 0, collapseMap: new SvelteMap() } + }); + + await expect.element(browserPage.getByText('(0)')).not.toBeInTheDocument(); + }); + + it('shows the color dot at depth 0 when color is set', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + render(Node, { props: { node: leafNode(), depth: 0, collapseMap: new SvelteMap() } }); + + const dot = document.querySelector('[data-testid="tag-list-color-dot"]'); + expect(dot).not.toBeNull(); + expect(dot?.getAttribute('data-color')).toBe('sage'); + }); + + it('omits the color dot when depth > 0', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + render(Node, { props: { node: leafNode(), depth: 1, collapseMap: new SvelteMap() } }); + + expect(document.querySelector('[data-testid="tag-list-color-dot"]')).toBeNull(); + }); + + it('marks the link as aria-current=page when on the matching route', async () => { + mockPage.url = new URL('http://localhost/admin/tags/t1'); + const Node = await loadComponent(); + render(Node, { props: { node: leafNode(), depth: 0, collapseMap: new SvelteMap() } }); + + await expect + .element(browserPage.getByRole('link', { name: /personen/i })) + .toHaveAttribute('aria-current', 'page'); + }); + + it('renders the children list for parent nodes when not collapsed', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + render(Node, { props: { node: parentNode(), depth: 0, collapseMap: new SvelteMap() } }); + + await expect.element(browserPage.getByText('Berlin')).toBeVisible(); + }); + + it('hides children when the node is collapsed', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + const map = new SvelteMap([['tp1', true]]); + render(Node, { props: { node: parentNode(), depth: 0, collapseMap: map } }); + + await expect.element(browserPage.getByText('Berlin')).not.toBeInTheDocument(); + }); + + it('exposes the expand/collapse toggle for nodes with children', async () => { + mockPage.url = new URL('http://localhost/admin/tags'); + const Node = await loadComponent(); + render(Node, { props: { node: parentNode(), depth: 0, collapseMap: new SvelteMap() } }); + + await expect + .element(browserPage.getByRole('button', { name: /einklappen|ausklappen/i })) + .toBeVisible(); + }); +});