import { act, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // Mock next-intl — passthrough t(key) => key vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key, })); // Mock @/lib/dashboard-api — spy on updateWidgetConfig vi.mock('@/lib/dashboard-api', () => ({ updateWidgetConfig: vi.fn().mockResolvedValue(undefined), })); // Import mocked function for assertions import { updateWidgetConfig } from '@/lib/dashboard-api'; // Must import AFTER mocks import { StopwatchWidget } from './stopwatch-widget'; describe('StopwatchWidget', () => { beforeEach(() => { // Use fake timers for deterministic testing vi.useFakeTimers(); vi.clearAllMocks(); }); afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); }); it('renders 00:00 and Start button with empty config', () => { render(); expect(screen.getByTestId('stopwatch-display')).toHaveTextContent('00:00'); // Button with accessible name matching the translation key 'stopwatch.start' expect( screen.getByRole('button', { name: /stopwatch\.start/i }), ).toBeInTheDocument(); }); it('clicking Start persists state=running with startedAt ISO string', async () => { render(); const startBtn = screen.getByRole('button', { name: /stopwatch\.start/i }); await act(async () => { fireEvent.click(startBtn); }); expect(updateWidgetConfig).toHaveBeenCalledWith( 'sw-1', expect.objectContaining({ state: 'running', startedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/), }), expect.anything(), ); }); it('advances display after 3000ms while running', async () => { render(); await act(async () => { fireEvent.click(screen.getByRole('button', { name: /stopwatch\.start/i })); }); await act(async () => { vi.advanceTimersByTime(3000); }); const display = screen.getByTestId('stopwatch-display').textContent ?? ''; // Display should show 00:03 (or similar — just not 00:00) expect(display).not.toBe('00:00'); }); it('Stop/Pause freezes display and persists state=paused', async () => { render(); // Start the stopwatch await act(async () => { fireEvent.click(screen.getByRole('button', { name: /stopwatch\.start/i })); }); await act(async () => { vi.advanceTimersByTime(2000); }); // Stop it await act(async () => { fireEvent.click(screen.getByRole('button', { name: /stopwatch\.stop/i })); }); expect(updateWidgetConfig).toHaveBeenLastCalledWith( 'sw-1', expect.objectContaining({ state: 'paused', startedAt: null, elapsed: expect.any(Number), }), expect.anything(), ); }); it('Reset returns display to 00:00 and persists state=stopped with elapsed=0 laps=[]', async () => { render(); await act(async () => { fireEvent.click(screen.getByRole('button', { name: /stopwatch\.start/i })); }); await act(async () => { vi.advanceTimersByTime(1500); }); await act(async () => { fireEvent.click(screen.getByRole('button', { name: /stopwatch\.reset/i })); }); expect(screen.getByTestId('stopwatch-display')).toHaveTextContent('00:00'); expect(updateWidgetConfig).toHaveBeenLastCalledWith( 'sw-1', expect.objectContaining({ state: 'stopped', startedAt: null, elapsed: 0, laps: [], }), expect.anything(), ); }); it('Lap while running appends elapsed to laps list', async () => { render(); await act(async () => { fireEvent.click(screen.getByRole('button', { name: /stopwatch\.start/i })); }); await act(async () => { vi.advanceTimersByTime(1000); }); await act(async () => { fireEvent.click(screen.getByRole('button', { name: /stopwatch\.lap/i })); }); // At least one lap entry visible expect(updateWidgetConfig).toHaveBeenLastCalledWith( 'sw-1', expect.objectContaining({ laps: expect.arrayContaining([expect.any(Number)]), }), expect.anything(), ); // Lap entry rendered in the DOM const lapItems = screen.getAllByRole('listitem'); expect(lapItems.length).toBeGreaterThanOrEqual(1); }); it('reload reconstruction: renders ~7000ms elapsed from config with startedAt 5s ago', async () => { // Fix system time so Date.now() is deterministic const fixedNow = new Date('2026-07-01T12:00:00.000Z'); vi.setSystemTime(fixedNow); // startedAt is 5 seconds before fixedNow const startedAt = new Date(fixedNow.getTime() - 5000).toISOString(); // Render with a running config: 2000ms already accumulated + 5s since startedAt = ~7000ms total render( , ); // Tick the interval once await act(async () => { vi.advanceTimersByTime(100); }); const display = screen.getByTestId('stopwatch-display').textContent ?? ''; // 7000ms = 7 seconds → should show 00:07 // More precisely: display must not show 00:00, 00:01, or 00:02 (which would ignore startedAt) // Accept any display > 5s const parts = display.split(':'); const seconds = parseInt(parts[parts.length - 1] ?? '0', 10); const minutes = parseInt(parts[parts.length - 2] ?? '0', 10); const totalSeconds = minutes * 60 + seconds; expect(totalSeconds).toBeGreaterThanOrEqual(5); }); it('quick-260916-bwo: die Anzeige skaliert per Container-Query-Klasse, keine Fensterbreiten-Formel im Style', () => { render(); const display = screen.getByTestId('stopwatch-display'); expect(display.className).toMatch(/cqw/); expect(display.className).toMatch(/cqh/); expect(display.style.fontSize).toBe(''); }); });