107 lines
6.4 KiB
TypeScript
107 lines
6.4 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { FindingRecord } from '../../src/domain/finding';
|
|
import { 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 the reference metric strip for compliance, primary statuses and the compact summary', () => {
|
|
expect(component).toContain('className="dashboard-status-rack"');
|
|
expect(component).toContain('label="Compliance Score"');
|
|
expect(component).toContain('<DashboardStatusMetric label="Deviation"');
|
|
expect(component).toContain('<DashboardStatusMetric label="Passed"');
|
|
expect(component).toContain('<DashboardStatusMetric label="Not Run"');
|
|
expect(component).toContain('className="dashboard-status-summary"');
|
|
expect(component).toContain('<span>Not Applicable</span>');
|
|
expect(component).toContain('<span>Error</span>');
|
|
expect(component).toContain('className="dashboard-ioe-summary" type="button"');
|
|
expect(component).toContain('setPathFindingIds(explicitIoeRecordIds)');
|
|
expect(styles).toMatch(/\.dashboard-status-rack\s*\{[^}]*grid-template-columns:\s*290px repeat\(3, 210px\) 250px;/s);
|
|
expect(styles).toMatch(/\.dashboard-status-copy > strong\s*\{[^}]*display:\s*grid;[^}]*white-space:\s*normal;/s);
|
|
expect(styles).toMatch(/\.dashboard-status-copy > strong small, \.dashboard-status-copy > small\s*\{[^}]*display:\s*block;/s);
|
|
expect(styles).toContain('grid-template-columns: repeat(2, minmax(0, 290px));');
|
|
expect(component).toContain("{value.toLocaleString('en-US')}<small>{roundedPercentage}%</small>");
|
|
expect(styles).toContain('.dashboard-status-ring-track { stroke: var(--donut-track); }');
|
|
expect(styles).toContain('button.dashboard-status-metric:hover { border-color: currentColor; background: var(--tile-surface-hover); }');
|
|
});
|
|
|
|
it('uses paired segmented distributions and a compact lower dashboard row', () => {
|
|
expect(component).toContain('heading="Severity distribution"');
|
|
expect(component).toContain('heading="Scan source distribution"');
|
|
expect(component).toContain('className="dashboard-lower-grid"');
|
|
expect(styles).toMatch(/\.dashboard-distribution-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2,/s);
|
|
expect(styles).toContain('.dashboard-segmented-bar');
|
|
});
|
|
|
|
it('matches the reference structure for directory coverage and risk profile', () => {
|
|
expect(component).toContain('className="dashboard-coverage-summary"');
|
|
expect(component).toContain('<small>typed observations</small>');
|
|
expect(component).toContain('className="dashboard-block dashboard-risk-profile"');
|
|
expect(component).toContain('className="dashboard-marked-summary"');
|
|
expect(component).toContain('className="dashboard-risk-tool"');
|
|
expect(component).toContain('<option value={fullRiskProfileId}>Full Risk Profile</option>');
|
|
expect(component).toContain('className="dashboard-risk-donut"');
|
|
expect(component).toContain('<span className="dashboard-donut-primary is-compact"><strong>100</strong><small>%</small></span>');
|
|
expect(styles).toContain('.dashboard-risk-donut-track { stroke: var(--donut-track); }');
|
|
expect(styles).toMatch(/\.dashboard-donut-primary\s*\{[^}]*position:\s*relative;/s);
|
|
expect(styles).toMatch(/\.dashboard-status-ring > span small\s*\{[^}]*left:\s*calc\(100% \+ var\(--space-1\)\);/s);
|
|
expect(component).toContain('categories.slice(0, 4)');
|
|
expect(component).toContain('matchesDynamicFacets(record, [sourceFacet]');
|
|
expect(component).not.toContain('dashboard-review-block');
|
|
expect(styles).toMatch(/\.dashboard-facts\s*\{[^}]*grid-template-columns:\s*repeat\(4,/s);
|
|
expect(styles).toMatch(/\.dashboard-risk-body\s*\{[^}]*grid-template-columns:/s);
|
|
});
|
|
|
|
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('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']);
|
|
});
|
|
});
|