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

167 lines
12 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 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(6, minmax(0, 1fr));');
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"] { 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: var(--bw-emphasis) solid var(--button-marked-bg);');
expect(datahubStyles).toContain('.datahub-tenant-card::before { content: none; }');
expect(datahubStyles).toContain('transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .12s ease;');
expect(datahubStyles).toMatch(/\.datahub-tenant-card:hover,\s*\.datahub-tenant-card:focus-within\s*\{[^}]*border-color:\s*var\(--line-strong\);[^}]*background:\s*var\(--panel-hover\);[^}]*box-shadow:\s*var\(--shadow-raised-hover\);[^}]*transform:\s*translateY\(-2px\);/s);
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).toContain('grid-template-columns: repeat(6, minmax(0, 1fr));');
expect(componentStyles).toContain('padding: var(--space-3);');
expect(componentStyles).toContain('.donut-metric-card::before');
expect(componentStyles).toContain('border-radius: var(--donut-card-radius);');
expect(componentStyles).toContain('background: linear-gradient(145deg, var(--donut-card-surface), var(--donut-card-surface-end));');
expect(componentStyles).toContain('.donut-metric-card__progress');
expect(componentStyles).toContain('stroke-linecap: round;');
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-donut-display: "Bahnschrift Condensed"');
expect(tokens).toContain('--donut-rack-surface:');
expect(tokens).toContain('--donut-ring-size: 128px;');
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).toContain('--donut-rack-surface: #E7E9ED;');
expect(tokens).toContain('--donut-deviation: #9A6500;');
expect(donut).toContain('strokeDasharray="75 25"');
expect(donut).toContain('const arcLength = boundedPercentage * 0.75;');
expect(donut).toContain("const progressStyle = { '--donut-progress-dash': dashArray } as CSSProperties;");
expect(donut).toContain('key={label + \'-\' + value + \'-\' + boundedPercentage}');
expect(donut).not.toContain('<animate');
expect(componentStyles).toContain('@keyframes donut-progress-build');
expect(componentStyles).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 > 0 ? 100 : 0} 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('summarizeTimeslotResults(models, selectedTimeslot.runIds)');
expect(datahub).toContain("'Reimport recommended'");
expect(datahub).toContain('className="tenant-processing-alert"');
expect(datahub).toContain('New ASSESSOR processing profile available');
expect(datahub).toContain('ASSESSOR has updated its internal scanner-processing profile.');
expect(datahub).toContain('replacesScanRunId={report.scanRunId}');
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('setScanSelection({');
expect(datahub).toContain('onSelectProject={selectProject}');
expect(overview).toContain("selected || processingSummary.state === 'update' || 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"] { color: var(--warning); }');
expect(datahubTableStyles).toContain('.tenant-processing-alert[data-status="error"]');
expect(datahub).toContain('const SHOW_MITRE_MATRIX_LIBRARY = false;');
expect(datahub).toContain('{SHOW_MITRE_MATRIX_LIBRARY && <MitreMatrixLibrary');
expect(datahubStyles).toContain('.datahub-reference-row.is-mitre-hidden > .baseline-catalog { 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.indexOf('Imported Security Baselines')).toBeLessThan(baselines.indexOf('Upload Security Baselines (ZIP)'));
});
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');
});
});