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..922dbab2
--- /dev/null
+++ b/frontend/src/components/admin/events/__tests__/EventDetailsModal.test.ts
@@ -0,0 +1,114 @@
+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.each([
+ { case: 'null', value: null },
+ { case: 'undefined', value: undefined },
+ ])('shows "-" when correlation_id is $case', ({ value }) => {
+ const detail = createMockEventDetail();
+ 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__/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..765f2edd
--- /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((1500).toLocaleString())).toBeInTheDocument();
+ expect(screen.getByText(new RegExp(`of ${(10000).toLocaleString()} 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__',
},
},
});