// @vitest-environment jsdom import '@testing-library/jest-dom/vitest'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { HttpResponse, http } from 'msw'; import { setupServer } from 'msw/node'; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { createDashboardRoutes } from '../../apps/dashboard/src/app/router.js'; interface TestStatsOverrides { isRunning?: boolean; recentActivity?: readonly unknown[]; } const server = setupServer(); function createStats(overrides: TestStatsOverrides = {}) { return { isRunning: overrides.isRunning ?? false, dryRunMode: true, totalProcessed: 18, totalMessagesSent: 12, totalResponses: 3, responseRate: 25, messagesThisHour: 2, maxMessagesPerHour: 8, subreddits: [{ name: 'casualconversation', type: 'chat' }], targetGenders: ['female'], recentActivity: overrides.recentActivity ?? [], invitationCooldown: null, }; } function installSuccessfulApi(overrides: TestStatsOverrides = {}) { server.use( http.get('/api/stats', () => HttpResponse.json(createStats(overrides))), http.get('/api/timers', () => HttpResponse.json({ nextMessageAvailableAt: null, hourlyResetAt: Date.now() + 3_600_000, isHourlyLimitReached: false, messagesRemaining: 6, }), ), ); } function renderDashboard(initialPath = '/') { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); const router = createMemoryRouter(createDashboardRoutes(), { initialEntries: [initialPath] }); render( , ); return { queryClient, router }; } beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterEach(() => { cleanup(); server.resetHandlers(); vi.restoreAllMocks(); }); afterAll(() => server.close()); describe('dashboard foundation', () => { it('provides responsive navigation with real routes for every platform area', async () => { installSuccessfulApi(); const user = userEvent.setup(); const { router } = renderDashboard('/'); const navigation = screen.getByRole('navigation', { name: 'Primary navigation' }); for (const label of ['Overview', 'Conversations', 'Media', 'Performance', 'Operations', 'Settings']) { expect(within(navigation).getByRole('link', { name: label })).toBeInTheDocument(); } await user.click(screen.getByRole('button', { name: 'Open navigation' })); expect(screen.getByRole('button', { name: 'Close navigation' })).toHaveAttribute('aria-expanded', 'true'); await user.click(within(navigation).getByRole('link', { name: 'Operations' })); await waitFor(() => expect(router.state.location.pathname).toBe('/operations')); expect(screen.getByRole('heading', { name: 'Operations' })).toBeInTheDocument(); }); it('shows loading then renders stats, timers, and an explicit empty activity state', async () => { let resolveStats: ((response: Response) => void) | undefined; server.use( http.get('/api/stats', async () => new Promise((resolve) => { resolveStats = resolve; }), ), http.get('/api/timers', () => HttpResponse.json({ nextMessageAvailableAt: null, hourlyResetAt: Date.now() + 3_600_000, isHourlyLimitReached: false, messagesRemaining: 6, }), ), ); renderDashboard(); expect(screen.getByRole('status', { name: 'Loading overview' })).toBeInTheDocument(); await waitFor(() => expect(resolveStats).toBeTypeOf('function')); resolveStats?.(HttpResponse.json(createStats())); expect(await screen.findByText('12', { selector: '[data-metric="messages-sent"]' })).toBeInTheDocument(); expect(screen.getByText('6 messages remaining')).toBeInTheDocument(); expect(screen.getByText('No recent activity yet.')).toBeInTheDocument(); expect(screen.getByText('Dry run')).toBeInTheDocument(); }); it('keeps stats visible when timers fail and retries only the failed panel', async () => { let timerRequests = 0; server.use( http.get('/api/stats', () => HttpResponse.json(createStats())), http.get('/api/timers', () => { timerRequests += 1; if (timerRequests === 1) return HttpResponse.json({ error: 'Rate limiter not initialized' }, { status: 503 }); return HttpResponse.json({ nextMessageAvailableAt: null, hourlyResetAt: Date.now() + 3_600_000, isHourlyLimitReached: false, messagesRemaining: 6, }); }), ); const user = userEvent.setup(); renderDashboard(); expect(await screen.findByText('12', { selector: '[data-metric="messages-sent"]' })).toBeInTheDocument(); const alert = await screen.findByRole('alert'); expect(alert).toHaveTextContent('Rate limiter not initialized'); await user.click(within(alert).getByRole('button', { name: 'Retry timers' })); expect(await screen.findByText('6 messages remaining')).toBeInTheDocument(); expect(timerRequests).toBe(2); }); it('shows refresh progress while preserving the current overview', async () => { installSuccessfulApi(); const user = userEvent.setup(); renderDashboard(); const refreshButton = await screen.findByRole('button', { name: 'Refresh overview' }); let resolveRefresh: ((response: Response) => void) | undefined; server.use( http.get('/api/stats', async () => new Promise((resolve) => { resolveRefresh = resolve; }), ), ); await user.click(refreshButton); expect(screen.getByRole('button', { name: 'Refreshing overview' })).toBeDisabled(); expect(screen.getByText('12', { selector: '[data-metric="messages-sent"]' })).toBeInTheDocument(); resolveRefresh?.(HttpResponse.json(createStats())); await waitFor(() => expect(screen.getByRole('button', { name: 'Refresh overview' })).toBeEnabled()); }); it('requires confirmation before starting and reflects the authoritative response', async () => { installSuccessfulApi(); let startRequests = 0; server.use( http.post('/api/start', () => { startRequests += 1; return HttpResponse.json({ success: true, running: true }); }), ); const user = userEvent.setup(); renderDashboard(); await user.click(await screen.findByRole('button', { name: 'Start bot' })); const dialog = screen.getByRole('dialog', { name: 'Start bot?' }); expect(dialog).toHaveTextContent('The bot will resume automated processing.'); await user.click(within(dialog).getByRole('button', { name: 'Cancel' })); expect(startRequests).toBe(0); await user.click(screen.getByRole('button', { name: 'Start bot' })); await user.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Confirm start' })); expect(await screen.findByText('Running', { selector: '[data-bot-status]' })).toBeInTheDocument(); expect(startRequests).toBe(1); }); });