Files
RedditBot/tests/dashboard/attention.test.tsx
T
Jamie BruntonandClaude Fable 5 e984bf20ac feat: dashboard UI/UX overhaul from full review
Shared foundation:
- New Modal component on native <dialog>.showModal() (focus trap, inert
  background, Escape, focus restore, scroll lock); all eight dialogs and
  both globalThis.confirm calls migrated to it
- Design tokens for success/warning/danger surfaces; pill danger variant;
  .sr-only utility; toast layering above dialog backdrops; not-allowed
  cursor for validity-disabled buttons

Conversations/attention: auto-scroll with jump-to-latest, per-conversation
draft persistence, suggestion undo, 30s auto-refresh with refresh controls,
unread emphasis and counts, blur-consistent share dialog, empty-accounts
guidance, collapsed user lookup, relative timestamps, danger pills for
critical items

Media/library/outreach: click-blurred-to-reveal, lightbox with arrow
navigation and captions, dirty-form preservation across refetches, upload
validation with thumbnail preview, save feedback, result counts with
load-more and clear-filters, ticking countdowns from a single clock

Overview/performance/operations/settings: mutation errors inside the
confirm dialog, live sending-window countdown, offline-aware loading
states, per-panel loading/error with retry, 7/30/90-day range toggle,
console scroll yielding with shown/retained counts, risky-save matching
by subreddit name instead of index, useBlocker unsaved-changes guard,
single-pass validation with focus-to-first-error, access panel JSON
validation and input reset

Tests: 806 -> 844 passing; both typechecks clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 09:07:42 +01:00

109 lines
3.9 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 { HttpResponse, http } from 'msw';
import { setupServer } from 'msw/node';
import { MemoryRouter } from 'react-router-dom';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { AttentionPage } from '../../apps/dashboard/src/features/attention/attention-page.js';
const MINUTE_MS = 60_000;
function attentionResponse(items: unknown[]) {
return { generatedAt: Date.now() - MINUTE_MS, items };
}
const criticalItem = {
id: 'item-critical',
kind: 'human-required',
severity: 'critical',
title: 'Human handoff required',
detail: 'Automation stopped and needs an operator reply.',
occurredAt: Date.now() - (14 * MINUTE_MS),
conversationId: 'room-alice',
username: 'alice',
};
const warningItem = {
id: 'item-warning',
kind: 'invitation-cooldown',
severity: 'warning',
title: 'Invitation cooldown active',
detail: 'Reddit is rate limiting new chat invitations.',
occurredAt: Date.now() - (2 * 60 * MINUTE_MS),
};
const server = setupServer();
function renderAttention() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<AttentionPage />
</QueryClientProvider>
</MemoryRouter>,
);
return queryClient;
}
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => {
cleanup();
server.resetHandlers();
});
afterAll(() => server.close());
describe('AttentionPage', () => {
it('labels critical items and the critical count with the danger pill', async () => {
server.use(http.get('/api/attention', () => HttpResponse.json(attentionResponse([criticalItem, warningItem]))));
renderAttention();
const items = await screen.findByRole('list', { name: 'Attention items' });
const [first, second] = within(items).getAllByRole('listitem');
expect(within(first).getByText('human required')).toHaveClass('pill', 'danger');
expect(within(second).getByText('invitation cooldown')).toHaveClass('pill', 'warning');
expect(screen.getByText('1 critical')).toHaveClass('pill', 'danger');
});
it('shows relative times with exact timestamps as titles', async () => {
server.use(http.get('/api/attention', () => HttpResponse.json(attentionResponse([criticalItem]))));
renderAttention();
const time = await screen.findByText('14m ago');
expect(time.tagName).toBe('TIME');
expect(time).toHaveAttribute('dateTime', new Date(criticalItem.occurredAt).toISOString());
expect(time).toHaveAttribute('title');
expect(screen.getByText(/Auto-refreshes every 30s/)).toBeInTheDocument();
});
it('links conversation items and gives unlinked items a place to act', async () => {
server.use(http.get('/api/attention', () => HttpResponse.json(attentionResponse([criticalItem, warningItem]))));
renderAttention();
expect(await screen.findByRole('link', { name: 'Open conversation' })).toHaveAttribute(
'href',
'/conversations/room-alice',
);
expect(screen.getByText('No linked conversation — check Operations.')).toBeInTheDocument();
expect(screen.getByText('Reddit is rate limiting new chat invitations.')).toBeInTheDocument();
});
it('uses the dedicated attention dot for the open-items summary', async () => {
server.use(http.get('/api/attention', () => HttpResponse.json(attentionResponse([criticalItem]))));
renderAttention();
await screen.findByText('Open items');
const strip = screen.getByRole('region', { name: 'Attention totals' });
const dot = strip.querySelector('.attention-status-dot');
expect(dot).toHaveClass('is-open');
expect(dot).not.toHaveClass('running');
expect(dot).not.toHaveClass('stopped');
});
});