312 lines
22 KiB
TypeScript
312 lines
22 KiB
TypeScript
import { expect, it, vi } from 'vitest';
|
||
import { JSDOM } from 'jsdom';
|
||
import { buildContextManagementHtml } from '../../src/reporting/context-management-report';
|
||
import { buildReportProjection } from '../../src/reporting/report-projection';
|
||
import { buildTechnicalReportHtml } from '../../src/reporting/technical-report';
|
||
import { ReportContextRequiredError } from '../../src/reporting/report-context';
|
||
import { reportContext } from './context-report-fixture';
|
||
import { managementBusinessImpacts, managementFindingDistribution, managementThemes } from '../../src/reporting/management-business-impact';
|
||
import { RELEASED_ASSESSMENT_CONTEXT_CATALOG } from '../../src/assessment-context/context-catalog';
|
||
import { managementCategoryTexts } from '../../src/reporting/management-report-texts';
|
||
|
||
function projection() {
|
||
const projection = buildReportProjection({ project: { id: 'dummy-project', name: 'Synthetic <Customer>' },
|
||
generatedAt: '2026-10-02T12:00:00Z', results: [], domains: [], domainOf: () => 'unknown' });
|
||
projection.context = reportContext();
|
||
return projection;
|
||
}
|
||
|
||
it('counts 477 unique deviations once per theme even when their technical evidence has 482 rows', () => {
|
||
const source = projection();
|
||
const cluster = source.context!.clusters[0];
|
||
cluster.id = 'data-sharing-exposure';
|
||
cluster.scanner = 476; cluster.gpo = 1; cluster.hasEvidence = true;
|
||
cluster.resultIds = Array.from({ length: 482 }, (_, index) => `technical-${index}`);
|
||
cluster.findingKeys = Array.from({ length: 477 }, (_, index) => `finding-${index}`);
|
||
source.context!.findingCount = 477;
|
||
source.coverage.deviations = 560;
|
||
source.workspaceCounts = { deviations: 477, assessed: 507 };
|
||
source.scoreSources = [
|
||
{ toolId: 'synthetic-scanner', method: 'pass-rate', weight: 1, passed: 10, deviations: 452, assessed: 462, issues: [] },
|
||
{ toolId: 'group-policy', method: 'pass-rate', weight: 1, passed: 20, deviations: 25, assessed: 45, issues: [] }
|
||
];
|
||
source.context!.clusters = [cluster, { ...cluster, id: 'cloud-external-access-exposure' }];
|
||
const before = JSON.stringify(source);
|
||
const themes = managementBusinessImpacts(source.context!);
|
||
expect(themes.find(item => item.id === 'information')!.findingCount).toBe(477);
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
expect(document.querySelector('.two-column p')?.textContent).toContain('Von 507 bewerteten Ergebnissen weichen 477');
|
||
const row = [...document.querySelectorAll('.business-overview tbody tr')].find(item => item.firstElementChild?.textContent === 'Informationen & Daten')!;
|
||
expect(row.children[2]?.textContent).toBe('477');
|
||
const bar = [...document.querySelectorAll('.bar-row')].find(item => item.firstElementChild?.textContent === 'Informationen & Daten')!;
|
||
expect(bar.querySelector('strong')?.textContent).toBe('477');
|
||
expect(JSON.stringify(source)).toBe(before);
|
||
});
|
||
|
||
it('renders the offline management cut with confidence, limitations and no hidden technical evidence', () => {
|
||
const source = projection();
|
||
const before = JSON.stringify(source);
|
||
const html = buildContextManagementHtml(source);
|
||
const document = new JSDOM(html).window.document;
|
||
for (const title of ['Summary', 'Context-Heatmap', 'Möglicher Business Impact',
|
||
'Bearbeitungsstand und Entscheidungen', 'Untersuchungsumfang und Datenbasis', 'Technische Domain-Auswertung']) expect(document.body.textContent).toContain(title);
|
||
expect(document.querySelectorAll('.heatmap li')).toHaveLength(2);
|
||
expect(document.querySelector('.title-meta span')?.textContent).toBe('Summary');
|
||
expect(document.querySelector('.two-column h2')?.textContent).toBe('Summary');
|
||
expect(document.body.textContent).not.toContain('Zusammenfassung');
|
||
expect(document.body.textContent).toContain('Die folgenden Angaben beschreiben je Thema den möglichen Business Impact');
|
||
expect(document.body.textContent).not.toContain('Die folgenden Blätter');
|
||
expect(document.body.textContent).not.toContain('Findings bleiben ohne fachliche Cluster-Zuordnung');
|
||
expect(document.querySelector('.bar-chart')?.parentElement?.querySelector('.note')).toBeNull();
|
||
expect(document.body.textContent).not.toContain('Anzahl zugeordneter Findings je Thema. Mehrfachzuordnungen');
|
||
expect(document.querySelector('.action-fields h3')?.textContent).toBe('Handlungsfelder');
|
||
// Evidenced themes with their exclusive counts, largest first.
|
||
expect([...document.querySelectorAll('.action-fields li')].map(item => item.textContent?.split(' – ')[0])).toEqual(
|
||
managementBusinessImpacts(source.context!).filter(item => item.clusters.length).map(item => item.label));
|
||
expect(document.querySelector('.action-fields li')?.textContent).toMatch(/ – \d+ Abweichung(en)?$/);
|
||
expect(document.querySelectorAll('.report-sheet')).toHaveLength(8);
|
||
expect(document.querySelectorAll('.masthead img[alt="ASSESSOR Logo"]')).toHaveLength(8);
|
||
expect(document.querySelector('.masthead img')?.getAttribute('src')).toMatch(/^data:image\/png;base64,/);
|
||
expect(document.querySelectorAll('.business-impact')).toHaveLength(4);
|
||
expect(document.querySelectorAll('footer')[7].textContent).toContain('Berichtsblatt 8 von 8');
|
||
expect(document.querySelector('.heatmap .critical')!.textContent).toContain('2 Abweichungen');
|
||
expect(document.querySelector('.heatmap .undetermined')!.textContent).toContain('2 Abweichungen');
|
||
expect(document.querySelector('.business-impact tbody tr')?.lastElementChild?.textContent).toBe('2');
|
||
expect(document.querySelector('.business-impact')?.textContent).not.toMatch(/Critical|High|Medium|Base Severity|\bhoch\b|\bmittel\b|\bniedrig\b/);
|
||
// Uncatalogued deviations are no open business mapping; they appear as Ohne Problemkategorie.
|
||
expect(document.body.textContent).not.toContain('Keine Business-Impact-Zuordnung; fachlich prüfen.');
|
||
expect(document.body.textContent).toContain('Kein belegter Assessment-Vergleich');
|
||
expect(html).not.toMatch(/<script\b|<link\b|https?:\/\/|secret-result|secret-gpo|secret-other/i);
|
||
expect(html).toContain('Synthetic <Customer>');
|
||
expect(JSON.stringify(source)).toBe(before);
|
||
// The Technical Report deepens the same problem fields without exposing Result IDs.
|
||
const technical = buildTechnicalReportHtml(source);
|
||
expect(technical).toContain('Cloud Privileged Access Exposure');
|
||
expect(technical).not.toMatch(/secret-result|secret-gpo|secret-other/);
|
||
});
|
||
|
||
it('requires Context, preserves neutral empty areas and distinguishes reviews from proven progress', () => {
|
||
const source = projection();
|
||
const context = source.context!;
|
||
delete source.context;
|
||
expect(() => buildContextManagementHtml(source)).toThrow(ReportContextRequiredError);
|
||
source.context = context;
|
||
context.clusters[0].hasEvidence = false; context.clusters[0].scanner = 0;
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
expect(document.querySelector('.heatmap li')!.className).toBe('neutral');
|
||
expect(document.body.textContent).toContain('Arbeitsstatus und Risikoakzeptanz beweisen keine Behebung');
|
||
expect(document.body.textContent).toContain('Neutral bedeutet nicht sicher');
|
||
source.remediation = { selfReported: true, items: 0, byStatus: {}, withoutOwner: 0, withoutDue: 0, riskAccepted: 2 };
|
||
expect(buildContextManagementHtml(source)).toContain('Für 2 Abweichungen ist eine Risikoakzeptanz dokumentiert.');
|
||
});
|
||
|
||
it('shows only counts in evidence tables while preserving underlying confidence data', () => {
|
||
const source = projection();
|
||
source.context!.clusters[0].confidence = { catalogued: 7, recognized: 3, estimated: 2 };
|
||
const before = JSON.stringify(source);
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
const table = document.querySelector('.business-impact table')!;
|
||
expect([...table.querySelectorAll('thead th')].map(cell => cell.textContent)).toEqual(['Problemfeld', 'Anteil', 'Anzahl']);
|
||
expect(table.querySelector('tbody tr')?.lastElementChild?.textContent).toBe('2');
|
||
expect(document.body.textContent).not.toMatch(/Nach Katalog|Nach Befundmerkmalen|Thematisch eingeordnet|7 \/ 3 \/ 2/);
|
||
expect(document.body.textContent).not.toContain('Automatische Einordnungen und mögliche Folgen sind als fachlich zu prüfender Ansatz zu lesen.');
|
||
expect(JSON.stringify(source)).toBe(before);
|
||
});
|
||
|
||
it('deduplicates overlapping evidence, leaves unknown categories unassigned and never treats absent evidence as safe', () => {
|
||
const source = projection();
|
||
const context = source.context!;
|
||
context.clusters.push({ ...context.clusters[0], id: 'ad-privileged-account-exposure', label: 'Synthetic AD control',
|
||
resultIds: ['secret-result-two', 'secret-result-three'] });
|
||
const themes = managementBusinessImpacts(context);
|
||
expect(themes.find(item => item.id === 'control')?.findingCount).toBe(3);
|
||
expect(themes.find(item => item.id === 'control')?.clusters).toHaveLength(2);
|
||
expect(themes.find(item => item.id === 'resilience')?.clusters).toHaveLength(0);
|
||
expect(themes.every(item => item.clusters.every(cluster => cluster.id !== 'other-deviations'))).toBe(true);
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
const resilience = [...document.querySelectorAll('.report-sheet')].find(sheet => sheet.getAttribute('aria-label') === 'Widerstandsfähigkeit & Wiederanlauf')!;
|
||
expect(resilience.textContent).toContain('keine Aussage ableitbar');
|
||
expect(resilience.textContent).not.toContain('Lücken bei Erkennung, Aktualität oder Wiederherstellung');
|
||
expect(document.body.textContent).not.toMatch(/Dasselbe Finding|Fehlende Belege bedeuten/);
|
||
context.clusters.forEach(cluster => { cluster.hasEvidence = false; });
|
||
expect(managementBusinessImpacts(context).every(item => item.findingCount === 0 && item.clusters.length === 0)).toBe(true);
|
||
});
|
||
|
||
it('creates the management report locally with deterministic text and no network call', () => {
|
||
const network = vi.fn(() => { throw new Error('Network is forbidden'); });
|
||
vi.stubGlobal('fetch', network);
|
||
try {
|
||
const source = projection();
|
||
expect(buildContextManagementHtml(source)).toBe(buildContextManagementHtml(source));
|
||
expect(network).not.toHaveBeenCalled();
|
||
expect(buildContextManagementHtml(source)).toContain('Offline erzeugt, ohne LLM und ohne Internetabruf.');
|
||
} finally { vi.unstubAllGlobals(); }
|
||
});
|
||
|
||
it('uses ASSESSOR branding only, marks every sheet confidential and limits Management to its header', () => {
|
||
const source = projection();
|
||
source.context!.clusters[0].tools = ['maester', 'pingcastle', 'purple-knight', 'cis-cat'];
|
||
source.context!.clusters[0].sources = [{ publisher: 'Maester', title: 'Maester scan evidence', url: 'https://example.invalid' }];
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
expect(document.body.textContent).not.toMatch(/maester|pingcastle|purple.?knight|cis.?cat|Scanner|Scanner-Quellen|Ergebnisse nach Quelle/i);
|
||
for (const sheet of document.querySelectorAll('.report-sheet')) {
|
||
expect(sheet.querySelector('header')?.textContent).toContain('Management Report');
|
||
expect(sheet.querySelector('header')?.textContent).toContain('Streng vertraulich');
|
||
expect(sheet.querySelector('footer')?.textContent).toContain('Streng vertraulich');
|
||
}
|
||
document.querySelectorAll('header').forEach(header => header.remove());
|
||
expect(document.body.textContent).not.toMatch(/Management/i);
|
||
});
|
||
|
||
it.each([0, 62.4, 100])('draws the existing ASSESSOR Overall Score without recalculation (%s)', score => {
|
||
const source = projection(); source.overallScore = score;
|
||
const before = JSON.stringify(source);
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
expect(document.querySelector('.overall-score svg')?.getAttribute('aria-label')).toContain(score.toLocaleString('de-DE'));
|
||
expect(document.querySelector('.score-value')?.getAttribute('stroke-dasharray')).toBe(`${score} ${100 - score}`);
|
||
expect(JSON.stringify(source)).toBe(before);
|
||
});
|
||
|
||
it.each([undefined, NaN, -1, 101])('does not fabricate a score when unavailable or invalid (%s)', score => {
|
||
const source = projection(); source.overallScore = score;
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
expect(document.querySelector('.overall-score svg')).toBeNull();
|
||
expect(document.querySelector('.overall-score')?.textContent).toContain('Kein Overall Score');
|
||
});
|
||
|
||
it('renders an exclusive partition in table and chart whose sum matches the finding total', () => {
|
||
const source = projection();
|
||
const distribution = managementFindingDistribution(source.context!, source.context!.findingCount);
|
||
const counts = [...distribution.themes.map(theme => String(theme.count)), String(distribution.unassigned)];
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
expect([...document.querySelectorAll('.business-overview tbody tr')].map(row => row.children[2]?.textContent)).toEqual(counts);
|
||
const sheet = document.querySelector('.business-overview')!.parentElement!;
|
||
expect([...sheet.querySelectorAll('.bar-row strong')].map(value => value.textContent)).toEqual(counts);
|
||
expect(sheet.querySelector('figcaption')?.textContent).toBe('Abweichungen nach Hauptthema');
|
||
expect(counts.reduce((sum, count) => sum + Number(count), 0)).toBe(source.context!.findingCount);
|
||
});
|
||
|
||
it('keeps conflicting main themes, missing assignments and out-of-scope findings out of named bars', () => {
|
||
const source = projection();
|
||
const cluster = source.context!.clusters[0];
|
||
source.context!.clusters = [
|
||
{ ...cluster, id: 'adcs-escalation', findingKeys: ['one', 'conflict', 'stale'] },
|
||
{ ...cluster, id: 'data-sharing-exposure', findingKeys: ['two', 'conflict'] },
|
||
{ ...cluster, id: 'detection-blind-spots', findingKeys: ['three'] }
|
||
];
|
||
const before = JSON.stringify(source);
|
||
const distribution = managementFindingDistribution(source.context!, 5, ['one', 'two', 'three', 'conflict', 'missing']);
|
||
expect(distribution.themes.map(theme => theme.count)).toEqual([1, 1, 0, 1]);
|
||
expect(distribution.unassigned).toBe(2);
|
||
const reversed = { ...source.context!, clusters: [...source.context!.clusters].reverse() };
|
||
expect(managementFindingDistribution(reversed, 5, ['missing', 'conflict', 'three', 'two', 'one'])).toEqual(distribution);
|
||
expect(JSON.stringify(source)).toBe(before);
|
||
});
|
||
|
||
it('assigns every catalogued category to exactly one of four themes and counts downstream consequences nowhere', () => {
|
||
// User decision 2026-10-04: four disjoint themes; financial and compliance consequences are prose only.
|
||
expect(managementThemes.map(theme => theme.id)).toEqual(['control', 'information', 'continuity', 'resilience']);
|
||
const categories = RELEASED_ASSESSMENT_CONTEXT_CATALOG.clusters.map(cluster => cluster.id).filter(id => id !== 'other-deviations');
|
||
for (const id of categories) expect(managementThemes.filter(theme => theme.clusterIds.includes(id)).map(theme => theme.id), id).toHaveLength(1);
|
||
expect(managementThemes.flatMap(theme => theme.clusterIds).every(id => categories.includes(id))).toBe(true);
|
||
const document = new JSDOM(buildContextManagementHtml(projection())).window.document;
|
||
expect([...document.querySelectorAll('.report-sheet')].map(sheet => sheet.getAttribute('aria-label'))).not.toEqual(
|
||
expect.arrayContaining(['Finanzielle Auswirkungen']));
|
||
expect([...document.querySelectorAll('.report-sheet')].some(sheet => /Compliance & Haftung/.test(sheet.getAttribute('aria-label') ?? ''))).toBe(false);
|
||
expect(document.body.textContent).toContain('Sie werden nicht als eigenes Thema gezählt.');
|
||
});
|
||
|
||
it('gives every catalogued category considered text and states each reading note only once', () => {
|
||
// User request 2026-10-04: considered text blocks instead of generic phrases repeated in every row.
|
||
const categories = RELEASED_ASSESSMENT_CONTEXT_CATALOG.clusters.map(cluster => cluster.id).filter(id => id !== 'other-deviations');
|
||
for (const id of categories) {
|
||
const text = managementCategoryTexts[id];
|
||
expect(text, id).toBeDefined();
|
||
for (const value of [text.name, text.situation, text.significance, text.lever]) expect(value.trim().length, id).toBeGreaterThan(0);
|
||
}
|
||
const all = Object.values(managementCategoryTexts).flatMap(text => [text.name, text.situation, text.significance, text.lever]).join(' ');
|
||
expect(all).not.toMatch(/pingcastle|purple.?knight|cis.?cat|maester|scuba/i);
|
||
expect(all).not.toMatch(/kein Nachweis|Mehrere beobachtete Befunde|Mehrere deklarierte Einstellungen/);
|
||
const source = projection();
|
||
source.context!.clusters.push({ ...source.context!.clusters[0], id: 'credential-relay-exposure', label: 'Credential Relay Exposure', severity: 'high' });
|
||
const html = new JSDOM(buildContextManagementHtml(source)).window.document.body.textContent ?? '';
|
||
expect(html.match(/kein Nachweis/g)).toHaveLength(1);
|
||
expect(html).toContain('Weiterleitung von Anmeldungen');
|
||
expect(html).toContain('Sachverhalt.');
|
||
});
|
||
|
||
it('places theme evidence tables at full width with narrow numeric columns', () => {
|
||
// User correction 2026-10-04: tables outside Summary span the sheet; counts and shares never wrap.
|
||
const document = new JSDOM(buildContextManagementHtml(projection())).window.document;
|
||
const table = document.querySelector('.business-impact table')!;
|
||
expect(table.closest('.two-column')).toBeNull();
|
||
expect([...table.querySelectorAll('thead th')].map(cell => cell.className)).toEqual(['', 'num', 'num']);
|
||
expect(document.querySelector('.business-overview table')?.closest('.two-column')).toBeNull();
|
||
const css = document.querySelector('style')!.textContent!;
|
||
expect(css).toContain('th.num,td.num{width:1%;white-space:nowrap;text-align:right');
|
||
expect(css).not.toContain('overflow-wrap:anywhere');
|
||
});
|
||
|
||
it('lists the themes in one count order in summary, overview, chart and theme sheets', () => {
|
||
const source = projection();
|
||
const [first, second] = source.context!.clusters;
|
||
Object.assign(first, { id: 'data-sharing-exposure', scanner: 3, gpo: 0, hasEvidence: true,
|
||
findingKeys: ['k1', 'k2', 'k3'], resultIds: ['k1', 'k2', 'k3'] });
|
||
Object.assign(second, { id: 'lateral-movement-exposure', scanner: 5, gpo: 0, hasEvidence: true,
|
||
findingKeys: ['k4', 'k5', 'k6', 'k7', 'k8'], resultIds: ['k4', 'k5', 'k6', 'k7', 'k8'] });
|
||
source.context!.findingCount = 8;
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
const themes = managementBusinessImpacts(source.context!);
|
||
const counts = new Map(themes.map(item => [item.label, item.findingCount]));
|
||
const expected = [...themes].sort((left, right) => right.findingCount - left.findingCount).map(item => item.label);
|
||
expect(counts.get(expected[0])).toBe(5);
|
||
const overview = [...document.querySelectorAll('.business-overview tbody tr')].map(item => item.firstElementChild?.textContent);
|
||
const chart = [...document.querySelectorAll('.bar-row')].map(item => item.firstElementChild?.textContent);
|
||
const sheets = [...document.querySelectorAll('.report-sheet')].map(item => item.getAttribute('aria-label')).filter(label => counts.has(label!));
|
||
const actions = [...document.querySelectorAll('.action-fields li')].map(item => item.textContent?.split(' – ')[0]);
|
||
expect(overview.slice(0, 4)).toEqual(expected);
|
||
expect(chart.slice(0, 4)).toEqual(expected);
|
||
expect(sheets).toEqual(expected);
|
||
expect(actions).toEqual(expected.filter(label => counts.get(label)! > 0));
|
||
expect(document.querySelector('.two-column p:nth-of-type(2)')?.textContent).toContain(`Den größten Anteil hat ${expected[0]}`);
|
||
});
|
||
|
||
it('renders Work-level domain rows that reconcile to the headline counts when the projection provides them', () => {
|
||
const source = projection();
|
||
source.workspaceCounts = { deviations: 4, assessed: 7 };
|
||
source.workspaceDomains = [
|
||
{ domainId: 'identity', domainLabel: 'Identity & Authentication', passed: 2, deviations: 1, assessed: 3 },
|
||
{ domainId: 'hardening', domainLabel: 'System Hardening', passed: 1, deviations: 3, assessed: 4 }
|
||
];
|
||
source.domains = [{ domainId: 'hardening', domainLabel: 'System Hardening', passed: 9, deviations: 9, assessed: 18, evidence: { passed: [], deviations: [] } }];
|
||
const document = new JSDOM(buildContextManagementHtml(source)).window.document;
|
||
const heading = [...document.querySelectorAll('h2')].find(item => item.textContent === 'Technische Domain-Auswertung')!;
|
||
const rows = [...heading.nextElementSibling!.querySelectorAll('tbody tr')].map(row => [...row.children].map(cell => cell.textContent));
|
||
expect(rows).toEqual([['Identity & Authentication', '3', '1', '2', '33 %'], ['System Hardening', '4', '3', '1', '75 %']]);
|
||
expect(document.body.textContent).toContain('System Hardening die höchste Abweichungsquote (3 von 4, 75 %)');
|
||
expect(document.body.textContent).toContain('die Summen entsprechen den bewerteten Ergebnissen und Abweichungen der Summary');
|
||
});
|
||
|
||
it('takes unassessed states, collections and the risk register from the Client Info counting rules', () => {
|
||
// Synthetic regression (Dummy 2026-10-05): result rows gave 170 not run and 0 unknown, Work shows 171 and 19;
|
||
// processing steps were counted as 38 collections; a draft risk acceptance read as documented acceptance.
|
||
const source = projection();
|
||
source.coverage.notRun = 170; source.coverage.unknown = 0; source.coverage.executionErrors = 2; source.coverage.notApplicable = 3;
|
||
source.workspaceCounts = { deviations: 4, assessed: 7, notRun: 171, executionErrors: 2, notApplicable: 3, notConfigured: 879, unknown: 19 };
|
||
source.riskRegister = { recorded: 1, current: 0 };
|
||
source.remediation = { selfReported: true, items: 0, byStatus: {}, withoutOwner: 0, withoutDue: 0, riskAccepted: 1 };
|
||
source.provenance = [
|
||
{ kind: 'ScanRun', id: 'scan-1', observedAt: '2026-09-08T12:00:00Z' },
|
||
{ kind: 'ScanRun', id: 'gpo-collection', observedAt: '2026-09-03T14:00:00Z' },
|
||
{ kind: 'ImportFile: synthetic.xml', id: 'file-1', observedAt: '2026-10-04T10:00:00Z', sourceDate: '2026-07-22T08:00:00Z' },
|
||
{ kind: 'EvaluationRun: completed', id: 'evaluation-1', observedAt: '2026-10-04T11:00:00Z' }
|
||
];
|
||
const text = new JSDOM(buildContextManagementHtml(source)).window.document.body.textContent!;
|
||
expect(text).toContain('171 Prüfungen wurden nicht ausgeführt, 2 endeten mit einem Ausführungsfehler');
|
||
expect(text).toMatch(/Nicht ausgeführte Prüfungen\s*171/);
|
||
expect(text).toContain('Nicht anwendbar: 3 · nicht konfiguriert: 879 · sonstige unbewertete Ergebnisse: 19.');
|
||
expect(text).toContain('Grundlage sind 2 Erhebungen im Timeslot vom 22.7.2026.');
|
||
expect(text).toContain('Im Risikoregister ist eine Risikoakzeptanz erfasst; keine davon ist aktuell gültig.');
|
||
expect(text).not.toContain('Risikoakzeptanz dokumentiert');
|
||
});
|