// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, expect, it, vi } from 'vitest'; import { AppProvider, useApp } from '../../src/app/AppProvider'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model'; import type { ImportedDataset } from '../../src/domain/dataset'; import { ActionlistControls } from '../../src/features/actionlist/ActionlistControls'; import { loadCanonicalProjectWorkspace } from '../../src/features/project-import/load-canonical-project-workspace'; import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records'; import { makeSheet } from '../../src/importers/sheet-factory'; import { browserDownloadAdapter } from '../../src/platform/browser/browser-download-adapter'; import * as exporter from '../../src/exports/work-view-export'; vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() })); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); localStorage.clear(); }); function Harness() { const app = useApp(); const records = useFilteredRecords(); return <> {records.map(record => record.title).join(',')} ; } async function mount() { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never); const rows = Array.from({ length: 90 }, (_, i) => [`Included ${String(i).padStart(3, '0')}`, 'Fail', 'Expected synthetic state', 'source-table:42 "Synthetic evidence"', String(i)]); const sheet = makeSheet('All Records', [['Title', 'Status', 'Expected', 'Results', 'Order'], ...rows, ['Excluded passed', 'Pass', 'Pass target', '', '90'], ['Excluded marked', 'Fail', 'Marked target', '', '91']], '', 'synthetic.html', { isProjectSheet: true, toolId: 'cis-cat', sourceFile: 'synthetic.html' }); const evidence = makeSheet('Synthetic evidence', [['Host', 'State'], ['Synthetic affected host', '0']], '', 'synthetic.html', { sourceFile: 'synthetic.html', sourceTableIndex: 42, toolId: 'cis-cat' }); const dataset = { fileName: 'Synthetic', profileId: 'cis-cat', sheets: [sheet, evidence] } as ImportedDataset; vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ dataset, assessmentIds: ['slot'], models: [{ project: { id: 'synthetic', name: 'Synthetic' }, assessment: { id: 'slot' }, reviews: [] } as unknown as CanonicalAssessmentModel] }); const container = document.createElement('div'); const root = createRoot(container); const downloads = vi.spyOn(browserDownloadAdapter, 'download').mockImplementation(() => {}); const projection = vi.spyOn(exporter, 'buildWorkViewExport'); await act(async () => root.render()); const click = async (selector: string) => act(async () => container.querySelector(selector)!.click()); await click('[data-open]'); return { container, downloads, projection, click, close: () => act(async () => root.unmount()) }; } it.each(['CSV', 'MD', 'HTML'])('exports the current filtered and sorted view, not marked records, with Results using %s', async format => { const ui = await mount(); try { await ui.click('[data-filter]'); await ui.click('[data-mark]'); expect(ui.projection).not.toHaveBeenCalled(); // no eager evidence projection on navigation/filtering const button = [...ui.container.querySelectorAll('button')].find(item => item.textContent === format)!; expect(button.disabled).toBe(false); await act(async () => button.click()); expect(ui.downloads).toHaveBeenCalledTimes(1); const payload = ui.downloads.mock.calls[0][0]; expect(payload.text).toContain('Synthetic affected host'); expect(payload.text).not.toContain('Excluded marked'); expect(payload.text).not.toContain('Excluded passed'); expect(payload.text).toContain('Included 089'); expect(payload.text).toContain('Included 000'); const report = ui.projection.mock.results[0].value as exporter.WorkViewExport; expect(report.entries.map(entry => entry.title).join(',')).toBe(ui.container.querySelector('output')!.textContent); expect(report.entries).toHaveLength(90); expect(report.entries.every(entry => entry.resultRowCount === 1)).toBe(true); expect(report.context.find(block => block.label === 'Scope')?.value).toContain('slot'); if (format === 'HTML') { const doc = new DOMParser().parseFromString(payload.text, 'text/html'); expect(doc.querySelectorAll('article')).toHaveLength(90); expect([...doc.querySelectorAll('table')].every(table => table.querySelectorAll('thead th').length === 2)).toBe(true); expect(doc.querySelector('script,img,iframe,link')).toBeNull(); } await ui.click('[data-reverse]'); await act(async () => button.click()); const next = ui.projection.mock.results.at(-1)!.value as exporter.WorkViewExport; expect(next.entries.map(entry => entry.id)).toEqual([...report.entries].reverse().map(entry => entry.id)); await ui.click('[data-workspace]'); expect(button.disabled).toBe(true); // marked record does not bypass active search await ui.click('[data-empty]'); expect(button.disabled).toBe(true); } finally { await ui.close(); } }); it('disables empty exports and reports a failed complete projection without downloading partial data', async () => { const ui = await mount(); try { ui.projection.mockImplementationOnce(() => { throw new Error('synthetic private evidence must not be exposed'); }); const button = [...ui.container.querySelectorAll('button')].find(item => item.textContent === 'CSV')!; await act(async () => button.click()); expect(ui.downloads).not.toHaveBeenCalled(); expect(document.body.querySelector('[role="dialog"]')?.textContent).toContain('No partial file'); expect(document.body.textContent).not.toContain('private evidence'); } finally { await ui.close(); } });