feat(dashboard): close legacy parity gaps
This commit is contained in:
1 parent
70d62b2da3
commit
0fecd426f4
21 files changed
+397
-28
No files matched your search
@@ -45,6 +45,7 @@ function installSuccessfulApi(overrides: TestStatsOverrides = {}) {
|
||||
messagesRemaining: 6,
|
||||
}),
|
||||
),
|
||||
http.get('/api/messages', () => HttpResponse.json([])),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +107,10 @@ describe('dashboard foundation', () => {
|
||||
messagesRemaining: 6,
|
||||
}),
|
||||
),
|
||||
http.get('/api/messages', () => HttpResponse.json([{
|
||||
id: 1, postId: 'post-1', recipient: 'alice', subject: 'Hello', message: 'A recent opener',
|
||||
sentAt: Date.now(), gotResponse: true, responseCheckedAt: Date.now(), strategyId: null, chatRoomId: null,
|
||||
}])),
|
||||
);
|
||||
|
||||
renderDashboard();
|
||||
@@ -118,6 +123,8 @@ describe('dashboard foundation', () => {
|
||||
expect(screen.getByText('6 messages remaining')).toBeInTheDocument();
|
||||
expect(screen.getByText('No recent activity yet.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dry run')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('list', { name: 'Recent sent messages' })).toHaveTextContent('A recent opener');
|
||||
expect(screen.getByRole('list', { name: 'Recent sent messages' })).toHaveTextContent('Responded');
|
||||
});
|
||||
|
||||
it('keeps stats visible when timers fail and retries only the failed panel', async () => {
|
||||
|
||||
@@ -178,6 +178,27 @@ describe('ConversationsPage', () => {
|
||||
expect(sendRequests).toBe(2);
|
||||
});
|
||||
|
||||
it('generates a media ask and looks up durable processed-user history', async () => {
|
||||
installConversationList();
|
||||
server.use(
|
||||
http.post('/api/conversations/room-alice/suggest-media-ask', () => HttpResponse.json({ suggestedReply: 'Would you like to share a photo?' })),
|
||||
http.get('/api/posts/by-author/alice', () => HttpResponse.json([{
|
||||
id: 'post-1', subreddit: 'askuk', author: 'alice', title: 'A processed post',
|
||||
imageUrl: null, imageAnalysis: null, generatedMessage: 'Hello',
|
||||
processedAt: Date.now(), messageSent: true, error: null,
|
||||
}])),
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
renderConversations();
|
||||
await user.type(screen.getByLabelText('Lookup Reddit username'), 'u/alice');
|
||||
await user.click(screen.getByRole('button', { name: 'Look up user' }));
|
||||
expect(await screen.findByRole('list', { name: 'Processed posts for u/alice' })).toHaveTextContent('A processed post');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /u\/alice/ }));
|
||||
await user.click(await screen.findByRole('button', { name: 'Suggest media ask' }));
|
||||
expect(await screen.findByRole('textbox', { name: 'Reply to u/alice' })).toHaveValue('Would you like to share a photo?');
|
||||
});
|
||||
|
||||
it('starts a conversation and preserves both fields after a recoverable failure', async () => {
|
||||
installConversationList();
|
||||
let startRequests = 0;
|
||||
|
||||
@@ -10,6 +10,22 @@ function entry(id: string, level: 'debug' | 'info' | 'warn' | 'error', message:
|
||||
}
|
||||
|
||||
describe('live console state', () => {
|
||||
it('clears retained and paused entries without changing local controls', () => {
|
||||
const initial = {
|
||||
...createLiveConsoleState(5),
|
||||
entries: [entry('one', 'info', 'one')],
|
||||
pendingEntries: [entry('two', 'warn', 'two')],
|
||||
isPaused: true,
|
||||
search: 'needle',
|
||||
};
|
||||
|
||||
expect(reduceLiveConsoleState(initial, { type: 'clear' })).toEqual({
|
||||
...initial,
|
||||
entries: [],
|
||||
pendingEntries: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('bounds entries, rejects duplicate IDs, and filters by level and search', () => {
|
||||
let state = createLiveConsoleState(2);
|
||||
state = reduceLiveConsoleState(state, { type: 'append', entry: entry('1', 'info', 'Sync started') });
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 } from 'vitest';
|
||||
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';
|
||||
|
||||
@@ -139,5 +139,35 @@ describe('OperationsPage', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { cleanup, render, screen } 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 } from 'vitest';
|
||||
import type { ReactNode } from 'react';
|
||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { ConversationRoute } from '../../apps/dashboard/src/features/conversations/conversation-route.js';
|
||||
import { MediaRoute } from '../../apps/dashboard/src/features/media/media-route.js';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
function LocationProbe() {
|
||||
const location = useLocation();
|
||||
return <output aria-label="Current URL">{location.pathname}{location.search}</output>;
|
||||
}
|
||||
|
||||
function renderRoute(path: string, element: ReactNode) {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><MemoryRouter initialEntries={[path]}><LocationProbe /><Routes><Route path="*" element={element} /></Routes></MemoryRouter></QueryClientProvider>);
|
||||
}
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
||||
afterEach(() => { cleanup(); server.resetHandlers(); });
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe('route-friendly filters', () => {
|
||||
it('hydrates and writes conversation search and status filters', async () => {
|
||||
server.use(http.get('/api/conversations', () => HttpResponse.json([])));
|
||||
const user = userEvent.setup();
|
||||
renderRoute('/conversations?q=alice&filter=unread', <ConversationRoute />);
|
||||
const search = await screen.findByRole('searchbox', { name: 'Search conversations' });
|
||||
expect(search).toHaveValue('alice');
|
||||
expect(screen.getByRole('combobox', { name: 'Filter conversations' })).toHaveValue('unread');
|
||||
await user.clear(search);
|
||||
await user.type(search, 'bob');
|
||||
expect(screen.getByLabelText('Current URL')).toHaveTextContent('q=bob');
|
||||
});
|
||||
|
||||
it('hydrates and writes media filters', async () => {
|
||||
server.use(
|
||||
http.get('/api/media', () => HttpResponse.json([{ id: 'one', chatUsername: 'alice', chatRoomId: 'room-a', sender: 'alice', timestamp: Date.now(), originalUrl: 'https://example.test/a.gif', fileName: 'a.gif', relativePath: 'a.gif', createdAt: Date.now(), urlPath: '/media/a.gif' }])),
|
||||
http.get('/api/media/sync', () => HttpResponse.json({ isSyncing: false, lastSyncAt: null, lastSyncDurationMs: null, lastSyncChats: null, lastError: null, nextSyncAt: null })),
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
renderRoute('/media?conversation=alice&type=gif', <MediaRoute />);
|
||||
expect(await screen.findByRole('combobox', { name: 'Conversation' })).toHaveValue('alice');
|
||||
await user.selectOptions(screen.getByRole('combobox', { name: 'Date range' }), '7d');
|
||||
expect(screen.getByLabelText('Current URL')).toHaveTextContent('range=7d');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user