135 lines
5.5 KiB
TypeScript
135 lines
5.5 KiB
TypeScript
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { genericProfile } from '../../src/profiles/generic/generic-profile';
|
|
import { summarizeSeverities, toggleSeveritySelection } from '../../src/features/sidebar/SidebarSeverityMetrics';
|
|
import { deriveDynamicFacets } from '../../src/features/search-filter/dynamic-facets';
|
|
import { compactSeveritySelection, DynamicFilterControls } from '../../src/features/search-filter/DynamicFilterControls';
|
|
|
|
const rows = [
|
|
['Title', 'Severity', 'Result'],
|
|
['Deviation 1', 'Critical', 'Failed'],
|
|
['Deviation 2', 'HIGH', 'Failed'],
|
|
['Deviation 3', 'hoch', 'Investigate'],
|
|
['Passed', 'Medium', 'Passed'],
|
|
['Deviation 4', 'Low', 'Failed'],
|
|
['Deviation 5', 'Info', 'Failed'],
|
|
['Unrated deviation', 'Unrated', 'Failed']
|
|
];
|
|
|
|
function options(statusFilter = 'deviation', facetFilters = {}) {
|
|
return {
|
|
sheet: makeSheet('Synthetic findings', rows, '', 'synthetic.csv'),
|
|
dataset: null,
|
|
activeProfile: genericProfile,
|
|
reviews: {},
|
|
statusFilter,
|
|
facetFilters,
|
|
search: '',
|
|
actionOnly: false
|
|
};
|
|
}
|
|
|
|
describe('sidebar severity metrics', () => {
|
|
it('uses Deviation as the diagram basis and excludes unknown source values', () => {
|
|
const summary = summarizeSeverities(options());
|
|
expect(summary?.metrics.map(metric => [metric.severity, metric.count, metric.percentage])).toEqual([
|
|
['critical', 1, 20],
|
|
['high', 2, 40],
|
|
['medium', 0, 0],
|
|
['low', 1, 20],
|
|
['info', 1, 20]
|
|
]);
|
|
expect(summary?.metrics.map(metric => metric.severity)).not.toContain('unknown');
|
|
});
|
|
|
|
it('follows an explicit user status filter without changing it', () => {
|
|
const summary = summarizeSeverities(options('all'));
|
|
expect(summary?.metrics.find(metric => metric.severity === 'medium')).toMatchObject({ count: 1 });
|
|
});
|
|
|
|
it('exposes exactly the five known severity filter values', () => {
|
|
const base = options();
|
|
const summary = summarizeSeverities(base);
|
|
if (!summary) throw new Error('Synthetic severity facet missing');
|
|
expect(summary.facetId).toBeTruthy();
|
|
expect(summary.metrics.map(metric => metric.severity)).toEqual([
|
|
'critical', 'high', 'medium', 'low', 'info'
|
|
]);
|
|
});
|
|
|
|
it('toggles multiple severities in canonical display order', () => {
|
|
expect(toggleSeveritySelection([], 'high')).toEqual(['high']);
|
|
expect(toggleSeveritySelection(['high'], 'critical')).toEqual(['critical', 'high']);
|
|
expect(toggleSeveritySelection(['critical', 'high'], 'high')).toEqual(['critical']);
|
|
expect(toggleSeveritySelection(['critical'], 'critical')).toEqual([]);
|
|
});
|
|
|
|
it('renders a compact multi-severity label in the filter control', () => {
|
|
const base = options();
|
|
const summary = summarizeSeverities(base);
|
|
if (!summary) throw new Error('Synthetic severity facet missing');
|
|
const severityFacet = deriveDynamicFacets(base.sheet, null, genericProfile, {})
|
|
.find(facet => facet.id === summary.facetId);
|
|
if (!severityFacet) throw new Error('Synthetic severity facet missing');
|
|
|
|
const html = renderToStaticMarkup(
|
|
<DynamicFilterControls
|
|
facets={[severityFacet]}
|
|
filters={{
|
|
[summary.facetId]: {
|
|
values: ['info', 'low', 'medium', 'high', 'critical']
|
|
}
|
|
}}
|
|
statusFilter="deviation"
|
|
onStatusFilter={() => undefined}
|
|
onFilter={() => undefined}
|
|
/>
|
|
);
|
|
|
|
expect(compactSeveritySelection(['info', 'critical', 'medium'])).toBe('Crit-Med-Inf');
|
|
expect(html).toContain('Crit-High-Med-Low-Inf');
|
|
expect(html).toContain('value="__severity_multi__" selected=""');
|
|
});
|
|
|
|
it('highlights a status only when it differs from the profile default', () => {
|
|
const base = options();
|
|
const statusFacet = deriveDynamicFacets(base.sheet, null, genericProfile, {})
|
|
.find(facet => facet.source === 'profile-status');
|
|
if (!statusFacet) throw new Error('Synthetic status facet missing');
|
|
const renderStatus = (statusFilter: string) => renderToStaticMarkup(
|
|
<DynamicFilterControls
|
|
facets={[statusFacet]}
|
|
filters={{}}
|
|
statusFilter={statusFilter}
|
|
defaultStatusFilter="deviation"
|
|
onStatusFilter={() => undefined}
|
|
onFilter={() => undefined}
|
|
/>
|
|
);
|
|
|
|
expect(renderStatus('deviation')).not.toContain('is-filtered');
|
|
expect(renderStatus('all')).toContain('is-filtered');
|
|
});
|
|
|
|
it('marks Source: Scan as the final right-edge filter control', () => {
|
|
const scanSheet = makeSheet('Synthetic findings', [
|
|
['Title', 'Severity', 'Source: Scan'],
|
|
['One', 'High', 'Scanner A'],
|
|
['Two', 'Low', 'Scanner B']
|
|
], '', 'synthetic.csv');
|
|
const facets = deriveDynamicFacets(scanSheet, null, genericProfile, {});
|
|
const html = renderToStaticMarkup(<DynamicFilterControls facets={facets} filters={{}} statusFilter="all"
|
|
onStatusFilter={() => undefined} onFilter={() => undefined} />);
|
|
expect(facets.at(-1)?.label).toBe('Source: Scan');
|
|
expect(html).toContain('dynamic-facet-select is-source-scan');
|
|
expect(html.lastIndexOf('aria-label="Source: Scan"')).toBeGreaterThan(html.indexOf('aria-label="Primary filters"'));
|
|
});
|
|
|
|
it('does not render a rack when the active sheet has no severity field', () => {
|
|
const missing = options();
|
|
missing.sheet = makeSheet('Synthetic findings', [['Title', 'Result'], ['One', 'Failed']], '', 'synthetic.csv');
|
|
expect(summarizeSeverities(missing)).toBeUndefined();
|
|
});
|
|
});
|