diff --git a/frontend/src/lib/components/UploadSuccessBanner.svelte b/frontend/src/lib/components/UploadSuccessBanner.svelte
new file mode 100644
index 00000000..e10b2f04
--- /dev/null
+++ b/frontend/src/lib/components/UploadSuccessBanner.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
+ {message}
+
+ {m.upload_banner_cta()}
+
+
+
+
diff --git a/frontend/src/lib/components/UploadSuccessBanner.svelte.spec.ts b/frontend/src/lib/components/UploadSuccessBanner.svelte.spec.ts
new file mode 100644
index 00000000..bed83d9a
--- /dev/null
+++ b/frontend/src/lib/components/UploadSuccessBanner.svelte.spec.ts
@@ -0,0 +1,57 @@
+import { describe, it, expect, afterEach, vi } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+
+import UploadSuccessBanner from './UploadSuccessBanner.svelte';
+
+afterEach(() => {
+ cleanup();
+ vi.useRealTimers();
+});
+
+describe('UploadSuccessBanner', () => {
+ it('renders singular copy for count of 1', async () => {
+ render(UploadSuccessBanner, { count: 1, onClose: () => {} });
+ const status = page.getByRole('status');
+ await expect.element(status).toBeInTheDocument();
+ await expect.element(status).toHaveTextContent(/1 Dokument/);
+ });
+
+ it('renders plural copy for count greater than 1', async () => {
+ render(UploadSuccessBanner, { count: 3, onClose: () => {} });
+ await expect.element(page.getByRole('status')).toHaveTextContent(/3 Dokumente/);
+ });
+
+ it('exposes role=status with aria-live polite', async () => {
+ render(UploadSuccessBanner, { count: 1, onClose: () => {} });
+ await expect.element(page.getByRole('status')).toHaveAttribute('aria-live', 'polite');
+ });
+
+ it('renders a CTA link to /enrich', async () => {
+ render(UploadSuccessBanner, { count: 2, onClose: () => {} });
+ await expect
+ .element(page.getByRole('link', { name: /ergänzen/i }))
+ .toHaveAttribute('href', '/enrich');
+ });
+
+ it('invokes onClose when the close button is clicked', async () => {
+ const onClose = vi.fn();
+ render(UploadSuccessBanner, { count: 1, onClose });
+ const button = document.querySelector(
+ '[data-testid="upload-banner-close"]'
+ ) as HTMLButtonElement | null;
+ expect(button).not.toBeNull();
+ button?.click();
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('auto-dismisses after 8000ms', async () => {
+ vi.useFakeTimers();
+ const onClose = vi.fn();
+ render(UploadSuccessBanner, { count: 1, onClose });
+ vi.advanceTimersByTime(7999);
+ expect(onClose).not.toHaveBeenCalled();
+ vi.advanceTimersByTime(2);
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});