From 8bd501936df58a6b877eb1df9952778492e99ce0 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sat, 7 Feb 2026 22:50:56 +0100 Subject: [PATCH 1/2] test: more tests for admin/events, also added alias for /src/__tests__ - $test --- .../src/components/__tests__/Header.test.ts | 4 +- .../__tests__/NotificationCenter.test.ts | 2 +- .../__tests__/ProtectedRoute.test.ts | 2 +- .../__tests__/EventDetailsModal.test.ts | 111 +++++++++++++ .../events/__tests__/EventFilters.test.ts | 71 ++++++++ .../events/__tests__/EventStatsCards.test.ts | 62 +++++++ .../events/__tests__/EventsTable.test.ts | 138 ++++++++++++++++ .../__tests__/ReplayPreviewModal.test.ts | 119 +++++++++++++ .../__tests__/ReplayProgressBanner.test.ts | 156 ++++++++++++++++++ .../__tests__/UserOverviewModal.test.ts | 137 +++++++++++++++ .../editor/__tests__/LanguageSelect.test.ts | 4 +- .../editor/__tests__/OutputPanel.test.ts | 6 +- .../editor/__tests__/ResourceLimits.test.ts | 4 +- .../editor/__tests__/SavedScripts.test.ts | 4 +- frontend/src/routes/__tests__/Editor.test.ts | 12 +- frontend/src/routes/__tests__/Home.test.ts | 8 +- frontend/src/routes/__tests__/Login.test.ts | 10 +- .../routes/__tests__/Notifications.test.ts | 8 +- .../src/routes/__tests__/Register.test.ts | 10 +- .../src/routes/__tests__/Settings.test.ts | 8 +- .../admin/__tests__/AdminLayout.test.ts | 8 +- .../admin/__tests__/AdminSettings.test.ts | 8 +- frontend/src/stores/__tests__/auth.test.ts | 2 +- frontend/vitest.config.ts | 1 + 24 files changed, 845 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts create mode 100644 frontend/src/components/admin/events/__tests__/EventFilters.test.ts create mode 100644 frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts create mode 100644 frontend/src/components/admin/events/__tests__/EventsTable.test.ts create mode 100644 frontend/src/components/admin/events/__tests__/ReplayPreviewModal.test.ts create mode 100644 frontend/src/components/admin/events/__tests__/ReplayProgressBanner.test.ts create mode 100644 frontend/src/components/admin/events/__tests__/UserOverviewModal.test.ts diff --git a/frontend/src/components/__tests__/Header.test.ts b/frontend/src/components/__tests__/Header.test.ts index 5f29b676..ea397c81 100644 --- a/frontend/src/components/__tests__/Header.test.ts +++ b/frontend/src/components/__tests__/Header.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock, suppressConsoleError } from '../../__tests__/test-utils'; +import { setupAnimationMock, suppressConsoleError } from '$test/test-utils'; // vi.hoisted must contain self-contained code - cannot import external modules const mocks = vi.hoisted(() => { @@ -43,7 +43,7 @@ vi.mock('../../stores/theme.svelte', () => ({ get toggleTheme() { return () => mocks.mockToggleTheme(); }, })); vi.mock('../NotificationCenter.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent( + (await import('$test/test-utils')).createMockSvelteComponent( '
NotificationCenter
', 'notification-center')); import Header from '$components/Header.svelte'; diff --git a/frontend/src/components/__tests__/NotificationCenter.test.ts b/frontend/src/components/__tests__/NotificationCenter.test.ts index f93dfe2d..966985e8 100644 --- a/frontend/src/components/__tests__/NotificationCenter.test.ts +++ b/frontend/src/components/__tests__/NotificationCenter.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '../../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; // Types for mock notification state interface MockNotification { diff --git a/frontend/src/components/__tests__/ProtectedRoute.test.ts b/frontend/src/components/__tests__/ProtectedRoute.test.ts index 76aa56bd..ac10dc2b 100644 --- a/frontend/src/components/__tests__/ProtectedRoute.test.ts +++ b/frontend/src/components/__tests__/ProtectedRoute.test.ts @@ -32,7 +32,7 @@ vi.mock('../../stores/auth.svelte', () => ({ })); vi.mock('../Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent( + (await import('$test/test-utils')).createMockSvelteComponent( '
Loading...
', 'spinner')); import ProtectedRoute from '$components/ProtectedRoute.svelte'; diff --git a/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts b/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts new file mode 100644 index 00000000..49ff5ccd --- /dev/null +++ b/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { setupAnimationMock } from '$test/test-utils'; +import { createMockEventDetail } from '$routes/admin/__tests__/test-utils'; + +vi.mock('@lucide/svelte', async () => + (await import('$test/test-utils')).createMockIconModule('X')); +vi.mock('$components/EventTypeIcon.svelte', async () => + (await import('$test/test-utils')).createMockSvelteComponent('icon')); + +import EventDetailsModal from '../EventDetailsModal.svelte'; + +function renderModal(overrides: Partial<{ + event: ReturnType | null; + open: boolean; +}> = {}) { + const onClose = vi.fn(); + const onReplay = vi.fn(); + const onViewRelated = vi.fn(); + const event = 'event' in overrides ? overrides.event : createMockEventDetail(); + const open = overrides.open ?? true; + const result = render(EventDetailsModal, { props: { event, open, onClose, onReplay, onViewRelated } }); + return { ...result, onClose, onReplay, onViewRelated }; +} + +describe('EventDetailsModal', () => { + beforeEach(() => { + setupAnimationMock(); + vi.clearAllMocks(); + }); + + it('renders nothing when closed', () => { + renderModal({ open: false }); + expect(screen.queryByText('Event Details')).not.toBeInTheDocument(); + }); + + it('renders nothing when event is null', () => { + renderModal({ event: null }); + expect(screen.queryByText('Basic Information')).not.toBeInTheDocument(); + }); + + it('shows modal title and basic information heading when open with event', () => { + renderModal(); + expect(screen.getByText('Event Details')).toBeInTheDocument(); + expect(screen.getByText('Basic Information')).toBeInTheDocument(); + }); + + it.each([ + { label: 'Event ID', value: 'evt-1' }, + { label: 'Event Type', value: 'execution_completed' }, + { label: 'Correlation ID', value: 'corr-123' }, + { label: 'Aggregate ID', value: 'exec-456' }, + ])('displays $label with value "$value"', ({ label, value }) => { + renderModal(); + expect(screen.getByText(label)).toBeInTheDocument(); + expect(screen.getByText(value)).toBeInTheDocument(); + }); + + it('shows full event JSON in pre block', () => { + const detail = createMockEventDetail(); + renderModal({ event: detail }); + expect(screen.getByText('Full Event Data')).toBeInTheDocument(); + const pre = document.querySelector('pre'); + expect(pre?.textContent).toContain('"event_id"'); + expect(pre?.textContent).toContain('"evt-1"'); + }); + + it('shows related events section with clickable buttons', () => { + renderModal(); + expect(screen.getByText('Related Events')).toBeInTheDocument(); + expect(screen.getByText('execution_started')).toBeInTheDocument(); + expect(screen.getByText('pod_created')).toBeInTheDocument(); + }); + + it('calls onViewRelated with event_id when clicking a related event', async () => { + const user = userEvent.setup(); + const { onViewRelated } = renderModal(); + await user.click(screen.getByText('execution_started')); + expect(onViewRelated).toHaveBeenCalledWith('rel-1'); + }); + + it('hides related events section when no related events', () => { + const detail = createMockEventDetail(); + detail.related_events = []; + renderModal({ event: detail }); + expect(screen.queryByText('Related Events')).not.toBeInTheDocument(); + }); + + it('calls onReplay with event_id when Replay Event button is clicked', async () => { + const user = userEvent.setup(); + const { onReplay } = renderModal(); + await user.click(screen.getByRole('button', { name: 'Replay Event' })); + expect(onReplay).toHaveBeenCalledWith('evt-1'); + }); + + it('calls onClose when Close button in footer is clicked', async () => { + const user = userEvent.setup(); + const { onClose } = renderModal(); + await user.click(screen.getByRole('button', { name: 'Close' })); + expect(onClose).toHaveBeenCalledOnce(); + }); + + it('shows "-" when correlation_id is null', () => { + const detail = createMockEventDetail(); + detail.event.metadata.correlation_id = undefined; + renderModal({ event: detail }); + const cells = screen.getAllByText('-'); + expect(cells.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/frontend/src/components/admin/events/__tests__/EventFilters.test.ts b/frontend/src/components/admin/events/__tests__/EventFilters.test.ts new file mode 100644 index 00000000..012e4f8a --- /dev/null +++ b/frontend/src/components/admin/events/__tests__/EventFilters.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { EVENT_TYPES, createDefaultEventFilters } from '$lib/admin/events/eventTypes'; + +import EventFilters from '../EventFilters.svelte'; + +function renderFilters(overrides: Partial<{ onApply: () => void; onClear: () => void }> = {}) { + const onApply = overrides.onApply ?? vi.fn(); + const onClear = overrides.onClear ?? vi.fn(); + const filters = createDefaultEventFilters(); + const result = render(EventFilters, { props: { filters, onApply, onClear } }); + return { ...result, onApply, onClear }; +} + +describe('EventFilters', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders heading and both action buttons', () => { + renderFilters(); + expect(screen.getByText('Filter Events')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Clear All' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument(); + }); + + it.each([ + { id: 'event-types-filter', label: 'Event Types' }, + { id: 'search-filter', label: 'Search' }, + { id: 'correlation-filter', label: 'Correlation ID' }, + { id: 'aggregate-filter', label: 'Aggregate ID' }, + { id: 'user-filter', label: 'User ID' }, + { id: 'service-filter', label: 'Service' }, + { id: 'start-time-filter', label: 'Start Time' }, + { id: 'end-time-filter', label: 'End Time' }, + ])('renders "$label" filter with id=$id', ({ id, label }) => { + renderFilters(); + expect(screen.getByLabelText(label)).toBeInTheDocument(); + expect(document.getElementById(id)).not.toBeNull(); + }); + + it('event types select lists all EVENT_TYPES as options', () => { + renderFilters(); + const select = screen.getByLabelText('Event Types') as HTMLSelectElement; + const options = Array.from(select.options).map(o => o.value); + expect(options).toEqual(EVENT_TYPES); + }); + + it('calls onApply when Apply button is clicked', async () => { + const user = userEvent.setup(); + const { onApply } = renderFilters(); + await user.click(screen.getByRole('button', { name: 'Apply' })); + expect(onApply).toHaveBeenCalledOnce(); + }); + + it('calls onClear when Clear All button is clicked', async () => { + const user = userEvent.setup(); + const { onClear } = renderFilters(); + await user.click(screen.getByRole('button', { name: 'Clear All' })); + expect(onClear).toHaveBeenCalledOnce(); + }); + + it('text inputs accept user input', async () => { + const user = userEvent.setup(); + renderFilters(); + const searchInput = screen.getByLabelText('Search') as HTMLInputElement; + await user.type(searchInput, 'test query'); + expect(searchInput.value).toBe('test query'); + }); +}); diff --git a/frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts b/frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts new file mode 100644 index 00000000..72b733fb --- /dev/null +++ b/frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import { createMockStats } from '$routes/admin/__tests__/test-utils'; + +import EventStatsCards from '../EventStatsCards.svelte'; + +function renderCards(stats: ReturnType | null, totalEvents = 500) { + return render(EventStatsCards, { props: { stats, totalEvents } }); +} + +describe('EventStatsCards', () => { + it('renders nothing when stats is null', () => { + const { container } = renderCards(null); + expect(container.textContent?.trim()).toBe(''); + }); + + it('shows all four stat cards when stats provided', () => { + renderCards(createMockStats()); + expect(screen.getByText('Events (Last 24h)')).toBeInTheDocument(); + expect(screen.getByText('Error Rate (24h)')).toBeInTheDocument(); + expect(screen.getByText('Avg Execution Time (24h)')).toBeInTheDocument(); + expect(screen.getByText('Active Users (24h)')).toBeInTheDocument(); + }); + + it('displays total_events with locale formatting and totalEvents denominator', () => { + renderCards(createMockStats({ total_events: 1500 }), 10000); + expect(screen.getByText('1,500')).toBeInTheDocument(); + expect(screen.getByText(/of 10,000 total/)).toBeInTheDocument(); + }); + + it.each([ + { error_rate: 5, expectedText: '5%', expectedClass: 'text-red-600' }, + { error_rate: 0, expectedText: '0%', expectedClass: 'text-green-600' }, + ])('error rate $error_rate shows "$expectedText" with $expectedClass', ({ error_rate, expectedText, expectedClass }) => { + const { container } = renderCards(createMockStats({ error_rate })); + const el = screen.getByText(expectedText); + expect(el).toBeInTheDocument(); + expect(container.querySelector(`.${expectedClass}`)).not.toBeNull(); + }); + + it('formats avg_processing_time to 2 decimal places', () => { + renderCards(createMockStats({ avg_processing_time: 3.456 })); + expect(screen.getByText('3.46s')).toBeInTheDocument(); + }); + + it('shows "0s" when avg_processing_time is falsy', () => { + renderCards(createMockStats({ avg_processing_time: 0 })); + expect(screen.getByText('0s')).toBeInTheDocument(); + }); + + it('shows active user count from top_users array length', () => { + renderCards(createMockStats({ + top_users: [ + { user_id: 'u1', count: 10 }, + { user_id: 'u2', count: 5 }, + { user_id: 'u3', count: 2 }, + ], + })); + expect(screen.getByText('3')).toBeInTheDocument(); + expect(screen.getByText('with events')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/admin/events/__tests__/EventsTable.test.ts b/frontend/src/components/admin/events/__tests__/EventsTable.test.ts new file mode 100644 index 00000000..d6930b91 --- /dev/null +++ b/frontend/src/components/admin/events/__tests__/EventsTable.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { createMockEvent, createMockEvents } from '$routes/admin/__tests__/test-utils'; + +vi.mock('@lucide/svelte', async () => + (await import('$test/test-utils')).createMockIconModule('Eye', 'Play', 'Trash2')); +vi.mock('$components/EventTypeIcon.svelte', async () => + (await import('$test/test-utils')).createMockSvelteComponent('icon')); + +import EventsTable from '../EventsTable.svelte'; + +function renderTable(events = createMockEvents(3)) { + const onViewDetails = vi.fn(); + const onPreviewReplay = vi.fn(); + const onReplay = vi.fn(); + const onDelete = vi.fn(); + const onViewUser = vi.fn(); + const result = render(EventsTable, { + props: { events, onViewDetails, onPreviewReplay, onReplay, onDelete, onViewUser }, + }); + return { ...result, onViewDetails, onPreviewReplay, onReplay, onDelete, onViewUser }; +} + +describe('EventsTable', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('shows empty state when no events', () => { + renderTable([]); + expect(screen.getByText('No events found')).toBeInTheDocument(); + }); + + it('renders table headers', () => { + renderTable(); + expect(screen.getByText('Time')).toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('User')).toBeInTheDocument(); + expect(screen.getByText('Service')).toBeInTheDocument(); + expect(screen.getByText('Actions')).toBeInTheDocument(); + }); + + it('renders one row per event in desktop table', () => { + const events = createMockEvents(4); + const { container } = renderTable(events); + const rows = container.querySelectorAll('tbody tr'); + expect(rows).toHaveLength(4); + }); + + it('renders mobile cards for each event', () => { + const events = createMockEvents(2); + const { container } = renderTable(events); + const cards = container.querySelectorAll('.mobile-card'); + expect(cards).toHaveLength(2); + }); + + it('displays formatted timestamp in table rows', () => { + const event = createMockEvent({ timestamp: '2024-06-15T14:30:00Z' }); + renderTable([event]); + const date = new Date('2024-06-15T14:30:00Z'); + expect(screen.getByText(date.toLocaleDateString())).toBeInTheDocument(); + expect(screen.getByText(date.toLocaleTimeString())).toBeInTheDocument(); + }); + + it('shows user_id as clickable link when present', () => { + const event = createMockEvent({ metadata: { user_id: 'user-42' } }); + renderTable([event]); + const userButtons = screen.getAllByTitle('View user overview'); + expect(userButtons.length).toBeGreaterThanOrEqual(1); + }); + + it('shows "-" when user_id is null', () => { + const event = createMockEvent({ metadata: { user_id: null } }); + renderTable([event]); + const dashes = screen.getAllByText('-'); + expect(dashes.length).toBeGreaterThanOrEqual(1); + }); + + it('shows service name in service column', () => { + const event = createMockEvent({ metadata: { service_name: 'my-service' } }); + renderTable([event]); + const serviceElements = screen.getAllByText('my-service'); + expect(serviceElements.length).toBeGreaterThanOrEqual(1); + }); + + describe('row click actions', () => { + it('calls onViewDetails when clicking a table row', async () => { + const user = userEvent.setup(); + const events = [createMockEvent({ event_id: 'evt-click' })]; + const { onViewDetails } = renderTable(events); + const rows = screen.getAllByRole('button', { name: 'View event details' }); + await user.click(rows[0]); + expect(onViewDetails).toHaveBeenCalledWith('evt-click'); + }); + + it('calls onViewDetails on Enter keydown on table row', async () => { + const events = [createMockEvent({ event_id: 'evt-key' })]; + const { onViewDetails } = renderTable(events); + const rows = screen.getAllByRole('button', { name: 'View event details' }); + await fireEvent.keyDown(rows[0], { key: 'Enter' }); + expect(onViewDetails).toHaveBeenCalledWith('evt-key'); + }); + }); + + describe('action buttons (stopPropagation)', () => { + it.each([ + { title: 'Preview replay', callback: 'onPreviewReplay' as const }, + { title: 'Replay', callback: 'onReplay' as const }, + { title: 'Delete', callback: 'onDelete' as const }, + ])('$title button calls $callback with event_id without triggering row click', async ({ title, callback }) => { + const user = userEvent.setup(); + const events = [createMockEvent({ event_id: 'evt-action' })]; + const handlers = renderTable(events); + const buttons = screen.getAllByTitle(title); + await user.click(buttons[0]); + expect(handlers[callback]).toHaveBeenCalledWith('evt-action'); + expect(handlers.onViewDetails).not.toHaveBeenCalled(); + }); + }); + + it('calls onViewUser with user_id when clicking user link (stopPropagation)', async () => { + const user = userEvent.setup(); + const event = createMockEvent({ metadata: { user_id: 'user-linked' } }); + const { onViewUser, onViewDetails } = renderTable([event]); + const userButtons = screen.getAllByTitle('View user overview'); + await user.click(userButtons[0]); + expect(onViewUser).toHaveBeenCalledWith('user-linked'); + expect(onViewDetails).not.toHaveBeenCalled(); + }); + + it('shows truncated event_id in tooltip', () => { + const event = createMockEvent({ event_id: 'abcdefgh-1234-5678' }); + renderTable([event]); + const truncated = screen.getAllByText('abcdefgh...'); + expect(truncated.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/frontend/src/components/admin/events/__tests__/ReplayPreviewModal.test.ts b/frontend/src/components/admin/events/__tests__/ReplayPreviewModal.test.ts new file mode 100644 index 00000000..967800fe --- /dev/null +++ b/frontend/src/components/admin/events/__tests__/ReplayPreviewModal.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { setupAnimationMock } from '$test/test-utils'; + +vi.mock('@lucide/svelte', async () => + (await import('$test/test-utils')).createMockIconModule('AlertTriangle', 'X')); + +import ReplayPreviewModal from '../ReplayPreviewModal.svelte'; + +interface ReplayPreview { + eventId: string; + total_events: number; + events_preview?: Array<{ + event_id: string; + event_type: string; + timestamp: string; + aggregate_id?: string; + }>; +} + +function makePreview(overrides: Partial = {}): ReplayPreview { + return { + eventId: 'evt-replay-1', + total_events: 3, + events_preview: [ + { event_id: 'e1', event_type: 'execution_requested', timestamp: '2024-01-15T10:00:00Z' }, + { event_id: 'e2', event_type: 'execution_completed', timestamp: '2024-01-15T10:01:00Z', aggregate_id: 'exec-1' }, + ], + ...overrides, + }; +} + +function renderModal(overrides: Partial<{ preview: ReplayPreview | null; open: boolean }> = {}) { + const onClose = vi.fn(); + const onConfirm = vi.fn(); + const preview = 'preview' in overrides ? overrides.preview : makePreview(); + const open = overrides.open ?? true; + const result = render(ReplayPreviewModal, { props: { preview, open, onClose, onConfirm } }); + return { ...result, onClose, onConfirm }; +} + +describe('ReplayPreviewModal', () => { + beforeEach(() => { + setupAnimationMock(); + vi.clearAllMocks(); + }); + + it('renders nothing when closed', () => { + renderModal({ open: false }); + expect(screen.queryByText('Replay Preview')).not.toBeInTheDocument(); + }); + + it('renders nothing visible when preview is null', () => { + renderModal({ preview: null }); + expect(screen.queryByText(/event.*will be replayed/i)).not.toBeInTheDocument(); + }); + + it('shows modal title and description when open', () => { + renderModal(); + expect(screen.getByText('Replay Preview')).toBeInTheDocument(); + expect(screen.getByText('Review the events that will be replayed')).toBeInTheDocument(); + }); + + it.each([ + { total: 1, expected: '1 event will be replayed' }, + { total: 5, expected: '5 events will be replayed' }, + ])('shows "$expected" for total_events=$total (pluralization)', ({ total, expected }) => { + renderModal({ preview: makePreview({ total_events: total }) }); + expect(screen.getByText(expected)).toBeInTheDocument(); + }); + + it('shows "Dry Run" badge', () => { + renderModal(); + expect(screen.getByText('Dry Run')).toBeInTheDocument(); + }); + + it('lists event previews with event_id, event_type, and aggregate_id', () => { + renderModal(); + expect(screen.getByText('Events to Replay:')).toBeInTheDocument(); + expect(screen.getByText('e1')).toBeInTheDocument(); + expect(screen.getByText('execution_requested')).toBeInTheDocument(); + expect(screen.getByText('e2')).toBeInTheDocument(); + expect(screen.getByText('execution_completed')).toBeInTheDocument(); + expect(screen.getByText('Aggregate: exec-1')).toBeInTheDocument(); + }); + + it('hides events preview section when events_preview is empty', () => { + renderModal({ preview: makePreview({ events_preview: [] }) }); + expect(screen.queryByText('Events to Replay:')).not.toBeInTheDocument(); + }); + + it('hides events preview section when events_preview is undefined', () => { + renderModal({ preview: makePreview({ events_preview: undefined }) }); + expect(screen.queryByText('Events to Replay:')).not.toBeInTheDocument(); + }); + + it('shows warning banner about replay consequences', () => { + renderModal(); + expect(screen.getByText('Warning')).toBeInTheDocument(); + expect(screen.getByText(/Replaying events will re-process them/)).toBeInTheDocument(); + }); + + it('calls onConfirm with eventId and onClose when Proceed is clicked', async () => { + const user = userEvent.setup(); + const { onConfirm, onClose } = renderModal(); + await user.click(screen.getByRole('button', { name: 'Proceed with Replay' })); + expect(onClose).toHaveBeenCalledOnce(); + expect(onConfirm).toHaveBeenCalledWith('evt-replay-1'); + }); + + it('calls onClose when Cancel button is clicked', async () => { + const user = userEvent.setup(); + const { onClose, onConfirm } = renderModal(); + await user.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onClose).toHaveBeenCalledOnce(); + expect(onConfirm).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/components/admin/events/__tests__/ReplayProgressBanner.test.ts b/frontend/src/components/admin/events/__tests__/ReplayProgressBanner.test.ts new file mode 100644 index 00000000..38738ea6 --- /dev/null +++ b/frontend/src/components/admin/events/__tests__/ReplayProgressBanner.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import type { EventReplayStatusResponse } from '$lib/api'; + +vi.mock('@lucide/svelte', async () => + (await import('$test/test-utils')).createMockIconModule('X')); + +import ReplayProgressBanner from '../ReplayProgressBanner.svelte'; + +function makeSession(overrides: Partial = {}): EventReplayStatusResponse { + return { + session_id: 'session-1', + status: 'in_progress', + total_events: 10, + replayed_events: 5, + progress_percentage: 50, + ...overrides, + }; +} + +function renderBanner(session: EventReplayStatusResponse | null = makeSession()) { + const onClose = vi.fn(); + const result = render(ReplayProgressBanner, { props: { session, onClose } }); + return { ...result, onClose }; +} + +describe('ReplayProgressBanner', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders nothing when session is null', () => { + const { container } = renderBanner(null); + expect(container.textContent?.trim()).toBe(''); + }); + + it('shows title and status', () => { + renderBanner(makeSession({ status: 'in_progress' })); + expect(screen.getByText('Replay in Progress')).toBeInTheDocument(); + expect(screen.getByText('in_progress')).toBeInTheDocument(); + }); + + it('shows progress text and percentage', () => { + renderBanner(makeSession({ replayed_events: 7, total_events: 20, progress_percentage: 35 })); + expect(screen.getByText('Progress: 7 / 20 events')).toBeInTheDocument(); + expect(screen.getByText('35%')).toBeInTheDocument(); + }); + + it('renders progress bar with correct width', () => { + const { container } = renderBanner(makeSession({ progress_percentage: 75 })); + const bar = container.querySelector('.bg-blue-600'); + expect(bar).not.toBeNull(); + expect(bar?.getAttribute('style')).toContain('width: 75%'); + }); + + it('clamps progress bar width to 100%', () => { + const { container } = renderBanner(makeSession({ progress_percentage: 150 })); + const bar = container.querySelector('.bg-blue-600'); + expect(bar?.getAttribute('style')).toContain('width: 100%'); + }); + + it('calls onClose when close button is clicked', async () => { + const user = userEvent.setup(); + const { onClose } = renderBanner(); + await user.click(screen.getByTitle('Close')); + expect(onClose).toHaveBeenCalledOnce(); + }); + + describe('errors', () => { + it('hides error section when no errors', () => { + renderBanner(makeSession({ errors: [] })); + expect(screen.queryByText(/error/i)).not.toBeInTheDocument(); + }); + + it('shows errors with event_id and error message', () => { + renderBanner(makeSession({ + errors: [ + { event_id: 'err-evt-1', error: 'Timeout exceeded' }, + { error: 'Connection refused' }, + ], + })); + expect(screen.getByText('err-evt-1')).toBeInTheDocument(); + expect(screen.getByText('Timeout exceeded')).toBeInTheDocument(); + expect(screen.getByText('Connection refused')).toBeInTheDocument(); + }); + }); + + describe('execution results', () => { + it('hides execution results section when not present', () => { + renderBanner(makeSession()); + expect(screen.queryByText('Execution Results:')).not.toBeInTheDocument(); + }); + + it('shows execution results with status badges and execution_id', () => { + renderBanner(makeSession({ + execution_results: [ + { + execution_id: 'exec-r1', + status: 'completed', + stdout: 'hello', + stderr: null, + lang: 'python', + lang_version: '3.11', + resource_usage: { execution_time_wall_seconds: 1.23, cpu_time_jiffies: 10, peak_memory_kb: 1024 }, + exit_code: 0, + error_type: null, + }, + ], + })); + expect(screen.getByText('Execution Results:')).toBeInTheDocument(); + expect(screen.getByText('exec-r1')).toBeInTheDocument(); + expect(screen.getByText('completed')).toBeInTheDocument(); + expect(screen.getByText('1.23s')).toBeInTheDocument(); + expect(screen.getByText('hello')).toBeInTheDocument(); + }); + + it.each([ + { status: 'completed', expectedClass: 'bg-green-100' }, + { status: 'failed', expectedClass: 'bg-red-100' }, + { status: 'running', expectedClass: 'bg-yellow-100' }, + ])('status "$status" shows badge with $expectedClass', ({ status, expectedClass }) => { + const { container } = renderBanner(makeSession({ + execution_results: [{ + execution_id: 'exec-x', + status, + stdout: null, + stderr: null, + lang: 'python', + lang_version: '3.11', + resource_usage: null, + exit_code: 0, + error_type: null, + }], + })); + expect(container.querySelector(`.${expectedClass}`)).not.toBeNull(); + }); + + it('shows stderr in red when present', () => { + renderBanner(makeSession({ + execution_results: [{ + execution_id: 'exec-err', + status: 'failed', + stdout: null, + stderr: 'NameError', + lang: 'python', + lang_version: '3.11', + resource_usage: null, + exit_code: 1, + error_type: null, + }], + })); + expect(screen.getByText('NameError')).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/admin/events/__tests__/UserOverviewModal.test.ts b/frontend/src/components/admin/events/__tests__/UserOverviewModal.test.ts new file mode 100644 index 00000000..a5744e9c --- /dev/null +++ b/frontend/src/components/admin/events/__tests__/UserOverviewModal.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import { setupAnimationMock } from '$test/test-utils'; +import { createMockUserOverview } from '$routes/admin/__tests__/test-utils'; + +vi.mock('@lucide/svelte', async () => + (await import('$test/test-utils')).createMockIconModule('X')); +vi.mock('$components/Spinner.svelte', async () => + (await import('$test/test-utils')).createMockSvelteComponent('
Loading...
', 'spinner')); +vi.mock('$components/EventTypeIcon.svelte', async () => + (await import('$test/test-utils')).createMockSvelteComponent('icon')); + +import UserOverviewModal from '../UserOverviewModal.svelte'; + +type UserOverview = ReturnType; + +function renderModal(overrides: Partial<{ + overview: UserOverview | null; + loading: boolean; + open: boolean; +}> = {}) { + const onClose = vi.fn(); + const result = render(UserOverviewModal, { + props: { + overview: 'overview' in overrides ? overrides.overview : createMockUserOverview(), + loading: overrides.loading ?? false, + open: overrides.open ?? true, + onClose, + }, + }); + return { ...result, onClose }; +} + +describe('UserOverviewModal', () => { + beforeEach(() => { + setupAnimationMock(); + vi.clearAllMocks(); + }); + + it('renders nothing when closed', () => { + renderModal({ open: false }); + expect(screen.queryByText('User Overview')).not.toBeInTheDocument(); + }); + + it('shows loading state and hides overview data when loading', () => { + renderModal({ loading: true, overview: null }); + expect(screen.queryByText('Profile')).not.toBeInTheDocument(); + expect(screen.queryByText('Execution Stats (last 24h)')).not.toBeInTheDocument(); + }); + + it('shows "No data available" when overview is null and not loading', () => { + renderModal({ overview: null }); + expect(screen.getByText('No data available')).toBeInTheDocument(); + }); + + describe('profile section', () => { + it.each([ + { label: 'User ID:', value: 'user-1' }, + { label: 'Username:', value: 'testuser' }, + { label: 'Email:', value: 'test@example.com' }, + { label: 'Role:', value: 'user' }, + ])('shows $label $value', ({ label, value }) => { + renderModal(); + expect(screen.getByText(label)).toBeInTheDocument(); + expect(screen.getByText(value)).toBeInTheDocument(); + }); + + it('shows Active: Yes and Superuser: No', () => { + renderModal(); + expect(screen.getByText('Active:')).toBeInTheDocument(); + expect(screen.getByText('Superuser:')).toBeInTheDocument(); + // 'No' appears for Superuser, Bypass, and Custom Rules — use getAllByText + const noElements = screen.getAllByText('No'); + expect(noElements.length).toBeGreaterThanOrEqual(1); + }); + }); + + describe('execution stats', () => { + it('shows all stat card labels', () => { + renderModal(); + expect(screen.getByText('Execution Stats (last 24h)')).toBeInTheDocument(); + expect(screen.getByText('Succeeded')).toBeInTheDocument(); + expect(screen.getByText('80')).toBeInTheDocument(); + expect(screen.getByText('Failed')).toBeInTheDocument(); + expect(screen.getByText('10')).toBeInTheDocument(); + expect(screen.getByText('Timeout')).toBeInTheDocument(); + expect(screen.getByText('Cancelled')).toBeInTheDocument(); + }); + + it('shows terminal total and total events labels', () => { + renderModal(); + expect(screen.getByText(/Terminal Total:/)).toBeInTheDocument(); + expect(screen.getByText(/Total Events:/)).toBeInTheDocument(); + // Both terminal_total and stats.total_events are 100 — verify at least one renders + const hundreds = screen.getAllByText('100'); + expect(hundreds).toHaveLength(2); + }); + }); + + describe('rate limits', () => { + it('shows rate limit section with values', () => { + renderModal(); + expect(screen.getByText('Rate Limits')).toBeInTheDocument(); + expect(screen.getByText('Bypass:')).toBeInTheDocument(); + expect(screen.getByText('Global Multiplier:')).toBeInTheDocument(); + expect(screen.getByText('Custom Rules:')).toBeInTheDocument(); + }); + + it('hides rate limit section when rate_limit_summary is null', () => { + const overview = createMockUserOverview(); + (overview as Record).rate_limit_summary = null; + renderModal({ overview }); + expect(screen.queryByText('Rate Limits')).not.toBeInTheDocument(); + }); + }); + + describe('recent events', () => { + it('shows recent events list when present', () => { + renderModal(); + expect(screen.getByText('Recent Execution Events')).toBeInTheDocument(); + }); + + it('hides recent events section when empty', () => { + const overview = createMockUserOverview(); + overview.recent_events = []; + renderModal({ overview }); + expect(screen.queryByText('Recent Execution Events')).not.toBeInTheDocument(); + }); + }); + + it('renders footer link to user management', () => { + renderModal(); + const link = screen.getByRole('link', { name: 'Open User Management' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/admin/users'); + }); +}); diff --git a/frontend/src/components/editor/__tests__/LanguageSelect.test.ts b/frontend/src/components/editor/__tests__/LanguageSelect.test.ts index 3e664469..4c3a2c9e 100644 --- a/frontend/src/components/editor/__tests__/LanguageSelect.test.ts +++ b/frontend/src/components/editor/__tests__/LanguageSelect.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, within, fireEvent, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '../../../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; vi.mock('@lucide/svelte', async () => - (await import('../../../__tests__/test-utils')).createMockIconModule('ChevronDown', 'ChevronRight')); + (await import('$test/test-utils')).createMockIconModule('ChevronDown', 'ChevronRight')); import LanguageSelect from '../LanguageSelect.svelte'; diff --git a/frontend/src/components/editor/__tests__/OutputPanel.test.ts b/frontend/src/components/editor/__tests__/OutputPanel.test.ts index 39853c30..20f2dfe2 100644 --- a/frontend/src/components/editor/__tests__/OutputPanel.test.ts +++ b/frontend/src/components/editor/__tests__/OutputPanel.test.ts @@ -9,11 +9,11 @@ const mocks = vi.hoisted(() => ({ })); vi.mock('@lucide/svelte', async () => - (await import('../../../__tests__/test-utils')).createMockIconModule('AlertTriangle', 'FileText', 'Copy')); + (await import('$test/test-utils')).createMockIconModule('AlertTriangle', 'FileText', 'Copy')); vi.mock('svelte-sonner', async () => - (await import('../../../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$components/Spinner.svelte', async () => - (await import('../../../__tests__/test-utils')).createMockSvelteComponent('
Loading...
', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('
Loading...
', 'spinner')); import OutputPanel from '../OutputPanel.svelte'; diff --git a/frontend/src/components/editor/__tests__/ResourceLimits.test.ts b/frontend/src/components/editor/__tests__/ResourceLimits.test.ts index bb00ce8d..a2a74050 100644 --- a/frontend/src/components/editor/__tests__/ResourceLimits.test.ts +++ b/frontend/src/components/editor/__tests__/ResourceLimits.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '../../../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; vi.mock('@lucide/svelte', async () => - (await import('../../../__tests__/test-utils')).createMockIconModule( + (await import('$test/test-utils')).createMockIconModule( 'MessageSquare', 'ChevronUp', 'ChevronDown', 'Cpu', 'MemoryStick', 'Clock', )); diff --git a/frontend/src/components/editor/__tests__/SavedScripts.test.ts b/frontend/src/components/editor/__tests__/SavedScripts.test.ts index 8ae32a4a..bcbc8c82 100644 --- a/frontend/src/components/editor/__tests__/SavedScripts.test.ts +++ b/frontend/src/components/editor/__tests__/SavedScripts.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '../../../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; vi.mock('@lucide/svelte', async () => - (await import('../../../__tests__/test-utils')).createMockIconModule('List', 'Trash2')); + (await import('$test/test-utils')).createMockIconModule('List', 'Trash2')); import SavedScripts from '../SavedScripts.svelte'; diff --git a/frontend/src/routes/__tests__/Editor.test.ts b/frontend/src/routes/__tests__/Editor.test.ts index b09d4c28..871b7c03 100644 --- a/frontend/src/routes/__tests__/Editor.test.ts +++ b/frontend/src/routes/__tests__/Editor.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '$lib/../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; function createMockLimits() { return { @@ -67,7 +67,7 @@ vi.mock('$stores/userSettings.svelte', () => ({ })); vi.mock('svelte-sonner', async () => - (await import('$lib/../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$lib/api-interceptors', () => ({ unwrap: (...args: unknown[]) => mocks.mockUnwrap(...args), @@ -75,22 +75,22 @@ vi.mock('$lib/api-interceptors', () => ({ })); vi.mock('$utils/meta', async () => - (await import('$lib/../__tests__/test-utils')).createMetaMock( + (await import('$test/test-utils')).createMetaMock( mocks.mockUpdateMetaTags, { editor: { title: 'Code Editor', description: 'Editor desc' } })); vi.mock('$lib/editor', () => ({ createExecutionState: () => mocks.mockExecutionState })); vi.mock('$components/Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent('', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('', 'spinner')); vi.mock('@lucide/svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockIconModule( + (await import('$test/test-utils')).createMockIconModule( 'CirclePlay', 'Settings', 'Lightbulb', 'FilePlus', 'Upload', 'Download', 'Save', 'List', 'Trash2')); vi.mock('$components/editor', async () => { - const utils = await import('$lib/../__tests__/test-utils'); + const utils = await import('$test/test-utils'); const components = utils.createMockNamedComponents({ OutputPanel: '
Execution Output
', LanguageSelect: '
', diff --git a/frontend/src/routes/__tests__/Home.test.ts b/frontend/src/routes/__tests__/Home.test.ts index 081f5f67..a8f88ac9 100644 --- a/frontend/src/routes/__tests__/Home.test.ts +++ b/frontend/src/routes/__tests__/Home.test.ts @@ -1,20 +1,20 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { setupAnimationMock } from '$lib/../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; const mocks = vi.hoisted(() => ({ mockUpdateMetaTags: vi.fn(), })); vi.mock('@mateothegreat/svelte5-router', async () => - (await import('$lib/../__tests__/test-utils')).createMockRouterModule()); + (await import('$test/test-utils')).createMockRouterModule()); vi.mock('$utils/meta', async () => - (await import('$lib/../__tests__/test-utils')).createMetaMock( + (await import('$test/test-utils')).createMetaMock( mocks.mockUpdateMetaTags, { home: { title: 'Home', description: 'Home desc' } })); vi.mock('@lucide/svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockIconModule('Zap', 'ShieldCheck', 'Clock')); + (await import('$test/test-utils')).createMockIconModule('Zap', 'ShieldCheck', 'Clock')); describe('Home', () => { beforeEach(() => { diff --git a/frontend/src/routes/__tests__/Login.test.ts b/frontend/src/routes/__tests__/Login.test.ts index 3cb76056..18fc8dcd 100644 --- a/frontend/src/routes/__tests__/Login.test.ts +++ b/frontend/src/routes/__tests__/Login.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '$lib/../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; const mocks = vi.hoisted(() => ({ mockLogin: vi.fn(), @@ -22,10 +22,10 @@ const mocks = vi.hoisted(() => ({ vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); vi.mock('@mateothegreat/svelte5-router', async () => - (await import('$lib/../__tests__/test-utils')).createMockRouterModule(mocks.mockGoto)); + (await import('$test/test-utils')).createMockRouterModule(mocks.mockGoto)); vi.mock('svelte-sonner', async () => - (await import('$lib/../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$lib/user-settings', () => ({ loadUserSettings: (...args: unknown[]) => mocks.mockLoadUserSettings(...args), @@ -36,11 +36,11 @@ vi.mock('$lib/api-interceptors', () => ({ })); vi.mock('$utils/meta', async () => - (await import('$lib/../__tests__/test-utils')).createMetaMock( + (await import('$test/test-utils')).createMetaMock( mocks.mockUpdateMetaTags, { login: { title: 'Login', description: 'Login desc' } })); vi.mock('$components/Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent('Loading', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('Loading', 'spinner')); describe('Login', () => { const user = userEvent.setup(); diff --git a/frontend/src/routes/__tests__/Notifications.test.ts b/frontend/src/routes/__tests__/Notifications.test.ts index f7771d66..202097c5 100644 --- a/frontend/src/routes/__tests__/Notifications.test.ts +++ b/frontend/src/routes/__tests__/Notifications.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock, createMockNotification, createMockNotifications } from '$lib/../__tests__/test-utils'; +import { setupAnimationMock, createMockNotification, createMockNotifications } from '$test/test-utils'; const mocks = vi.hoisted(() => ({ addToast: vi.fn(), @@ -20,13 +20,13 @@ const mocks = vi.hoisted(() => ({ vi.mock('$stores/notificationStore.svelte', () => ({ notificationStore: mocks.mockNotificationStore })); vi.mock('svelte-sonner', async () => - (await import('$lib/../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$components/Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent('Loading', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('Loading', 'spinner')); vi.mock('@lucide/svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockIconModule( + (await import('$test/test-utils')).createMockIconModule( 'Bell', 'Trash2', 'Clock', 'CircleCheck', 'AlertCircle', 'Info')); vi.mock('$lib/api', () => ({})); diff --git a/frontend/src/routes/__tests__/Register.test.ts b/frontend/src/routes/__tests__/Register.test.ts index c0f5ed1b..e92d5d5c 100644 --- a/frontend/src/routes/__tests__/Register.test.ts +++ b/frontend/src/routes/__tests__/Register.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '$lib/../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; const mocks = vi.hoisted(() => ({ registerApiV1AuthRegisterPost: vi.fn(), @@ -16,21 +16,21 @@ vi.mock('$lib/api', () => ({ })); vi.mock('@mateothegreat/svelte5-router', async () => - (await import('$lib/../__tests__/test-utils')).createMockRouterModule(mocks.mockGoto)); + (await import('$test/test-utils')).createMockRouterModule(mocks.mockGoto)); vi.mock('svelte-sonner', async () => - (await import('$lib/../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$lib/api-interceptors', () => ({ getErrorMessage: (...args: unknown[]) => mocks.mockGetErrorMessage(...args), })); vi.mock('$utils/meta', async () => - (await import('$lib/../__tests__/test-utils')).createMetaMock( + (await import('$test/test-utils')).createMetaMock( mocks.mockUpdateMetaTags, { register: { title: 'Register', description: 'Register desc' } })); vi.mock('$components/Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent('Loading', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('Loading', 'spinner')); describe('Register', () => { const user = userEvent.setup(); diff --git a/frontend/src/routes/__tests__/Settings.test.ts b/frontend/src/routes/__tests__/Settings.test.ts index cd2db7e2..5e5a820c 100644 --- a/frontend/src/routes/__tests__/Settings.test.ts +++ b/frontend/src/routes/__tests__/Settings.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import { setupAnimationMock } from '$lib/../__tests__/test-utils'; +import { setupAnimationMock } from '$test/test-utils'; function createMockSettings() { return { @@ -56,13 +56,13 @@ vi.mock('$stores/userSettings.svelte', () => ({ })); vi.mock('svelte-sonner', async () => - (await import('$lib/../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$components/Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent('Loading', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('Loading', 'spinner')); vi.mock('@lucide/svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockIconModule('ChevronDown')); + (await import('$test/test-utils')).createMockIconModule('ChevronDown')); describe('Settings', () => { const user = userEvent.setup(); diff --git a/frontend/src/routes/admin/__tests__/AdminLayout.test.ts b/frontend/src/routes/admin/__tests__/AdminLayout.test.ts index b5e65757..68a93cee 100644 --- a/frontend/src/routes/admin/__tests__/AdminLayout.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminLayout.test.ts @@ -19,16 +19,16 @@ const mocks = vi.hoisted(() => ({ vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); vi.mock('@mateothegreat/svelte5-router', async () => - (await import('$lib/../__tests__/test-utils')).createMockRouterModule(mocks.mockGoto)); + (await import('$test/test-utils')).createMockRouterModule(mocks.mockGoto)); vi.mock('svelte-sonner', async () => - (await import('$lib/../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$components/Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent('Loading', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('Loading', 'spinner')); vi.mock('@lucide/svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockIconModule('ShieldCheck')); + (await import('$test/test-utils')).createMockIconModule('ShieldCheck')); describe('AdminLayout', () => { beforeEach(() => { diff --git a/frontend/src/routes/admin/__tests__/AdminSettings.test.ts b/frontend/src/routes/admin/__tests__/AdminSettings.test.ts index f60457e6..3f776769 100644 --- a/frontend/src/routes/admin/__tests__/AdminSettings.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminSettings.test.ts @@ -49,19 +49,19 @@ vi.mock('../../../lib/api', () => ({ vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); vi.mock('@mateothegreat/svelte5-router', async () => - (await import('$lib/../__tests__/test-utils')).createMockRouterModule()); + (await import('$test/test-utils')).createMockRouterModule()); vi.mock('svelte-sonner', async () => - (await import('$lib/../__tests__/test-utils')).createToastMock(mocks.addToast)); + (await import('$test/test-utils')).createToastMock(mocks.addToast)); vi.mock('$routes/admin/AdminLayout.svelte', () => import('$routes/admin/__tests__/mocks/MockAdminLayout.svelte')); vi.mock('$components/Spinner.svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockSvelteComponent('Loading', 'spinner')); + (await import('$test/test-utils')).createMockSvelteComponent('Loading', 'spinner')); vi.mock('@lucide/svelte', async () => - (await import('$lib/../__tests__/test-utils')).createMockIconModule('ShieldCheck')); + (await import('$test/test-utils')).createMockIconModule('ShieldCheck')); describe('AdminSettings', () => { const user = userEvent.setup(); diff --git a/frontend/src/stores/__tests__/auth.test.ts b/frontend/src/stores/__tests__/auth.test.ts index 52f643c7..6bfc7389 100644 --- a/frontend/src/stores/__tests__/auth.test.ts +++ b/frontend/src/stores/__tests__/auth.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { suppressConsoleError, suppressConsoleWarn } from '../../__tests__/test-utils'; +import { suppressConsoleError, suppressConsoleWarn } from '$test/test-utils'; // Mock the API functions const mockLoginApi = vi.fn(); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index febf1f9e..f7a164af 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ $routes: '/src/routes', $utils: '/src/utils', $styles: '/src/styles', + $test: '/src/__tests__', }, }, }); From 84cfa373c0dbba03be49b37e02266680571bce4b Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sat, 7 Feb 2026 22:59:54 +0100 Subject: [PATCH 2/2] test: fixes --- .../admin/events/__tests__/EventDetailsModal.test.ts | 7 +++++-- .../admin/events/__tests__/EventStatsCards.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts b/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts index 49ff5ccd..922dbab2 100644 --- a/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts +++ b/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts @@ -101,9 +101,12 @@ describe('EventDetailsModal', () => { expect(onClose).toHaveBeenCalledOnce(); }); - it('shows "-" when correlation_id is null', () => { + it.each([ + { case: 'null', value: null }, + { case: 'undefined', value: undefined }, + ])('shows "-" when correlation_id is $case', ({ value }) => { const detail = createMockEventDetail(); - detail.event.metadata.correlation_id = undefined; + detail.event.metadata.correlation_id = value as undefined; renderModal({ event: detail }); const cells = screen.getAllByText('-'); expect(cells.length).toBeGreaterThanOrEqual(1); diff --git a/frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts b/frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts index 72b733fb..765f2edd 100644 --- a/frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts +++ b/frontend/src/components/admin/events/__tests__/EventStatsCards.test.ts @@ -24,8 +24,8 @@ describe('EventStatsCards', () => { it('displays total_events with locale formatting and totalEvents denominator', () => { renderCards(createMockStats({ total_events: 1500 }), 10000); - expect(screen.getByText('1,500')).toBeInTheDocument(); - expect(screen.getByText(/of 10,000 total/)).toBeInTheDocument(); + expect(screen.getByText((1500).toLocaleString())).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`of ${(10000).toLocaleString()} total`))).toBeInTheDocument(); }); it.each([