// @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('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( {}} onNext={() => {}} onReview={review} onRiskAcceptance={save}/>)); const trigger = container.querySelector('.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( {}} onNext={() => {}} onReview={() => {}} onRiskAcceptance={async () => {}}/>)); const trigger = container.querySelector('.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 {}} 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()); await fill('Recorded by', 'Synthetic recorder'); await fill('Change / decision reason', 'Synthetic external approval verified'); await act(async () => document.querySelector('.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()); } }); });