mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
135 lines
5.1 KiB
TypeScript
135 lines
5.1 KiB
TypeScript
// FE-COMP-PLUGINACTIVITY-001 to FE-COMP-PLUGINACTIVITY-010
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { render, screen, waitFor, within } from '../../../tests/helpers/render';
|
|
import { server } from '../../../tests/helpers/msw/server';
|
|
import PluginActivityPanel from './PluginActivityPanel';
|
|
|
|
interface ActivityRow {
|
|
ts: string;
|
|
plugin_id: string;
|
|
plugin_name: string | null;
|
|
method: string;
|
|
resource: string | null;
|
|
code: string;
|
|
}
|
|
|
|
const row = (over: Partial<ActivityRow> = {}): ActivityRow => ({
|
|
ts: '2025-06-01T10:00:00.000Z',
|
|
plugin_id: 'weather',
|
|
plugin_name: 'Weather',
|
|
method: 'trips.read',
|
|
resource: 'trip:42',
|
|
code: 'ok',
|
|
...over,
|
|
});
|
|
|
|
function serveActivity(activity: ActivityRow[]): void {
|
|
server.use(http.get('/api/plugin-activity', () => HttpResponse.json({ activity })));
|
|
}
|
|
|
|
/** The <td> cells of the one rendered data row. */
|
|
const cells = () => within(screen.getByRole('table')).getAllByRole('row')[1].querySelectorAll('td');
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
serveActivity([]);
|
|
});
|
|
|
|
describe('PluginActivityPanel', () => {
|
|
it('FE-COMP-PLUGINACTIVITY-001: shows the loading line before the log arrives', async () => {
|
|
server.use(http.get('/api/plugin-activity', () => new Promise(() => {})));
|
|
render(<PluginActivityPanel />);
|
|
|
|
expect(screen.getByText('Plugin activity')).toBeInTheDocument();
|
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Refresh' })).toBeDisabled();
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-002: an empty log renders the empty state instead of a table', async () => {
|
|
render(<PluginActivityPanel />);
|
|
|
|
expect(await screen.findByText('No plugin activity yet.')).toBeInTheDocument();
|
|
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Refresh' })).toBeEnabled();
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-003: a failing request falls back to the empty state, not a crash', async () => {
|
|
server.use(http.get('/api/plugin-activity', () => HttpResponse.json({ error: 'nope' }, { status: 500 })));
|
|
render(<PluginActivityPanel />);
|
|
|
|
expect(await screen.findByText('No plugin activity yet.')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-004: entries render as a table with all five columns', async () => {
|
|
serveActivity([row()]);
|
|
render(<PluginActivityPanel />);
|
|
|
|
const table = await screen.findByRole('table');
|
|
const headers = within(table).getAllByRole('columnheader').map(th => th.textContent);
|
|
expect(headers).toEqual(['Plugin', 'Action', 'Resource', 'When', 'Result']);
|
|
expect(within(table).getByText('Weather')).toBeInTheDocument();
|
|
expect(within(table).getByText('trips.read')).toBeInTheDocument();
|
|
expect(within(table).getByText('trip:42')).toBeInTheDocument();
|
|
expect(within(table).getByText('ok')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-005: a nameless plugin falls back to its id and a null resource to a dash', async () => {
|
|
serveActivity([row({ plugin_name: null, resource: null })]);
|
|
render(<PluginActivityPanel />);
|
|
|
|
await screen.findByRole('table');
|
|
expect(cells()[0]).toHaveTextContent('weather');
|
|
expect(cells()[2]).toHaveTextContent('—');
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-006: an "ok" code gets the neutral pill', async () => {
|
|
serveActivity([row({ code: 'ok' })]);
|
|
render(<PluginActivityPanel />);
|
|
|
|
await screen.findByRole('table');
|
|
expect(screen.getByText('ok')).toHaveClass('bg-surface-hover', 'text-content-secondary');
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-007: an access denial gets the danger pill', async () => {
|
|
serveActivity([row({ code: 'FORBIDDEN' })]);
|
|
render(<PluginActivityPanel />);
|
|
|
|
await screen.findByRole('table');
|
|
expect(screen.getByText('FORBIDDEN')).toHaveClass('bg-danger-soft', 'text-danger');
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-008: any other non-ok code gets the warning pill', async () => {
|
|
serveActivity([row({ code: 'RATE_LIMITED' })]);
|
|
render(<PluginActivityPanel />);
|
|
|
|
await screen.findByRole('table');
|
|
expect(screen.getByText('RATE_LIMITED')).toHaveClass('bg-warning-soft', 'text-warning');
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-009: an unparseable timestamp is shown verbatim', async () => {
|
|
serveActivity([row({ ts: 'not-a-date' })]);
|
|
render(<PluginActivityPanel />);
|
|
|
|
await screen.findByRole('table');
|
|
expect(cells()[3]).toHaveTextContent('not-a-date');
|
|
});
|
|
|
|
it('FE-COMP-PLUGINACTIVITY-010: Refresh re-reads the log', async () => {
|
|
const user = userEvent.setup();
|
|
let calls = 0;
|
|
server.use(http.get('/api/plugin-activity', () => {
|
|
calls += 1;
|
|
return HttpResponse.json({ activity: calls === 1 ? [] : [row({ plugin_name: 'Second pass' })] });
|
|
}));
|
|
render(<PluginActivityPanel />);
|
|
|
|
await screen.findByText('No plugin activity yet.');
|
|
await user.click(screen.getByRole('button', { name: 'Refresh' }));
|
|
|
|
await waitFor(() => expect(screen.getByText('Second pass')).toBeInTheDocument());
|
|
expect(calls).toBe(2);
|
|
});
|
|
});
|