// @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('offers whole-finding and exact Defender device scopes without mixing system scope into the narrative', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const container = document.createElement('div'); document.body.append(container); const root = createRoot(container); const observations = [ { name: 'Device A', ip: '192.0.2.10', site: 'Synthetic site', sensor: 'sensor-a', cve: 'CVE-2026-1001', severity: 'High', description: 'Synthetic', additionalSourceFields: { 'Device ID': 'DEV-A' }, source: { tableIndex: 0, rowIndex: 1 } }, { name: 'Device B', ip: '192.0.2.20', site: 'Synthetic site', sensor: 'sensor-a', cve: 'CVE-2026-1001', severity: 'High', description: 'Synthetic', additionalSourceFields: { 'Device ID': 'DEV-B' }, source: { tableIndex: 0, rowIndex: 2 } } ]; const record = makeSheet('Synthetic', [['CVE', 'Source Observations'], ['CVE-2026-1001', JSON.stringify(observations)]], '', 'defender.csv', { toolId: 'defender-for-iot-vulnerabilities' }).analysis.records[0]; Object.assign(record, { findingOccurrenceId: 'occurrence-1', findingDefinitionId: 'definition-1', scanRunId: 'run-1', importFileId: 'file-1' }); try { await act(async () => root.render( {}} onNext={() => {}} onReview={() => {}} onRiskAcceptance={async () => {}} onScopedRiskAcceptance={async () => {}}/>)); await act(async () => container.querySelector('.accepted-risk-trigger')!.click()); const select = document.querySelector('.risk-acceptance-scope-select select')!; expect([...select.options].map(option => option.textContent)).toEqual(['Entire finding', 'Device A', 'Device B']); expect(document.querySelector('.risk-acceptance-system-scope')).toBeNull(); await act(async () => { Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')!.set!.call(select, 'device-id:dev-a'); select.dispatchEvent(new Event('change', { bubbles: true })); }); expect(document.querySelector('.risk-acceptance-system-scope')?.textContent).toContain('CVE-2026-1001 on Device A'); expect(document.querySelector('.risk-acceptance-system-scope')?.textContent).toContain('1 covered CVE'); expect(control('Affected assets, systems and exclusions').value).toBe(''); } finally { await act(async () => root.unmount()); } }); 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()); } }); });