From 6d4aa8bd5c279f912077d7109ab887cc2b2796ca Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 31 May 2026 12:26:42 +0200 Subject: [PATCH] test(admin-tags): pin merge/delete previews to the direct count (#698) Characterization tests for AC#8: the merge preview and the delete-impact warning describe direct-document operations, so they must report the tag's direct documentCount, never a subtree rollup. Both tests pass a stray subtreeDocumentCount and assert it does not leak into the preview, so a future change can't silently desync a destructive-action preview. Co-Authored-By: Claude Opus 4.8 --- .../tags/[id]/TagDeleteGuard.svelte.spec.ts | 15 +++++++++++ .../tags/[id]/TagMergeZone.svelte.spec.ts | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts b/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts index c55fd080..dbcd3069 100644 --- a/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts +++ b/frontend/src/routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts @@ -68,6 +68,21 @@ describe('TagDeleteGuard', () => { renderWithConfirm(); await expect.element(page.getByText(/3 Dokument/)).toBeInTheDocument(); }); + + // Characterization (#698): the delete-impact warning describes a destructive single-tag delete, + // which removes only the tag's DIRECT document_tags rows — so it must show documentCount, never + // a subtree rollup. Pinned so a future change can't silently desync this warning. + it('shows the direct documentCount in the impact summary, not a subtree rollup', async () => { + const tagWithStraySubtree = { + id: 't1', + name: 'Familie', + documentCount: 3, + subtreeDocumentCount: 99 + }; + renderWithConfirm({ tag: tagWithStraySubtree, allTags }); + await expect.element(page.getByText(/3 Dokument/)).toBeInTheDocument(); + expect(document.body.textContent).not.toContain('99'); + }); }); describe('TagDeleteGuard – confirmation dialog', () => { diff --git a/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte.spec.ts b/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte.spec.ts index bace59b7..6adf946b 100644 --- a/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte.spec.ts +++ b/frontend/src/routes/admin/tags/[id]/TagMergeZone.svelte.spec.ts @@ -66,6 +66,32 @@ describe('TagMergeZone – step flow', () => { }); }); +describe('TagMergeZone – preview uses the direct document count (characterization, #698)', () => { + it('shows the tag direct documentCount in the merge preview, not a subtree rollup', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue([{ id: 't2', name: 'Reise' }]) + }) + ); + // documentCount (direct) = 3; a stray subtree rollup of 99 must NOT leak into the preview — + // merge re-tags only direct documents, so the preview has to stay the direct count. + const mergeTag = { id: 't1', name: 'Familie', documentCount: 3, subtreeDocumentCount: 99 }; + render(TagMergeZone, { tag: mergeTag, allTags, form: null }); + + const input = page.getByRole('combobox'); + await input.fill('R'); + await vi.advanceTimersByTimeAsync(300); + await page.getByRole('option', { name: 'Reise' }).click(); + await vi.advanceTimersByTimeAsync(0); + + await expect.element(page.getByTestId('merge-step2')).toBeInTheDocument(); + expect(document.body.textContent).toContain('3 Dokumente'); + expect(document.body.textContent).not.toContain('99'); + }); +}); + describe('TagMergeZone – stale state reset', () => { it('resets target selection when tag prop changes', async () => { vi.stubGlobal(