feat(web): connect jobs and audit workspaces

This commit is contained in:
chick
2026-07-18 01:42:17 +08:00
parent 53d9229b45
commit 88d41a7a18
7 changed files with 1177 additions and 2 deletions
@@ -4,11 +4,15 @@ import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AppShell, type InstanceContext } from './app-shell.js';
import type { AuditDataSource } from './audit/audit-page.js';
import type { EventStreamClient } from './events/event-stream-client.js';
import { type FleetDataSource, type FleetSnapshot } from './fleet/fleet-page.js';
import type { JobsDataSource } from './jobs/jobs-page.js';
afterEach(() => {
cleanup();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
const snapshot: FleetSnapshot = {
@@ -52,6 +56,12 @@ function source(load: FleetDataSource['load']): FleetDataSource {
return { load };
}
const quietEventStreamClient: EventStreamClient = {
subscribe: () => () => undefined,
};
const emptyPage = { items: [], page: { page: 1, pageSize: 25, total: 0 } };
describe('React AppShell and Fleet vertical slice', () => {
it('loads real injected data and renders canonical origins and owner routes without React key warnings', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
@@ -197,4 +207,76 @@ describe('React AppShell and Fleet vertical slice', () => {
expect(screen.queryByText('Owner modem')).toBeNull();
expect(screen.getByText(/Instance context is unavailable/i)).toBeTruthy();
});
it.each([
{
pathname: '/jobs',
endpoint: '/api/v1/jobs?page=1&pageSize=25&sort=createdAt&direction=desc',
emptyMessage: /No jobs match the current query/i,
},
{
pathname: '/audit',
endpoint: '/api/v1/audit?sort=occurredAt&direction=desc&page=1&pageSize=25',
emptyMessage: /No audit events match the current query/i,
},
])(
'uses the default HTTP data source for $pathname',
async ({ pathname, endpoint, emptyMessage }) => {
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(
new Response(JSON.stringify(emptyPage), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetcher);
render(<AppShell pathname={pathname} eventStreamClient={quietEventStreamClient} />);
expect(await screen.findByText(emptyMessage)).toBeTruthy();
expect(fetcher).toHaveBeenCalledTimes(1);
expect(fetcher).toHaveBeenCalledWith(endpoint, {
method: 'GET',
credentials: 'same-origin',
headers: { accept: 'application/json' },
signal: expect.any(AbortSignal),
});
},
);
it.each([
{
pathname: '/jobs',
sourceProp: 'jobsDataSource' as const,
emptyMessage: /No jobs match the current query/i,
},
{
pathname: '/audit',
sourceProp: 'auditDataSource' as const,
emptyMessage: /No audit events match the current query/i,
},
])(
'uses an explicitly injected data source instead of global fetch for $pathname',
async ({ pathname, sourceProp, emptyMessage }) => {
const fetcher = vi.fn<typeof fetch>();
vi.stubGlobal('fetch', fetcher);
const load = vi
.fn<JobsDataSource['load'] | AuditDataSource['load']>()
.mockResolvedValue(emptyPage);
const injectedSource = { load };
render(
<AppShell
pathname={pathname}
eventStreamClient={quietEventStreamClient}
{...(sourceProp === 'jobsDataSource'
? { jobsDataSource: injectedSource as JobsDataSource }
: { auditDataSource: injectedSource as AuditDataSource })}
/>,
);
expect(await screen.findByText(emptyMessage)).toBeTruthy();
expect(load).toHaveBeenCalledTimes(1);
expect(fetcher).not.toHaveBeenCalled();
},
);
});