174 lines
8.3 KiB
TypeScript
174 lines
8.3 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import '@testing-library/jest-dom/vitest';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { cleanup, render, screen, within } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { HttpResponse, http } from 'msw';
|
|
import { setupServer } from 'msw/node';
|
|
import type { PropsWithChildren, ReactElement } from 'react';
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import { OperationsPage } from '../../apps/dashboard/src/features/operations/operations-page.js';
|
|
import { PerformancePage } from '../../apps/dashboard/src/features/performance/performance-page.js';
|
|
|
|
const server = setupServer();
|
|
|
|
function TestProviders({ children }: PropsWithChildren) {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
});
|
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
}
|
|
|
|
function renderPage(element: ReactElement) {
|
|
return render(element, { wrapper: TestProviders });
|
|
}
|
|
|
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
|
afterEach(() => {
|
|
cleanup();
|
|
server.resetHandlers();
|
|
});
|
|
afterAll(() => server.close());
|
|
|
|
describe('PerformancePage', () => {
|
|
it('shows sample sizes, confidence warnings, useful comparisons, and successful examples', async () => {
|
|
server.use(
|
|
http.get('/api/analytics/by-subreddit', () =>
|
|
HttpResponse.json([
|
|
{ subreddit: 'askuk', totalSent: 40, totalResponses: 12, responseRate: 30 },
|
|
{ subreddit: 'tiny', totalSent: 3, totalResponses: 2, responseRate: 67 },
|
|
]),
|
|
),
|
|
http.get('/api/analytics/by-strategy', () =>
|
|
HttpResponse.json([
|
|
{ strategyId: 'friendly', totalSent: 25, totalResponses: 10, responseRate: 40, configured: true },
|
|
{ strategyId: 'new-test', totalSent: 2, totalResponses: 1, responseRate: 50 },
|
|
]),
|
|
),
|
|
http.get('/api/analytics/trend', ({ request }) => {
|
|
expect(new URL(request.url).searchParams.get('days')).toBe('30');
|
|
return HttpResponse.json([
|
|
{ date: '2026-08-07', totalSent: 10, totalResponses: 2, responseRate: 20 },
|
|
{ date: '2026-08-08', totalSent: 8, totalResponses: 4, responseRate: 50 },
|
|
]);
|
|
}),
|
|
http.get('/api/successful', () =>
|
|
HttpResponse.json([
|
|
{
|
|
id: 7,
|
|
sentMessageId: 22,
|
|
originalMessage: 'A thoughtful opener',
|
|
imageAnalysis: null,
|
|
postTitle: 'Weekend plans',
|
|
responseSnippet: 'That sounds lovely!',
|
|
subreddit: 'askuk',
|
|
createdAt: Date.parse('2026-08-08T10:00:00Z'),
|
|
},
|
|
]),
|
|
),
|
|
);
|
|
|
|
renderPage(<PerformancePage />);
|
|
|
|
expect(await screen.findByRole('heading', { name: 'Performance' })).toBeInTheDocument();
|
|
const subredditTable = screen.getByRole('table', { name: 'Subreddit performance' });
|
|
expect(within(subredditTable).getByText('n = 40')).toBeInTheDocument();
|
|
expect(within(subredditTable).getByText('n = 3')).toBeInTheDocument();
|
|
expect(within(subredditTable).getByText('Low confidence')).toBeInTheDocument();
|
|
const strategyTable = screen.getByRole('table', { name: 'Strategy performance' });
|
|
expect(within(strategyTable).getByText('n = 25')).toBeInTheDocument();
|
|
expect(within(strategyTable).getByText('Configured')).toBeInTheDocument();
|
|
expect(screen.getByRole('list', { name: '30 day response trend' })).toHaveTextContent('50%');
|
|
expect(screen.getByRole('article', { name: 'Successful example from askuk' })).toHaveTextContent('A thoughtful opener');
|
|
expect(screen.getByRole('article', { name: 'Successful example from askuk' })).toHaveTextContent('That sounds lovely!');
|
|
});
|
|
});
|
|
|
|
describe('OperationsPage', () => {
|
|
it('keeps readable activity, job state, and raw searchable logs separate', async () => {
|
|
server.use(
|
|
http.get('/api/activity', () =>
|
|
HttpResponse.json([
|
|
{ timestamp: Date.parse('2026-08-08T12:00:00Z'), type: 'response', message: 'Reply from u/alex', recipient: 'alex' },
|
|
]),
|
|
),
|
|
http.get('/api/logs', ({ request }) => {
|
|
expect(new URL(request.url).searchParams.get('limit')).toBe('200');
|
|
return HttpResponse.json([
|
|
{ timestamp: Date.parse('2026-08-08T12:00:01Z'), level: 'info', message: 'Sync complete' },
|
|
{ timestamp: Date.parse('2026-08-08T12:00:02Z'), level: 'error', message: 'Matrix request failed' },
|
|
]);
|
|
}),
|
|
http.get('/api/chats/sync', () =>
|
|
HttpResponse.json({
|
|
syncing: true,
|
|
syncingKind: 'delta',
|
|
lastFullSyncAt: null,
|
|
lastDeltaSyncAt: null,
|
|
hasEverFullSynced: false,
|
|
cachedChats: 18,
|
|
progress: {
|
|
kind: 'delta',
|
|
phase: 'loading-details',
|
|
current: 4,
|
|
total: 10,
|
|
label: 'Loading room history',
|
|
startedAt: Date.parse('2026-08-08T11:59:00Z'),
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
const user = userEvent.setup();
|
|
renderPage(<OperationsPage realtimeEnabled={false} />);
|
|
|
|
expect(await screen.findByRole('heading', { name: 'Operations' })).toBeInTheDocument();
|
|
const activityRegion = screen.getByRole('region', { name: 'Activity' });
|
|
expect(await within(activityRegion).findByText('Reply from u/alex')).toBeInTheDocument();
|
|
const jobsRegion = screen.getByRole('region', { name: 'Sync jobs' });
|
|
expect(await within(jobsRegion).findByText('4 of 10')).toBeInTheDocument();
|
|
expect(screen.getByRole('status', { name: 'Realtime connection' })).toHaveTextContent('State may be stale');
|
|
|
|
const consoleRegion = screen.getByRole('region', { name: 'Live console' });
|
|
expect(consoleRegion).toHaveTextContent('Sync complete');
|
|
expect(consoleRegion).toHaveTextContent('Matrix request failed');
|
|
await user.selectOptions(within(consoleRegion).getByLabelText('Log level'), 'error');
|
|
expect(consoleRegion).not.toHaveTextContent('Sync complete');
|
|
expect(consoleRegion).toHaveTextContent('Matrix request failed');
|
|
await user.type(within(consoleRegion).getByLabelText('Search logs'), 'no match');
|
|
expect(consoleRegion).toHaveTextContent('No logs match the current filters.');
|
|
expect(within(consoleRegion).getByRole('button', { name: 'Pause console' })).toHaveAttribute('aria-pressed', 'false');
|
|
expect(within(consoleRegion).getByLabelText('Auto-scroll')).toBeChecked();
|
|
await user.clear(within(consoleRegion).getByLabelText('Search logs'));
|
|
await user.click(within(consoleRegion).getByRole('button', { name: 'Clear local console' }));
|
|
expect(consoleRegion).toHaveTextContent('Server logs were not deleted');
|
|
});
|
|
|
|
it('starts delta sync directly and confirms expensive chat cache operations', async () => {
|
|
let deltaCalls = 0;
|
|
let fullCalls = 0;
|
|
server.use(
|
|
http.get('/api/activity', () => HttpResponse.json([])),
|
|
http.get('/api/logs', () => HttpResponse.json([])),
|
|
http.get('/api/chats/sync', () => HttpResponse.json({ syncing: false, syncingKind: null, lastFullSyncAt: null, lastDeltaSyncAt: null, hasEverFullSynced: false, cachedChats: 2, progress: null })),
|
|
http.post('/api/chats/sync/delta', () => { deltaCalls += 1; return HttpResponse.json({ success: true, kind: 'delta' }); }),
|
|
http.post('/api/chats/sync/full', () => { fullCalls += 1; return HttpResponse.json({ success: true, kind: 'full' }); }),
|
|
http.post('/api/chats/fetch-all', () => HttpResponse.json({ success: true, count: 7 })),
|
|
);
|
|
const confirmSpy = vi.spyOn(globalThis, 'confirm').mockReturnValue(false);
|
|
const user = userEvent.setup();
|
|
renderPage(<OperationsPage realtimeEnabled={false} />);
|
|
await user.click(await screen.findByRole('button', { name: 'Sync latest chats' }));
|
|
expect(await screen.findByText(/Delta sync started/)).toBeInTheDocument();
|
|
expect(deltaCalls).toBe(1);
|
|
await user.click(screen.getByRole('button', { name: 'Full cache sync' }));
|
|
expect(fullCalls).toBe(0);
|
|
confirmSpy.mockReturnValue(true);
|
|
await user.click(screen.getByRole('button', { name: 'Full cache sync' }));
|
|
expect(await screen.findByText(/Full sync started/)).toBeInTheDocument();
|
|
expect(fullCalls).toBe(1);
|
|
await user.click(screen.getByRole('button', { name: 'Fetch all chats' }));
|
|
expect(await screen.findByText('Fetched 7 chats.')).toBeInTheDocument();
|
|
});
|
|
});
|