Assessor_2.0/tests/features/risk-acceptance-dialog.test...

108 lines
7.3 KiB
TypeScript

// @vitest-environment jsdom
import { act, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { RiskAcceptanceDialog } from '../../src/features/finding-reader/RiskAcceptanceDialog';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { appendRiskAcceptanceReview, type RiskAcceptanceCommand } from '../../src/domain/risk-acceptance';
import { makeSheet } from '../../src/importers/sheet-factory';
import { riskCommand } from '../domain/risk-acceptance-fixture';
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); });
function control(label: string) {
return [...document.querySelectorAll('label')].find(item => item.querySelector('span')?.textContent === label)!.querySelector<HTMLInputElement | HTMLTextAreaElement>('input, textarea')!;
}
async function fill(label: string, value: string) {
const input = control(label);
await act(async () => {
const prototype = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
Object.getOwnPropertyDescriptor(prototype, 'value')!.set!.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
});
}
async function click(label: string) {
const button = [...document.querySelectorAll('button')].find(button => button.textContent === label)!;
await act(async () => button.click());
}
describe('Accepted Risk dialog', () => {
it('opens from the finding header and blocks incomplete acceptance without changing the review', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const container = document.createElement('div'); document.body.append(container);
const root = createRoot(container);
const record = makeSheet('Synthetic', [['Title', 'Status'], ['Synthetic risk', 'Failed']], '', 'synthetic.csv').analysis.records[0];
const save = vi.fn(); const review = vi.fn();
try {
await act(async () => root.render(<FindingReader record={record} title={record.title} linkedSheets={[]} resultReferences={[]} search="" position={0} recordCount={1} onPrevious={() => {}} onNext={() => {}} onReview={review} onRiskAcceptance={save}/>));
const trigger = container.querySelector<HTMLButtonElement>('.accepted-risk-trigger')!;
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog');
expect(trigger.textContent).toBe('Accepted Risk ?');
trigger.focus();
await act(async () => trigger.click());
expect(document.querySelector('[role="dialog"]')?.getAttribute('aria-label')).toBe('Accepted Risk');
await click('Record acceptance');
expect(document.querySelector('[role="alert"]')).not.toBeNull();
expect(save).not.toHaveBeenCalled(); expect(review).not.toHaveBeenCalled();
await act(async () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })));
expect(document.querySelector('[role="dialog"]')).toBeNull();
expect(document.activeElement).toBe(trigger);
} finally { await act(async () => root.unmount()); }
});
it('marks an existing draft risk acceptance with the dedicated button state', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const container = document.createElement('div'); document.body.append(container);
const root = createRoot(container);
const record = makeSheet('Synthetic', [['Title', 'Status'], ['Synthetic draft risk', 'Failed']], '', 'synthetic.csv').analysis.records[0];
const review = appendRiskAcceptanceReview(undefined, record.findingOccurrenceId ?? record.id, riskCommand('draft'), new Date().toISOString());
try {
await act(async () => root.render(<FindingReader record={record} title={record.title} review={{ ...review, include: false, status: 'Open', priority: 'Medium', owner: '', due: '', prerequisites: '', access: '', nextStep: '', evidence: '', comment: '', markerQuestion: false, markerBlocked: false, markerQuickwin: false, markerExternal: false, markerNotRelevant: false }} linkedSheets={[]} resultReferences={[]} search="" position={0} recordCount={1} onPrevious={() => {}} onNext={() => {}} onReview={() => {}} onRiskAcceptance={async () => {}}/>));
const trigger = container.querySelector<HTMLButtonElement>('.accepted-risk-trigger')!;
expect(trigger.textContent).toBe('Accepted RiskDraft');
expect(trigger.dataset.riskAcceptanceState).toBe('draft');
expect(trigger.classList.contains('is-draft')).toBe(true);
} finally { await act(async () => root.unmount()); }
});
it('keeps entries after a failed save, confirms persistence only after success and renders revision history', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const container = document.createElement('div'); document.body.append(container);
const root = createRoot(container);
let fail = true;
const savedCommands: RiskAcceptanceCommand[] = [];
function Harness() {
const [review, setReview] = useState(() => appendRiskAcceptanceReview(undefined, 'occurrence', riskCommand('draft'), new Date().toISOString()));
return <RiskAcceptanceDialog title="Synthetic risk" occurrenceId="occurrence" value={review.riskAcceptance} onClose={() => {}} onSave={async command => {
if (fail) throw new Error('Synthetic database unavailable');
savedCommands.push(command);
setReview(current => appendRiskAcceptanceReview(current, 'occurrence', command, new Date().toISOString()));
}}/>;
}
try {
await act(async () => root.render(<Harness/>));
await fill('Recorded by', 'Synthetic recorder');
await fill('Change / decision reason', 'Synthetic external approval verified');
await act(async () => document.querySelector<HTMLInputElement>('.risk-acceptance-confirm input')!.click());
await click('Record acceptance');
expect(savedCommands).toHaveLength(0);
expect(document.querySelector('[role="dialog"]')?.getAttribute('aria-label')).toBe('Review entry');
expect(document.querySelector('.risk-acceptance-review')?.textContent).toContain('Synthetic external approval verified');
await click('Change');
expect(control('Change / decision reason').value).toBe('Synthetic external approval verified');
await click('Record acceptance');
await click('OK');
expect(document.querySelector('[role="alert"]')?.textContent).toContain('Synthetic database unavailable');
expect(control('Change / decision reason').value).toBe('Synthetic external approval verified');
expect(document.querySelector('[role="status"]')).toBeNull();
fail = false;
await click('Record acceptance');
await click('OK');
expect(savedCommands).toHaveLength(1);
expect(document.querySelector('[role="status"]')?.textContent).toContain('saved successfully');
expect(document.querySelectorAll('.risk-acceptance-history details')).toHaveLength(2);
await fill('Change / decision reason', 'Synthetic control failure');
await click('Revoke acceptance');
await click('OK');
expect(document.querySelectorAll('.risk-acceptance-history details')).toHaveLength(3);
expect(document.querySelector('.risk-acceptance-form')?.textContent).toContain('Revoked');
} finally { await act(async () => root.unmount()); }
});
});