// @vitest-environment jsdom import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import type { BsiCatalogState } from '../../src/compliance/bsi-catalog-management'; import { BsiCatalogLibrary } from '../../src/compliance/ui/BsiCatalogLibrary'; import { useRegulatoryDecisionRevision } from '../../src/compliance/ui/decision-revision'; // Data Hub BSI tile (user decision 2026-10-08): like the MITRE matrix tile, it names the active catalog and // mapping and offers `Update from BSI`; `Use bundled catalog` appears only after an update. Synthetic states. const bundled: BsiCatalogState = { version: 1, origin: 'bundled', catalogVersion: '2026-09-24T07:12:39.131983+00:00', crosswalkVersion: '1.0.1-qa', requirementCount: 901, crosswalkEntryCount: 83, catalogSha256: 'a'.repeat(64), crosswalkSha256: 'b'.repeat(64), missingTargets: ['BER.6.8'] }; const updated: BsiCatalogState = { ...bundled, origin: 'bsi', updatedAt: '2026-10-08T12:00:00.000Z', catalogVersion: '2026-10-01T00:00:00+00:00', crosswalkVersion: '1.0.2', missingTargets: ['BER.6.8', 'GEB.5.4'] }; let host: HTMLDivElement; let root: Root; let revisions: number[]; function Revision() { revisions.push(useRegulatoryDecisionRevision()); return null; } const render = () => act(async () => { root.render(<>); await new Promise(resolve => setTimeout(resolve, 0)); }); const badge = () => host.querySelector('[data-bsi-catalog] .datahub-rack-badge')?.textContent; const button = (label: string) => [...host.querySelectorAll('[data-bsi-catalog] button')].find(item => item.textContent === label); beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); revisions = []; host = document.createElement('div'); document.body.append(host); root = createRoot(host); }); afterEach(async () => { await act(async () => root.unmount()); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); resetEnginePreferencesForTests(); }); describe('Data Hub BSI catalog tile', () => { it('names the bundled catalog and mapping, updates from BSI and reloads the derived BSI views', async () => { vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled); const refresh = vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockResolvedValue({ ...updated, updated: true }); await render(); expect(host.querySelector('[data-bsi-catalog] .datahub-rack-header strong')?.textContent).toBe('BSI GRUNDSCHUTZ++ KERNEL'); expect(badge()).toBe('2026-09-24 · Mapping 1.0.1-qa · Bundled'); expect(button('Use bundled catalog')).toBeUndefined(); const before = revisions.at(-1)!; await act(async () => button('Update from BSI')!.click()); expect(refresh).toHaveBeenCalledTimes(1); expect(badge()).toBe('2026-10-01 · Mapping 1.0.2 · Updated'); expect(host.querySelector('[role="status"]')?.textContent).toBe('✓Updated to catalog 2026-10-01 and mapping 1.0.2. 2 mapping targets are not in the catalog.'); // Client Info and Work reload their derived BSI requirements. expect(revisions.at(-1)).toBe(before + 1); expect(button('Use bundled catalog')).toBeDefined(); }); it('says when the catalog is up to date without reloading other views', async () => { vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled); vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockResolvedValue({ ...bundled, updated: false }); await render(); const before = revisions.at(-1); await act(async () => button('Update from BSI')!.click()); expect(host.querySelector('[role="status"]')?.textContent).toBe('✓The BSI catalog is up to date.'); expect(revisions.at(-1)).toBe(before); }); it('returns to the bundled catalog and shows an update failure without changing the badge', async () => { vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(updated); const reset = vi.spyOn(AssessorApiClient.prototype, 'resetBsiCatalog').mockResolvedValue(bundled); vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockRejectedValue(new Error('BSI could not be reached. Please try again later.')); await render(); await act(async () => button('Update from BSI')!.click()); expect(host.querySelector('[role="alert"]')?.textContent).toBe('BSI could not be reached. Please try again later.'); expect(badge()).toBe('2026-10-01 · Mapping 1.0.2 · Updated'); await act(async () => button('Use bundled catalog')!.click()); expect(reset).toHaveBeenCalledTimes(1); expect(badge()).toBe('2026-09-24 · Mapping 1.0.1-qa · Bundled'); expect(host.querySelector('[role="status"]')?.textContent).toBe('✓The bundled BSI catalog is active again.'); expect(button('Use bundled catalog')).toBeUndefined(); }); it('renders and requests nothing while the Compliance Engine is off', async () => { const request = vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled); setEngineEnabled('compliance', false); await render(); expect(host.querySelector('[data-bsi-catalog]')).toBeNull(); expect(request).not.toHaveBeenCalled(); }); it('sits in the Data Hub reference row directly after the ISO corpus tile', () => { const source = readFileSync(join(process.cwd(), 'src/features/project-import/DataHub.tsx'), 'utf8'); expect(source).toContain(' \n \n'); const styles = readFileSync(join(process.cwd(), 'src/styles/datahub-redesign.css'), 'utf8'); expect(styles).toContain('.datahub-reference-row > .bsi-catalog-library { grid-area: 4 / 1 / 5 / -1; }'); }); });