From e824e23c8cd2513fca7aa3aab6c5109b057fe700 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 20 Apr 2026 21:58:05 +0200 Subject: [PATCH] feat(dashboard): add EnrichmentBlock wrapper component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composes UploadSuccessBanner + DashboardNeedsMetadata and reserves a 360px skeleton while \$navigating re-runs the loader with a fresh incomplete list. Prevents the layout-shift jump after a batch upload (Leonie's resolved decision #3 on issue #296). Renders nothing when there is nothing to show — keeps the clean empty dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/components/EnrichmentBlock.svelte | 39 ++++++++++++ .../components/EnrichmentBlock.svelte.spec.ts | 61 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 frontend/src/lib/components/EnrichmentBlock.svelte create mode 100644 frontend/src/lib/components/EnrichmentBlock.svelte.spec.ts diff --git a/frontend/src/lib/components/EnrichmentBlock.svelte b/frontend/src/lib/components/EnrichmentBlock.svelte new file mode 100644 index 00000000..2743329f --- /dev/null +++ b/frontend/src/lib/components/EnrichmentBlock.svelte @@ -0,0 +1,39 @@ + + +{#if showBlock} +
+ {#if bannerCount > 0} + + {/if} + {#if topDocs.length > 0} + + {:else if showSkeleton} + + {/if} +
+{/if} diff --git a/frontend/src/lib/components/EnrichmentBlock.svelte.spec.ts b/frontend/src/lib/components/EnrichmentBlock.svelte.spec.ts new file mode 100644 index 00000000..be187cf5 --- /dev/null +++ b/frontend/src/lib/components/EnrichmentBlock.svelte.spec.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +import EnrichmentBlock from './EnrichmentBlock.svelte'; + +vi.mock('$app/stores', async () => { + const { writable } = await import('svelte/store'); + return { navigating: writable(null) }; +}); + +afterEach(cleanup); + +type Doc = { id: string; title: string; uploadedAt: string }; + +function doc(id: string, title = 'Doc'): Doc { + return { id, title, uploadedAt: '2026-04-20T12:00:00' }; +} + +describe('EnrichmentBlock', () => { + it('renders nothing when topDocs is empty and banner count is 0', async () => { + render(EnrichmentBlock, { + topDocs: [], + totalCount: 0, + bannerCount: 0, + onBannerClose: vi.fn() + }); + await expect.element(page.getByTestId('enrichment-block')).not.toBeInTheDocument(); + }); + + it('renders the list component when topDocs is non-empty', async () => { + render(EnrichmentBlock, { + topDocs: [doc('d1')], + totalCount: 1, + bannerCount: 0, + onBannerClose: vi.fn() + }); + await expect.element(page.getByTestId('dashboard-needs-metadata')).toBeInTheDocument(); + }); + + it('renders the banner when bannerCount > 0', async () => { + render(EnrichmentBlock, { + topDocs: [], + totalCount: 0, + bannerCount: 3, + onBannerClose: vi.fn() + }); + await expect.element(page.getByRole('status')).toBeInTheDocument(); + }); + + it('composes banner + list when both are present', async () => { + render(EnrichmentBlock, { + topDocs: [doc('d1')], + totalCount: 1, + bannerCount: 2, + onBannerClose: vi.fn() + }); + await expect.element(page.getByRole('status')).toBeInTheDocument(); + await expect.element(page.getByTestId('dashboard-needs-metadata')).toBeInTheDocument(); + }); +});