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; + } + }); +});