diff --git a/frontend/src/lib/planner/UndoBar.svelte b/frontend/src/lib/planner/UndoBar.svelte
new file mode 100644
index 0000000..2c6f1c6
--- /dev/null
+++ b/frontend/src/lib/planner/UndoBar.svelte
@@ -0,0 +1,67 @@
+
+
+{#if visible}
+
+
+ {message}
+
+
+
+{/if}
diff --git a/frontend/src/lib/planner/UndoBar.test.ts b/frontend/src/lib/planner/UndoBar.test.ts
new file mode 100644
index 0000000..90a8f04
--- /dev/null
+++ b/frontend/src/lib/planner/UndoBar.test.ts
@@ -0,0 +1,56 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, act } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import UndoBar from './UndoBar.svelte';
+
+describe('UndoBar', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('is not mounted when visible is false', () => {
+ render(UndoBar, { props: { visible: false, message: 'Test', onundo: vi.fn(), ondismiss: vi.fn() } });
+ expect(screen.queryByTestId('undo-bar')).toBeNull();
+ });
+
+ it('is mounted and shows message when visible is true', () => {
+ render(UndoBar, { props: { visible: true, message: 'Gericht hinzugefügt', onundo: vi.fn(), ondismiss: vi.fn() } });
+ expect(screen.getByTestId('undo-bar')).toBeTruthy();
+ expect(screen.getByText('Gericht hinzugefügt')).toBeTruthy();
+ });
+
+ it('shows Rückgängig button', () => {
+ render(UndoBar, { props: { visible: true, message: 'Test', onundo: vi.fn(), ondismiss: vi.fn() } });
+ expect(screen.getByRole('button', { name: /Rückgängig/i })).toBeTruthy();
+ });
+
+ it('calls onundo when Rückgängig is clicked', async () => {
+ const onundo = vi.fn();
+ render(UndoBar, { props: { visible: true, message: 'Test', onundo, ondismiss: vi.fn() } });
+ await userEvent.click(screen.getByRole('button', { name: /Rückgängig/i }));
+ expect(onundo).toHaveBeenCalledOnce();
+ });
+
+ it('calls ondismiss after 4 seconds', async () => {
+ const ondismiss = vi.fn();
+ render(UndoBar, { props: { visible: true, message: 'Test', onundo: vi.fn(), ondismiss } });
+ await act(() => { vi.advanceTimersByTime(4000); });
+ expect(ondismiss).toHaveBeenCalledOnce();
+ });
+
+ it('does not call ondismiss before 4 seconds', async () => {
+ const ondismiss = vi.fn();
+ render(UndoBar, { props: { visible: true, message: 'Test', onundo: vi.fn(), ondismiss } });
+ await act(() => { vi.advanceTimersByTime(3999); });
+ expect(ondismiss).not.toHaveBeenCalled();
+ });
+
+ it('has role="status" for accessibility', () => {
+ render(UndoBar, { props: { visible: true, message: 'Test', onundo: vi.fn(), ondismiss: vi.fn() } });
+ expect(screen.getByRole('status')).toBeTruthy();
+ });
+});
diff --git a/frontend/src/test-setup.ts b/frontend/src/test-setup.ts
index bb02c60..147fa4e 100644
--- a/frontend/src/test-setup.ts
+++ b/frontend/src/test-setup.ts
@@ -1 +1,35 @@
import '@testing-library/jest-dom/vitest';
+// Import @testing-library/svelte here so its beforeEach (setup/asyncWrapper=act)
+// is registered before our own beforeEach below.
+import '@testing-library/svelte';
+import { configure } from '@testing-library/dom';
+import { vi } from 'vitest';
+import { tick } from 'svelte';
+import userEvent from '@testing-library/user-event';
+
+// Patch userEvent direct-API methods to use delay:null when fake timers are
+// active. With delay:null, user-event's internal wait() short-circuits
+// (typeof null !== 'number') and no setTimeout is scheduled — so clicks and
+// other interactions work correctly under vi.useFakeTimers().
+const originalClick = userEvent.click.bind(userEvent);
+// @ts-expect-error patching direct API
+userEvent.click = (element: Element, options = {}) => {
+ if (vi.isFakeTimers()) {
+ // @ts-expect-error delay:null is a valid user-event option
+ return originalClick(element, { delay: null, ...options });
+ }
+ return originalClick(element, options);
+};
+
+// Also update asyncWrapper to call tick() after async operations so Svelte
+// DOM updates are flushed. @testing-library/svelte's act() already does this,
+// but we re-configure after it to preserve our fake-timer behaviour.
+beforeEach(() => {
+ configure({
+ asyncWrapper: async (fn: () => Promise) => {
+ const result = await fn();
+ await tick();
+ return result;
+ }
+ });
+});