197 lines
14 KiB
TypeScript
197 lines
14 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { FindingRecord } from '../../src/domain/finding';
|
|
import type { EnvironmentSnapshot } from '../../src/domain/environment-context';
|
|
import { dashboardCisCatSeverityRemainder, dashboardEnvironmentSummary, dashboardExplicitIoeCount, dashboardExplicitIoeRecordIds, dashboardPingCastleRiskCategories, dashboardRiskTools, dashboardScanSourceLabel } from '../../src/features/dashboard/DashboardPanel';
|
|
|
|
const component = readFileSync(new URL('../../src/features/dashboard/DashboardPanel.tsx', import.meta.url), 'utf8');
|
|
const styles = readFileSync(new URL('../../src/styles/datahub.css', import.meta.url), 'utf8');
|
|
|
|
describe('dashboard reference layout', () => {
|
|
it('uses exactly six compact square metrics with the existing timeslot context', () => {
|
|
expect(component).toContain('className="client-info-tile-grid"');
|
|
expect(component).toContain('className="client-info-metric-grid"');
|
|
expect(component).toContain('<WorkTimeslotControl />');
|
|
expect(styles).toMatch(/\.dashboard-page\s*\{[^}]*container-type:\s*inline-size;/s);
|
|
expect(styles).toContain('.dashboard-context .work-timeslot-select { margin-left: auto; }');
|
|
expect(component.match(/<DashboardMetricTile/g)).toHaveLength(6);
|
|
expect(component).toContain('<DashboardMetricTile label="Compliance Score"');
|
|
expect(component).toContain('<DashboardMetricTile label="Deviation"');
|
|
expect(component).toContain('<DashboardMetricTile label="Passed"');
|
|
expect(component).toContain('<DashboardMetricTile label="High Severity"');
|
|
expect(component).toContain('<DashboardMetricTile label="Typed Observations"');
|
|
expect(component).toContain('<DashboardMetricTile label="Directory Sections"');
|
|
expect(component.indexOf('label="Compliance Score"')).toBeLessThan(component.indexOf('label="Deviation"'));
|
|
expect(component.indexOf('label="Deviation"')).toBeLessThan(component.indexOf('label="High Severity"'));
|
|
expect(component.indexOf('label="High Severity"')).toBeLessThan(component.indexOf('label="Passed"'));
|
|
expect(component).toContain('<DashboardMetricTile label="Compliance Score" value={`${complianceScore}%`} emphasis />');
|
|
expect(component).toContain('<DashboardMetricTile label="Passed" value={passed.toLocaleString(\'en-US\')} tone="passed" />');
|
|
expect(component).toMatch(/label="Deviation"[^\n]*onClick=/);
|
|
expect(component).toMatch(/label="High Severity"[^\n]*onClick=/);
|
|
expect(component).toContain('setPathFindingIds(explicitIoeRecordIds)');
|
|
expect(styles).toMatch(/\.client-info-tile-grid\s*\{[^}]*display:\s*grid;[^}]*gap:\s*var\(--segment-gap\);/s);
|
|
expect(component).toContain('className="client-info-top-row"');
|
|
expect(styles).toMatch(/\.client-info-top-row\s*\{[^}]*grid-template-columns:\s*minmax\(0, 3fr\) minmax\(320px, 1fr\);/s);
|
|
expect(styles).toMatch(/\.client-info-top-row\s*\{[^}]*align-items:\s*stretch;/s);
|
|
expect(styles).toMatch(/\.client-info-metric-grid\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(6, minmax\(0, 1fr\)\);/s);
|
|
expect(styles).toMatch(/\.client-info-top-row > \.client-info-metric-grid \.client-info-metric\s*\{[^}]*align-self:\s*stretch;[^}]*height:\s*100%;[^}]*min-height:\s*220px;[^}]*aspect-ratio:\s*auto;/s);
|
|
expect(styles).toMatch(/\.client-info-metric\.is-emphasis > strong\s*\{[^}]*font-size:\s*var\(--type-h1-size\);[^}]*font-weight:\s*var\(--fw-semibold\);/s);
|
|
});
|
|
|
|
it('uses exactly ten progressively disclosed information cards in the same dense grid', () => {
|
|
expect(component).toContain('className="client-info-card-grid"');
|
|
expect(component.match(/className="client-info-card-column"/g)).toHaveLength(3);
|
|
expect(component.match(/<DashboardLargeCard/g)).toHaveLength(8);
|
|
expect(component.match(/<DashboardDonutCard/g)).toHaveLength(2);
|
|
['Assessment Status', 'Severity Distribution', 'Directory Overview', 'Directory Coverage', 'Identity & Privilege', 'Control Paths & Trusts', 'Certificates & PKI', 'GPO & Password Hygiene', 'Infrastructure & Resilience', 'Infrastructure Details']
|
|
.forEach(title => expect(component).toContain(`title="${title}"`));
|
|
expect(styles).toMatch(/\.client-info-card-grid\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);/s);
|
|
expect(styles).toMatch(/\.client-info-card-column\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*gap:\s*var\(--segment-gap\);/s);
|
|
expect(component).not.toContain('DashboardMasonryGrid');
|
|
expect(component).not.toContain('gridRowEnd');
|
|
expect(styles).toMatch(/\.client-info-panels-layout\s*\{[^}]*grid-template-columns:\s*minmax\(0, 3fr\) minmax\(320px, 1fr\);/s);
|
|
expect(styles).toMatch(/\.client-info-card\s*\{[^}]*min-height:\s*min-content;[^}]*align-self:\s*stretch;[^}]*padding:\s*0;[^}]*border:\s*0;[^}]*border-radius:\s*var\(--radius-sm\);[^}]*background:\s*var\(--tile-surface\);[^}]*box-shadow:\s*none;/s);
|
|
expect(styles).toMatch(/\.client-info-card > header\s*\{[^}]*min-height:\s*60px;[^}]*padding:\s*var\(--space-3\) var\(--space-5\);[^}]*border-bottom:\s*var\(--border-hair\) solid var\(--line\);/s);
|
|
expect(component).toContain('className="client-info-card-toggle"');
|
|
expect(component).toContain('aria-expanded={expanded}');
|
|
expect(component).toContain("<TriangleIcon direction={expanded ? 'down' : 'right'} />");
|
|
expect(styles).toMatch(/\.client-info-card-toggle \.triangle-icon\s*\{[^}]*width:\s*16px;[^}]*height:\s*16px;/s);
|
|
expect(component).toContain('initiallyExpanded = true');
|
|
expect(component.match(/initiallyExpanded=\{false\}/g)).toHaveLength(9);
|
|
expect(component).toContain('<DashboardDonutCard title="Assessment Status"');
|
|
expect(component).not.toMatch(/<DashboardDonutCard title="Assessment Status"[^>]*initiallyExpanded=\{false\}/);
|
|
expect(styles).toMatch(/\.client-info-card\.is-collapsed\s*\{[^}]*align-self:\s*start;[^}]*width:\s*100%;/s);
|
|
expect(styles).toMatch(/\.client-info-card\.is-collapsed > header\s*\{[^}]*border-bottom:\s*0;/s);
|
|
expect(component).not.toContain('className="client-info-card-summary-track"');
|
|
expect(component.match(/summary=\{\{/g)).toHaveLength(7);
|
|
expect(component).not.toContain('className="dashboard-status-rack"');
|
|
expect(component).not.toContain('className="dashboard-lower-grid"');
|
|
});
|
|
|
|
it('projects normalized scanner context into reusable fact, quantity-bar and donut displays', () => {
|
|
expect(component).toContain('dashboardEnvironmentSummary(environment.snapshot)');
|
|
expect(component).toContain('className="client-info-donut"');
|
|
expect(component).toContain('className="client-info-bars"');
|
|
expect(component).toContain('className="client-info-facts"');
|
|
expect(component).toContain('environmentSummary.controlPaths.slice(1)');
|
|
expect(component).toContain('environmentSummary.identity.slice(1)');
|
|
expect(component).toContain("const ordinalTones = ['critical', 'high', 'medium', 'accent', 'neutral']");
|
|
expect(component).toContain("sectionObservationCount(snapshot, 'sectionAdminGroups')");
|
|
expect(component).toContain("observationNumber(snapshot, 'risk.anomalies', 'Anomalies score')");
|
|
expect(styles).toContain('.client-info-donut-track { stroke: var(--donut-track); }');
|
|
expect(styles).not.toMatch(/\.client-info-card\s*\{[^}]*border-left:/s);
|
|
expect(styles).not.toMatch(/\.client-info-card\s*\{[^}]*overflow:\s*hidden;/s);
|
|
});
|
|
|
|
it('aligns metric labels, tracks, values and optional status text on a stable row grid', () => {
|
|
expect(component).toContain('className="client-info-bar-detail"');
|
|
expect(component).toContain("className={`client-info-bar-status${item.restricted ? '' : ' is-empty'}`}");
|
|
expect(styles).toMatch(/\.client-info-bars\s*\{[^}]*gap:\s*0;/s);
|
|
expect(styles).toMatch(/\.client-info-bar-row\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) minmax\(48px, auto\) minmax\(120px, 1\.15fr\);[^}]*min-height:\s*64px;/s);
|
|
expect(styles).toMatch(/\.client-info-bar-track\s*\{[^}]*grid-column:\s*3;[^}]*grid-row:\s*1 \/ -1;[^}]*width:\s*100%;[^}]*height:\s*30px;/s);
|
|
expect(styles).toMatch(/\.client-info-bars strong\s*\{[^}]*grid-column:\s*2;[^}]*grid-row:\s*1;[^}]*justify-content:\s*flex-end;[^}]*white-space:\s*nowrap;/s);
|
|
expect(styles).toMatch(/\.client-info-bars \.client-info-bar-status\s*\{[^}]*grid-column:\s*2;[^}]*grid-row:\s*2;[^}]*min-height:\s*12px;/s);
|
|
});
|
|
|
|
it('keeps infrastructure metrics and the complete resilience facts in separate cards', () => {
|
|
expect(component).toMatch(/title="Infrastructure & Resilience"[\s\S]*?<DashboardMetricBars items=\{environmentSummary\.risk\.slice\(1\)\} \/>[\s\S]*?<\/DashboardLargeCard>/);
|
|
expect(component).toContain('<DashboardLargeCard title="Infrastructure Details" initiallyExpanded={false}>');
|
|
expect(component).toContain("{ label: 'Marked findings', value:");
|
|
expect(component).toContain('...environmentSummary.resilienceFacts');
|
|
expect(component).not.toContain('className="client-info-resilience"');
|
|
expect(styles).toMatch(/\.client-info-card-details\s*\{[^}]*display:\s*grid;[^}]*align-content:\s*start;[^}]*gap:\s*0;[^}]*min-width:\s*0;[^}]*padding:\s*0 var\(--space-5\) var\(--space-3\);/s);
|
|
expect(styles).toMatch(/\.client-info-facts > div\s*\{[^}]*grid-template-columns:\s*minmax\(110px, \.8fr\) minmax\(0, 1\.2fr\);[^}]*min-height:\s*56px;/s);
|
|
});
|
|
|
|
it('derives card values only from normalized environment snapshot contracts', () => {
|
|
const observation = (property: string, label: string, value: string | number | boolean, sectionKey?: string) => ({
|
|
version: 1 as const, id: `${property}-${label}`, scanRunId: 'run-1', importFileId: 'file-1',
|
|
tool: { id: 'synthetic', name: 'Synthetic', version: '1' }, subject: { type: 'directory' },
|
|
property, label, originalValue: value, normalizedValue: value, valueType: typeof value === 'number' ? 'number' as const : typeof value === 'boolean' ? 'boolean' as const : 'string' as const,
|
|
state: 'observed' as const, sectionKey, source: { rowIndex: 0 }, mappingVersion: '1', attributes: {}
|
|
});
|
|
const snapshot = {
|
|
version: 1, projectId: 'project-1', scanRunIds: ['run-1'], generatedAt: '2026-08-25T00:00:00.000Z',
|
|
coverage: { observed: 4, explicitlyAbsent: 1, empty: 2, restricted: 3, incomplete: 0 },
|
|
sections: [{ version: 1, id: 'section-1', scanRunId: 'run-1', importFileId: 'file-1', tool: { id: 'synthetic', name: 'Synthetic', version: '1' }, sectionKey: 'sectionAdminGroups', label: 'Groups', ordinal: 1, state: 'license-restricted', tables: [], source: { rowIndex: 0 }, extractorVersion: '1', attributes: {} }],
|
|
observations: [
|
|
observation('domain.fqdn', 'Domain', 'example.local'),
|
|
observation('risk.anomalies', 'Anomalies score', 42),
|
|
observation('admin.name', 'Name', 'Admin A', 'sectionAdminGroups')
|
|
]
|
|
} satisfies EnvironmentSnapshot;
|
|
const summary = dashboardEnvironmentSummary(snapshot);
|
|
expect(summary.directoryFacts[0]).toEqual({ label: 'Domain', value: 'example.local' });
|
|
expect(summary.directoryFacts.map(item => item.label)).not.toContain('PingCastle');
|
|
expect(summary.coverage.map(item => item.value)).toEqual([4, 2, 1, 3, 0]);
|
|
expect(summary.identity[0]).toMatchObject({ label: 'Admin groups', value: 1, restricted: true });
|
|
expect(summary.risk[1]).toMatchObject({ label: 'Anomalies', value: 42 });
|
|
});
|
|
|
|
it('removes only a trailing scan timestamp from source labels', () => {
|
|
expect(dashboardScanSourceLabel('PurpleKnight · 2026-08-22T08:15:30.000Z')).toBe('PurpleKnight');
|
|
expect(dashboardScanSourceLabel('CIS-CAT Pro · 2026-08-22')).toBe('CIS-CAT Pro');
|
|
expect(dashboardScanSourceLabel('Scanner · Production')).toBe('Scanner · Production');
|
|
});
|
|
|
|
it('groups scan runs into one selectable risk profile per tool', () => {
|
|
expect(dashboardRiskTools([
|
|
{ value: 'pingcastle-a', label: 'PingCastle · 2026-08-22T08:15:30.000Z', count: 20 },
|
|
{ value: 'pingcastle-b', label: 'PingCastle · 2026-08-23T08:15:30.000Z', count: 24 },
|
|
{ value: 'purpleknight-a', label: 'PurpleKnight · 2026-08-23T09:00:00.000Z', count: 30 }
|
|
])).toEqual([
|
|
{ id: 'pingcastle', label: 'PingCastle', sourceValues: ['pingcastle-a', 'pingcastle-b'], count: 44 },
|
|
{ id: 'purpleknight', label: 'PurpleKnight', sourceValues: ['purpleknight-a'], count: 30 }
|
|
]);
|
|
});
|
|
|
|
it('adds only the severity-less CIS-CAT share as a neutral distribution segment', () => {
|
|
expect(dashboardCisCatSeverityRemainder([
|
|
{ value: 'cis-cat-run', label: 'CIS-CAT Pro · 2026-08-17', count: 411 },
|
|
{ value: 'maester-run', label: 'Maester · 2026-07-28', count: 318 }
|
|
], [])).toEqual({
|
|
value: 'dashboard-cis-cat-without-severity',
|
|
label: 'CIS-CAT',
|
|
count: 411,
|
|
sourceValues: ['cis-cat-run']
|
|
});
|
|
expect(dashboardCisCatSeverityRemainder([
|
|
{ value: 'cis-cat-run', label: 'CIS-CAT Pro · 2026-08-17', count: 411 }
|
|
], [{ value: 'high', label: 'High', count: 12 }])?.count).toBe(399);
|
|
expect(styles).toContain('.severity-dashboard-cis-cat-without-severity { --severity: var(--donut-not-run); }');
|
|
});
|
|
|
|
it('keeps the complete PingCastle risk catalog including zero scores', () => {
|
|
expect(dashboardPingCastleRiskCategories([
|
|
{ property: 'risk.anomalies', normalizedValue: 20, originalValue: '20' },
|
|
{ property: 'risk.stale_objects', normalizedValue: 10, originalValue: '10' },
|
|
{ property: 'risk.privileged_accounts', normalizedValue: 7, originalValue: '7' },
|
|
{ property: 'risk.trusts', normalizedValue: 0, originalValue: '0' }
|
|
], [])).toEqual([
|
|
{ value: 'anomalies', label: 'Anomalies', count: 20 },
|
|
{ value: 'stale objects', label: 'Stale Objects', count: 10 },
|
|
{ value: 'privileged accounts', label: 'Privileged Accounts', count: 7 },
|
|
{ value: 'trusts', label: 'Trusts', count: 0 }
|
|
]);
|
|
});
|
|
|
|
it('counts only explicit IOE-found source results for the top summary', () => {
|
|
const record = (id: string, value: string): FindingRecord => ({
|
|
id, index: 0, title: id,
|
|
fields: [{ index: 0, label: 'Result', value }],
|
|
filled: [], meta: []
|
|
});
|
|
expect(dashboardExplicitIoeCount([
|
|
record('ioe', 'IOE Found'),
|
|
record('ioes', 'IOEs found'),
|
|
record('deviation', 'Failed'),
|
|
record('pass', 'Passed')
|
|
])).toBe(2);
|
|
expect(dashboardExplicitIoeRecordIds([
|
|
record('ioe', 'IOE Found'),
|
|
record('ioes', 'IOEs found'),
|
|
record('deviation', 'Failed')
|
|
])).toEqual(['ioe', 'ioes']);
|
|
});
|
|
});
|