210 lines
16 KiB
TypeScript
210 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*transparent;[^}]*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("const dashArray = String(boundedPercentage) + ' ' + String(100 - boundedPercentage);");
|
|
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).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 > 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('loadCanonicalProjectWorkspace(');
|
|
expect(datahub).toContain('selectedTimeslot.assessmentIds');
|
|
expect(datahub).toContain('summarizeWorkspaceResults(sheet.analysis.records, profileForSheet(dataset, sheet))');
|
|
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('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('setScanSelection({');
|
|
expect(datahub).toContain('onSelectProject={selectProject}');
|
|
expect(overview).toContain("selected || processingSummary.state === '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(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).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');
|
|
});
|
|
});
|