Assessor_2.0/tests/features/datahub-centered-layout.tes...

214 lines
16 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import { DatahubHistoryOverview } from '../../src/features/project-import/DatahubHistoryOverview';
const datahubStyles = readFileSync(new URL('../../src/styles/datahub-redesign.css', import.meta.url), 'utf8');
const datahubTableStyles = readFileSync(new URL('../../src/styles/datahub.css', import.meta.url), 'utf8');
const componentStyles = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
const tileContractStyles = readFileSync(new URL('../../src/styles/tile-contract.css', import.meta.url), 'utf8');
const tokens = readFileSync(new URL('../../src/styles/ui-tokens.css', import.meta.url), 'utf8');
const overview = readFileSync(new URL('../../src/features/project-import/DatahubHistoryOverview.tsx', import.meta.url), 'utf8');
const donut = readFileSync(new URL('../../src/components/DonutMetricCard/DonutMetricCard.tsx', import.meta.url), 'utf8');
const datahub = readFileSync(new URL('../../src/features/project-import/DataHub.tsx', import.meta.url), 'utf8');
const baselines = readFileSync(new URL('../../src/features/project-import/MicrosoftBaselineLibrary.tsx', import.meta.url), 'utf8');
describe('Datahub centered reference rack layout', () => {
it('uses the binding six-card donut design for the selected timeslot', () => {
expect(datahubStyles).toContain('.datahub-page > .datahub-rack-module { width: min(1180px, 100%); margin-inline: auto; }');
expect(datahubStyles).toContain('.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start;');
expect(datahubStyles).toContain('.datahub-tenant-card.is-selected');
expect(datahubStyles).toContain('.datahub-tenant-processing-dot { position: absolute; z-index: 3; top: var(--datahub-readiness-dot-inset); left: var(--datahub-readiness-dot-inset);');
expect(datahubStyles).toContain('background: var(--state-online); pointer-events: none;');
expect(datahubStyles).toContain('.datahub-tenant-processing-dot[data-status="update"],');
expect(datahubStyles).toContain('.datahub-tenant-processing-dot[data-status="review"] { background: var(--warning); }');
expect(datahubStyles).toContain('.datahub-tenant-processing-dot[data-status="error"] { background: var(--state-offline); }');
expect(datahubStyles).toContain('.datahub-tenant-show[data-status="update"]');
expect(datahubStyles).toContain('border-color: var(--warning); color: var(--warning);');
expect(datahubStyles).toContain('outline: calc(var(--bw-emphasis) + var(--bw-hairline)) solid var(--tile-selected-border);');
expect(datahubStyles).toContain('outline-offset: calc(-1 * (var(--bw-emphasis) + var(--bw-hairline)));');
expect(datahubStyles).toContain('.datahub-tenant-card::before { content: none; }');
expect(datahubStyles).toContain('transition: background .12s ease, border-color .12s ease;');
expect(datahubStyles).toMatch(/\.datahub-tenant-card:hover,\s*\.datahub-tenant-card:focus-within\s*\{[^}]*border-color:\s*var\(--tile-border\);[^}]*background:\s*var\(--tile-surface-hover\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
expect(datahubStyles).not.toMatch(/\.datahub-tenant-card\s*\{[^}]*box-shadow/s);
expect(datahubStyles).toContain('box-shadow: var(--tile-card-shadow);');
expect(datahubStyles).not.toContain('translateY(-2px)');
expect(datahubStyles).toContain('.datahub-tenant-select { position: absolute; inset: 0;');
expect(datahubStyles).toContain('grid-template-rows: minmax(0, 1fr) auto auto;');
expect(datahubStyles).toContain('font: var(--fw-semibold) calc(var(--donut-title-size) * 1.35)');
expect(datahubStyles).toContain('align-self: center; justify-self: center;');
expect(datahubStyles).toContain('text-align: center; text-overflow: ellipsis; white-space: normal;');
expect(datahubStyles).toContain('-webkit-line-clamp: 3;');
expect(datahubStyles).toContain('.datahub-tenant-summary { align-self: end;');
expect(datahubStyles).toContain('.datahub-tenant-latest { min-width: 0;');
expect(datahubStyles).toContain('opacity: .72;');
expect(datahubStyles).toContain('.datahub-tenant-show:active { position: absolute;');
expect(datahubStyles).toContain('background: var(--button-marked-bg);');
expect(datahubStyles).toContain('.datahub-tenant-add > span');
expect(datahubStyles).not.toContain('.datahub-reference-divider');
expect(componentStyles).toContain('Verbindlicher Donut-Standard: Referenz vom 2026-08-05.');
expect(componentStyles).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s);
expect(tileContractStyles).toMatch(/@media \(min-width: 1101px\)\s*\{\s*\.datahub-reference-controls,\s*\.datahub-tenant-card-rack\s*\{[^}]*grid-template-columns:\s*repeat\(6, minmax\(0, 1fr\)\);/s);
expect(componentStyles).toContain('padding: var(--segment-inset);');
expect(componentStyles).not.toContain('.donut-metric-card::before');
expect(componentStyles).toContain('border-radius: var(--tile-radius);');
expect(componentStyles).toContain('background: var(--tile-surface);');
expect(componentStyles).toContain('.donut-metric-card__progress');
expect(componentStyles).not.toContain('stroke-linecap: round;');
expect(componentStyles).toContain('transform: rotate(-90deg);');
expect(componentStyles).toContain('.donut-metric-card--all { color: var(--donut-all); }');
expect(componentStyles).toContain('.donut-metric-card--deviation { color: var(--donut-deviation); }');
expect(componentStyles).toContain('.donut-metric-card--passed { color: var(--donut-passed); }');
expect(componentStyles).toContain('.donut-metric-card--not-applicable { color: var(--donut-not-applicable); }');
expect(componentStyles).toContain('.donut-metric-card--not-run { color: var(--donut-not-run); }');
expect(componentStyles).toContain('.donut-metric-card--error { color: var(--donut-error); }');
expect(tokens).toContain('--font-ui: "Inter Variable", sans-serif;');
expect(tokens).toContain('--font-donut-display: var(--font-ui);');
expect(tokens).toContain('--donut-rack-surface:');
expect(tokens).toContain('--donut-ring-size: 128px;');
expect(tokens).toContain('--donut-ring-stroke: 12px;');
expect(tokens).toContain('--datahub-readiness-dot-size: 14px;');
expect(tokens).toContain('--datahub-readiness-dot-inset: var(--space-1);');
expect(tokens).toContain('Donut-Standard: identische Geometrie, helle Referenzpalette.');
expect(tokens.match(/--segment-surface: transparent;/g)).toHaveLength(2);
expect(tokens.match(/--segment-border: transparent;/g)).toHaveLength(2);
expect(tokens).toContain('--donut-rack-surface: var(--segment-surface);');
expect(tokens).toContain('--donut-deviation: #9A6500;');
expect(donut).toContain('<circle className="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100" />');
expect(donut).toContain('d={donutArcPath(boundedPercentage)}');
expect(componentStyles).toContain('fill: currentColor;');
expect(donut).toContain('key={label + \'-\' + value + \'-\' + boundedPercentage}');
expect(donut).not.toContain('<animate');
expect(componentStyles).toContain('@keyframes donut-progress-build');
expect(componentStyles).not.toContain('@keyframes donut-accent-build');
expect(overview).toContain('datahub-reference-controls donut-metric-rack');
expect(overview).toContain('DonutMetricCard className="datahub-reference-knob" label="Results All" value={resultMetrics.all} percentage={resultMetrics.all !== null ? 100 : 0} percentageBasisLabel="Of all records" tone="all"');
expect(overview).toContain('DonutMetricCard className="datahub-reference-knob" label="Deviation"');
expect(overview).toContain('DonutMetricCard className="datahub-reference-knob" label="Passed"');
expect(overview).toContain('DonutMetricCard className="datahub-reference-knob" label="Not Applicable"');
expect(overview).toContain('DonutMetricCard className="datahub-reference-knob" label="Not Run"');
expect(overview).toContain('DonutMetricCard className="datahub-reference-knob" label="Error"');
expect(overview.match(/<DonutMetricCard/g)?.length).toBe(6);
expect(overview.indexOf('datahub-status-module')).toBeLessThan(overview.indexOf('datahub-tenants-module'));
expect(overview).toContain('aria-pressed={selected}');
expect(overview).toContain('onClick={() => onSelectProject(project.id)}');
expect(overview).toContain('className="btn datahub-tenant-show"');
expect(overview).toContain('datahub-tenant-add');
expect(overview).toContain('summarizeClientProcessingReadiness');
expect(overview).toContain('const activeProcessingRunIds = groups.flatMap(group => group.scanRunIds);');
expect(overview).toContain('activeProcessingRunIds');
expect(overview).toContain('data-status={processingSummary.state}');
expect(overview).not.toContain('EmptyTenantColumn');
expect(overview).not.toContain('datahub-history-grid');
expect(datahub).toContain('getTimeslotResultMetrics(');
expect(datahub).toContain('selectedTimeslot.assessmentIds');
expect(datahub).not.toContain('loadCanonicalProjectWorkspace(');
expect(datahub).toContain("'Reimport recommended'");
expect(datahub).toContain('className="tenant-processing-alert tenant-processing-alert-inline"');
expect(datahub).toContain('New ASSESSOR processing profile available');
expect(datahub).toContain("aria-label={`${report.tool.name}: ${processingHeading}`}");
expect(datahub).toContain("processing.state !== 'ready' && <section");
expect(datahub).not.toContain('{affectedReports.length > 0 && <section');
expect(datahub).toContain('replacesScanRunId={report.scanRunId}');
expect(datahub).toContain('targetAssessmentId={report.assessmentIds[0]}');
expect(datahub).toContain('Use imported {report.tool.name}');
expect(datahub).toContain("compareReimportSourceHashes(affected.files.map(file => file.sha256), report.files.map(file => file.sha256)) === 'match'");
expect(datahub).toContain('adoptScanRunReplacement');
expect(datahub).toContain('const selectProject = useCallback((projectId: string) => {');
expect(datahub).toContain('setTimeslotMetrics(null);');
expect(datahub).toContain('setScanSelection({');
expect(datahub).toContain('onSelectProject={selectProject}');
expect(overview).toContain("selected || processingSummary.state === 'update' || processingSummary.state === 'reference-update' || processingSummary.state === 'review' || processingSummary.state === 'error'");
expect(overview).toContain('data-status={processingSummary.state}');
expect(datahub).toContain('setExpandedTimeslotIds(affectedTimeslotIds)');
expect(datahub).toContain('open={expandedTimeslotIds.includes(group.id)}');
expect(datahubTableStyles).toContain('.tenant-report-status[data-status="reimport-recommended"],');
expect(datahubTableStyles).toContain('.tenant-report-status[data-status="review-required"] { color: var(--warning); }');
expect(datahubTableStyles).toContain('.tenant-processing-alert[data-status="error"]');
expect(datahubTableStyles).toContain('.tenant-processing-alert-inline {');
expect(datahub).toContain('<MitreMatrixLibrary');
expect(datahub).not.toContain('SHOW_MITRE_MATRIX_LIBRARY');
expect(datahubStyles).toContain('.datahub-reference-row > .mitre-library { grid-area: 1 / 1 / 2 / -1; }');
expect(baselines).toContain('<strong>Imported Security Baselines</strong>');
expect(baselines).toContain('className="tenant-scan-history baseline-catalog"');
expect(baselines).not.toContain('className="tenant-scan-history baseline-catalog" open');
expect(baselines).toContain('className="tenant-history-meta"');
expect(baselines).toContain('className="tenant-history-meta baseline-title"><b>{records.length.toLocaleString(\'en-US\')}</b><strong>Imported Security Baselines</strong>');
expect(baselines).toContain('<TriangleIcon direction="right"/>');
expect(baselines).not.toContain('⌃');
expect(datahubTableStyles).toContain('.tenant-scan-history > .tenant-file-table {');
expect(datahubTableStyles).toContain('.viewer.app-area-data-hub { scrollbar-gutter: stable; }');
expect(datahubTableStyles).toContain('.tenant-scan-history .tenant-file-row {');
expect(datahubTableStyles).toContain('.tenant-scan-history .tenant-file-head {');
expect(datahubTableStyles).toContain('.tenant-scan-history .tenant-file-row b {');
expect(baselines).toContain('Upload Security Baselines (ZIP)');
expect(baselines).toContain('className="btn datahub-maintenance-action baseline-upload-action"');
expect(baselines).toContain('<span>Actions</span>');
expect(baselines).toContain('className="btn tenant-scan-delete"');
expect(baselines).toContain('Delete security baseline?');
expect(baselines).toContain('Baseline and derived evaluations will be deleted.');
expect(baselines).not.toContain('remain protected and cannot be deleted');
expect(baselines.indexOf('Imported Security Baselines')).toBeLessThan(baselines.indexOf('Upload Security Baselines (ZIP)'));
});
it.each([
['dead-letter', 'error', true],
['recovery-required', 'error', true],
['partial', 'ready', false],
['queued', 'processing', false],
['running', 'processing', false]
] as const)('shows actionable Manage only when needed for an unselected %s GPO client', (state, indicator, showManage) => {
const markup = renderToStaticMarkup(createElement(DatahubHistoryOverview, {
projects: [{ id: 'synthetic', name: 'Synthetic', description: '', attributes: {}, createdAt: '2026-09-06T00:00:00Z', updatedAt: '2026-09-06T00:00:00Z' }],
projectData: {}, projectHistory: { synthetic: {
version: 1, projectId: 'synthetic',
assessments: [{ id: 'synthetic-slot', projectId: 'synthetic', name: 'Synthetic slot', effectiveAt: '2026-09-06T00:00:00Z', createdAt: '2026-09-06T00:00:00Z', updatedAt: '2026-09-06T00:00:00Z' }],
scanRuns: []
} },
projectGpoData: { synthetic: [{ version: 1, artifactId: 'synthetic-artifact', fileName: 'synthetic.zip', sizeBytes: 1, submittedAt: '2026-09-06T00:00:00Z', updatedAt: '2026-09-06T00:00:00Z', state, attempts: 3, maxAttempts: 3, gpoCount: 0, backupInstanceCount: 0 }] },
selection: null, timeslotMetrics: null, loading: false, comparisonBusy: false, comparisonError: null,
onCreate: () => undefined, onShow: () => undefined, onOpen: () => undefined, onSelectProject: () => undefined
}));
expect(markup).toContain(`datahub-tenant-processing-dot" data-status="${indicator}"`);
expect(markup).toContain('aria-pressed="false"');
expect(markup.includes('>Manage</button>')).toBe(showManage);
expect(markup).toContain('datahub-tenant-open" type="button" disabled');
if (indicator !== 'ready') expect(markup).not.toContain('datahub-tenant-processing-dot" data-status="ready"');
});
it('renders only real client cards, exactly one selected client action, and one add card', () => {
const project = (id: string, name: string) => ({
id, name, description: '', createdAt: '2026-08-22T08:00:00.000Z', updatedAt: '2026-08-22T08:00:00.000Z', attributes: {}
});
const markup = renderToStaticMarkup(createElement(DatahubHistoryOverview, {
projects: [project('project-a', 'Client A'), project('project-b', 'Client B')],
projectData: {},
projectHistory: {},
projectGpoData: {},
selection: { projectId: 'project-b', slots: [] },
timeslotMetrics: null,
loading: false,
comparisonBusy: false,
comparisonError: null,
onCreate: () => undefined,
onShow: () => undefined,
onOpen: () => undefined,
onSelectProject: () => undefined
}));
expect(markup.match(/aria-label="Mandant Client [AB] auswählen"/g)?.length).toBe(2);
expect(markup.match(/aria-pressed="true"/g)?.length).toBe(1);
expect(markup.match(/>Manage<\/button>/g)?.length).toBe(1);
expect(markup.match(/>Open<\/button>/g)?.length).toBe(2);
expect(markup.match(/title="No scan data available"/g)?.length).toBe(2);
expect(markup).not.toContain('>Show</button>');
expect(markup).toContain('aria-label="Mandant anlegen"');
expect(markup).not.toContain('Empty');
});
});