Assessor_2.0/tests/components/activity-center.test.tsx

66 lines
4.4 KiB
TypeScript

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, expect, it, vi } from 'vitest';
import { ActivityCenter } from '../../src/components/ActivityCenter';
import { activitySnapshot, beginActivity, clearFinishedActivities, trackActivity } from '../../src/platform/activity-store';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import { requestActivity } from '../../src/api/request-activity';
afterEach(() => { clearFinishedActivities(); vi.unstubAllGlobals(); });
it('shows concurrent real API operations and preserves failures while other work completes', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT',true);
const pending: ((r:Response)=>void)[]=[];
const api=new AssessorApiClient({fetcher:vi.fn(()=>new Promise<Response>(resolve=>pending.push(resolve)))});
const container=document.createElement('div');document.body.append(container);const root=createRoot(container);
try {
await act(async()=>root.render(<ActivityCenter/>));
let core!:Promise<unknown>;let context!:Promise<unknown>;let report!:Promise<unknown>;
await act(async()=>{
core=api.loadAssessmentModel('private-client');
context=api.getAssessmentContext('private-client', ['private-slot'], []);
report=api.createReportRun('private-client',{kind:'technical',assessmentIds:['private-slot']}).catch(()=>undefined);
});
expect(container.textContent).toContain('3 running');
for(const area of ['Core Engine','Context Engine','Report Engine']) expect(container.textContent).toContain(area);
expect(container.textContent).not.toContain('private-client');
await act(async()=>{pending[1](new Response('{}'));await context;});
expect(container.textContent).toContain('2 running');expect(container.textContent).toContain('Completed');
await act(async()=>{pending[2](new Response('{}',{status:500}));await report;});
expect(container.textContent).toContain('Failed');expect(container.textContent).toContain('1 running');
await act(async()=>{pending[0](new Response('{}'));await core;});
expect(container.textContent).toContain('1 failed');
const toggle=container.querySelector<HTMLButtonElement>('button[aria-controls]')!;
await act(async()=>toggle.click());expect(container.querySelector('ul')).toBeNull();
await act(async()=>toggle.click());expect(container.querySelectorAll('li')).toHaveLength(3);
await act(async()=>[...container.querySelectorAll('button')].find(b=>b.textContent==='Clear finished')!.click());
expect(container.textContent).toBe('');
} finally {await act(async()=>root.unmount());container.remove();}
});
it('never changes operation results and marks aborts as cancelled',async()=>{
const failure=new DOMException('Cancelled','AbortError');
await expect(trackActivity('Future Engine','Loading future data',async()=>{throw failure;})).rejects.toBe(failure);
expect(activitySnapshot().at(-1)?.state).toBe('cancelled');
const op=beginActivity('Core Engine','Opening client');op.update('Preparing findings');op.fail();op.complete();
expect(activitySnapshot().at(-1)).toMatchObject({state:'failed',label:'Preparing findings'});
});
it('retains active tasks when clearing or bounding completed history',()=>{
const active=beginActivity('Core Engine','Long-running operation');active.queue();
expect(activitySnapshot().at(-1)?.state).toBe('queued');
for(let i=0;i<60;i++) beginActivity('Core Engine','Short operation').complete();
expect(activitySnapshot()).toHaveLength(41);clearFinishedActivities();expect(activitySnapshot()).toHaveLength(1);active.complete();
});
it('names catalog updates and omits background heartbeat requests',()=>{
expect(requestActivity('/health')).toBeUndefined();
expect(requestActivity('/reference-data/mitre-attack/refresh','POST')?.label).toBe('Updating MITRE catalog');
expect(requestActivity('/reference-data/mitre-attack/source')?.label).toBe('Downloading MITRE source');
expect(requestActivity('/reference-data/policy-evaluation-rule-catalog')?.label).toContain('catalog');
expect(requestActivity('/future?secret=private')?.label).toBe('Loading application data');
});
it('allows future engines to describe operations without importing them into Core',async()=>{
const api=new AssessorApiClient({fetcher:vi.fn(async()=>new Response('{}')),describeActivity:()=>({area:'Future Engine',label:'Loading future data'})});
await api.listProjects();
expect(activitySnapshot().at(-1)).toMatchObject({area:'Future Engine',label:'Loading future data',state:'completed'});
});