setScoringOpen(false)} />}
-
+
-
+
} snapshot={environment.snapshot} />
diff --git a/tests/app/app-version.test.ts b/tests/app/app-version.test.ts
index a30e967..ee10e00 100644
--- a/tests/app/app-version.test.ts
+++ b/tests/app/app-version.test.ts
@@ -9,7 +9,7 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
describe('ASSESSOR version in the brand header', () => {
it('shows the centrally maintained product version and opens About from the brand', () => {
- expect(ASSESSOR_PRODUCT_VERSION).toBe('2.8.6');
+ expect(ASSESSOR_PRODUCT_VERSION).toBe('2.8.7');
expect(app).toContain("import { ASSESSOR_PRODUCT_VERSION } from '../domain/assessor-release';");
expect(app).toContain('aria-label="About ASSESSOR"');
expect(app).toContain("import assessorLogo from '../assets/branding/assessor-icon-transparent.png';");
diff --git a/tests/features/client-info-facts.test.tsx b/tests/features/client-info-facts.test.tsx
new file mode 100644
index 0000000..9655a16
--- /dev/null
+++ b/tests/features/client-info-facts.test.tsx
@@ -0,0 +1,62 @@
+// @vitest-environment jsdom
+import { act } from 'react';
+import { createRoot } from 'react-dom/client';
+import { afterEach, expect, it, vi } from 'vitest';
+import { useApp } from '../../src/app/AppProvider';
+import { AssessorApiClient } from '../../src/api/assessor-api-client';
+import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
+import { clientFactsSnapshot, clientFactsState } from '../fixtures/client-info-facts';
+
+vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() }));
+vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => Synthetic timeslot }));
+afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); });
+
+it.each(['populated', 'unavailable', 'non-numeric count'])('shows each overview fact once in the existing tiles and preserves Results: %s', async mode => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const snapshot = structuredClone(clientFactsSnapshot);
+ if (mode === 'unavailable') { snapshot.observations = []; snapshot.sections = []; }
+ if (mode === 'non-numeric count') {
+ const count = snapshot.observations.find(item => item.property === 'domain.controller_count')!;
+ count.originalValue = count.normalizedValue = 'Two reported controllers'; count.valueType = 'string';
+ }
+ vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(snapshot);
+ vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
+ vi.mocked(useApp).mockReturnValue(clientFactsState() as unknown as ReturnType);
+ const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
+ try {
+ await act(async () => root.render());
+ const domain = host.querySelector('.client-domain-information')!;
+ const groups = [...host.querySelectorAll('.client-env-group')];
+ const directory = groups.find(group => group.querySelector('h3')?.textContent === 'Directory')!;
+ const inventory = groups.find(group => group.querySelector('h3')?.textContent === 'Inventory')!;
+ const labels = (element: Element) => [...element.querySelectorAll('dt')].map(item => item.textContent);
+ expect(host.querySelector('.client-posture')).not.toBeNull();
+ expect(host.querySelector('.client-posture-connectors')).not.toBeNull();
+ expect(host.querySelector('.client-priority')).not.toBeNull();
+ expect(host.querySelectorAll('.client-data-fact')).toHaveLength(6);
+ expect(groups).toHaveLength(5);
+ expect(labels(directory)).toEqual(['NetBIOS name', 'Schema version', 'Domain created', 'Entra ID']);
+ expect(labels(domain)).toEqual(mode === 'non-numeric count'
+ ? ['Domain', 'Forest', 'Domain controllers', 'Domain functional level', 'Forest functional level', 'DC OS']
+ : ['Domain', 'Forest', 'Domain functional level', 'Forest functional level', 'DC OS']);
+ expect(labels(inventory)).toEqual(['Enabled / disabled', 'Password never expires', 'Admin groups', 'Trusts', 'Assessed host']);
+ // Equal domain/forest and functional-level values are distinct facts, not duplicates.
+ if (mode !== 'unavailable') {
+ expect([...domain.querySelectorAll('dd')].filter(item => item.textContent === 'contoso.example')).toHaveLength(2);
+ expect([...domain.querySelectorAll('dd')].filter(item => item.textContent === 'Windows Server 2016')).toHaveLength(2);
+ } else expect([...domain.querySelectorAll('dd')].every(item => item.textContent === '—')).toBe(true);
+ for (const [title, group, expected] of [
+ ['Directory', directory, ['Domain', 'Forest', 'Domains', 'Domain functional level', 'Schema version']],
+ ['Inventory', inventory, ['User accounts', 'Computer objects', 'Domain controllers', 'Assessed host']]
+ ] as const) {
+ const trigger = group.querySelector('button')!;
+ expect(trigger.getAttribute('aria-label')).toBe(`Open ${title} results`);
+ await act(async () => trigger.click());
+ const popup = document.querySelector('.floating-data-window')!;
+ for (const label of expected) expect(labels(popup)).toContain(label);
+ if (mode !== 'unavailable') expect(popup.querySelector('table')).not.toBeNull();
+ await act(async () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })));
+ expect(document.querySelector('.floating-data-window')).toBeNull();
+ }
+ } finally { await act(async () => root.unmount()); }
+});
diff --git a/tests/features/dashboard-environment-detail.test.ts b/tests/features/dashboard-environment-detail.test.ts
index 6b247f6..7ff97ec 100644
--- a/tests/features/dashboard-environment-detail.test.ts
+++ b/tests/features/dashboard-environment-detail.test.ts
@@ -122,7 +122,7 @@ describe('client info environment detail', () => {
expect(component).not.toContain('className="client-environment-detail"');
expect(component).not.toContain('environmentDetailRef');
expect(component).toContain('');
- expect(component).toContain('');
+ expect(component).toContain('');
expect(component).toContain('unit: \'/100 ASSESSOR\'');
expect(component).toContain('PingCastle risk ${exactNative}/100');
expect(component).toContain('useGpoInventory');
diff --git a/tests/fixtures/client-info-facts.ts b/tests/fixtures/client-info-facts.ts
new file mode 100644
index 0000000..32e20a0
--- /dev/null
+++ b/tests/fixtures/client-info-facts.ts
@@ -0,0 +1,48 @@
+import type { EnvironmentSnapshot, EnvironmentObservation, ScannerReportSection } from '../../src/domain/environment-context';
+import { makeSheet } from '../../src/importers/sheet-factory';
+import { genericProfile } from '../../src/profiles/generic/generic-profile';
+
+const tool = { id: 'synthetic', name: 'Synthetic scanner' };
+const observation = (property: string, value: string | number): EnvironmentObservation => ({
+ version: 1, id: property, scanRunId: 'synthetic-run', importFileId: 'synthetic-file', tool,
+ subject: { type: 'directory' }, property, label: property, originalValue: value, normalizedValue: value,
+ valueType: typeof value === 'number' ? 'number' : 'string', state: 'observed',
+ source: { rowIndex: 0 }, mappingVersion: '1', attributes: {}
+});
+const section = (sectionKey: string, name: string, headers: string[], rows: (string | number)[][]): ScannerReportSection => ({
+ version: 1, id: sectionKey, scanRunId: 'synthetic-run', importFileId: 'synthetic-file', tool,
+ sectionKey, label: name, ordinal: 0, state: 'observed', tables: [{ name, headers, rows }],
+ source: { rowIndex: 0 }, extractorVersion: '1', attributes: {}
+});
+
+export const clientFactsSnapshot: EnvironmentSnapshot = {
+ version: 1, projectId: 'synthetic-project', scanRunIds: ['synthetic-run'], generatedAt: '2026-01-15T00:00:00Z',
+ coverage: { observed: 11, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 },
+ observations: [
+ observation('domain.fqdn', 'contoso.example'), observation('forest.name', 'contoso.example'),
+ observation('domain.netbios_name', 'CONTOSO'), observation('domain.count', 1),
+ observation('domain.controller_count', 2), observation('domain.functional_level', 'Windows Server 2016'),
+ observation('forest.functional_level', 'Windows Server 2016'), observation('forest.schema_version', '87'),
+ observation('domain.created_at', '2020-01-15'), observation('entra.tenant_name', 'Synthetic tenant'),
+ observation('host.name', 'Synthetic host')
+ ],
+ sections: [
+ section('sectionDomainInformation', 'Directory evidence', ['Domain', 'Forest'], [['contoso.example', 'contoso.example']]),
+ section('sectionUserInformation', 'Account analysis', ['Nb User Accounts', 'Nb Enabled', 'Nb Disabled', 'Nb pwd never Expire'], [[16, 15, 1, 5]]),
+ section('sectionComputerInformation', 'Computer analysis', ['Nb Computer Accounts'], [[24]])
+ ]
+};
+
+export function clientFactsState() {
+ const sheet = makeSheet('Synthetic layout', [
+ ['Name', 'Status', 'Severity', 'Source: Scan'], ['Synthetic finding', 'Failed', 'High', 'Scanner A']
+ ], '', 'synthetic.csv', { isProjectSheet: true });
+ return {
+ dataset: { fileName: 'Synthetic layout', sheets: [sheet], scanTool: 'Generic' },
+ activeSheet: sheet, activeProfile: genericProfile, reviews: {}, facetFilters: {},
+ targetProjectId: 'synthetic-project', activeScanRunIds: ['synthetic-run'], activeAssessmentIds: ['synthetic-assessment'],
+ backendStatus: 'unavailable', projects: [], assessments: [], selectedProject: null,
+ setStatusFilter() {}, setFacetFilter() {}, setPathFindingIds() {}, resetFilters() {},
+ setView() {}, setActionOnly() {}, setActiveSheet() {}
+ };
+}
diff --git a/tests/server/assessor-http-server.test.ts b/tests/server/assessor-http-server.test.ts
index 093e3fa..dd4dbce 100644
--- a/tests/server/assessor-http-server.test.ts
+++ b/tests/server/assessor-http-server.test.ts
@@ -52,7 +52,7 @@ describe('local ASSESSOR HTTP server', () => {
const health = await fetch(`${baseUrl}/api/v1/health`);
expect(await health.json()).toEqual({
- status: 'ok', application: 'ASSESSOR', productVersion: '2.8.6', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
+ status: 'ok', application: 'ASSESSOR', productVersion: '2.8.7', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
});
expect(health.headers.get('access-control-allow-origin')).toBeNull();
});