Add grouped metric health cards with variant pills, hover tooltips, and info panel to compliance page

This commit is contained in:
root
2026-04-22 18:30:59 +00:00
parent aa3ce3bae9
commit 0bea387ac9
10 changed files with 2923 additions and 31 deletions

View File

@@ -41,5 +41,16 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!(fast-check)/)"
],
"moduleNameMapper": {
"^pure-rand/(.*)$": "<rootDir>/node_modules/pure-rand/lib/$1.js"
}
},
"devDependencies": {
"fast-check": "^4.7.0"
}
}

View File

@@ -1,14 +1,22 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw } from 'lucide-react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw, Info } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import ComplianceUploadModal from './ComplianceUploadModal';
import ComplianceDetailPanel from './ComplianceDetailPanel';
import ComplianceChartsPanel from './ComplianceChartsPanel';
import MetricInfoPanel from './MetricInfoPanel';
import metricDefinitionsRaw from '../../data/metricDefinitions.json';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const TEAL = '#14B8A6';
const TEAMS = ['STEAM', 'ACCESS-ENG'];
// Build definitions lookup map once at module level
const METRIC_DEFINITIONS = {};
for (const def of metricDefinitionsRaw) {
METRIC_DEFINITIONS[def.metric_id] = def;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -38,18 +46,83 @@ function pctDisplay(pct) {
return `${Math.round(pct * 100)}%`;
}
// Deduplicate summary entries — one per metric_id for the selected team
// (exclude aggregate "ALL: NTS-AEO" rows)
function teamMetrics(entries, team) {
return entries.filter(e => e.team === team);
const STATUS_SEVERITY = {
'Below 15% of Target': 0,
'Within 15% of Target': 1,
'Meets/Exceeds Target': 2,
};
function computeWorstStatus(statuses) {
let worst = 'Meets/Exceeds Target';
let worstSev = 2;
for (const s of statuses) {
const sev = STATUS_SEVERITY[s] ?? 0;
if (sev < worstSev) {
worstSev = sev;
worst = s;
}
}
return worst;
}
function groupByMetricFamily(allEntries, team) {
const teamEntries = allEntries.filter(e => e.team === team);
const familyMap = {};
for (const entry of teamEntries) {
const baseId = entry.metric_id;
if (!baseId) continue;
if (!familyMap[baseId]) {
familyMap[baseId] = [];
}
familyMap[baseId].push(entry);
}
return Object.entries(familyMap).map(([metricId, entries]) => ({
metricId,
entries,
category: entries[0].category,
target: entries[0].target,
worstStatus: computeWorstStatus(entries.map(e => e.status)),
}));
}
// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------
function MetricHealthCard({ entry, active, onClick }) {
function VariantPill({ entry }) {
const color = statusColor(entry.status);
const isOk = entry.status === 'Meets/Exceeds Target';
const isOk = entry.status === 'Meets/Exceeds Target';
return (
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.3rem',
padding: '0.15rem 0.45rem',
background: `${color}1F`,
borderRadius: '0.2rem',
border: `1px solid ${color}25`,
fontSize: '0.62rem',
fontFamily: 'monospace',
color: '#CBD5E1',
whiteSpace: 'nowrap',
}}>
{!isOk && (
<span style={{
width: '4px', height: '4px', borderRadius: '50%',
background: color, flexShrink: 0,
boxShadow: `0 0 5px ${color}`,
}} />
)}
<span style={{ color: '#94A3B8' }}>{entry.description || entry.team}</span>
<span style={{ color, fontWeight: '600' }}>{pctDisplay(entry.compliance_pct)}</span>
</span>
);
}
function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLookup }) {
const color = statusColor(family.worstStatus);
const isOk = family.worstStatus === 'Meets/Exceeds Target';
return (
<button
@@ -66,33 +139,58 @@ function MetricHealthCard({ entry, active, onClick }) {
transition: 'all 0.15s',
minWidth: '160px',
flex: '1 1 0',
position: 'relative',
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.borderColor = color + '80'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.borderColor = color + '40'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.borderColor = active ? color : color + '40'; }}
>
{/* Info icon — top-right */}
<span
onClick={(e) => { e.stopPropagation(); onInfoClick(family.metricId); }}
style={{
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
cursor: 'pointer',
color: '#475569',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0.15rem',
borderRadius: '0.2rem',
transition: 'color 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; }}
>
<Info style={{ width: '13px', height: '13px' }} />
</span>
{/* Metric ID */}
<div style={{ fontFamily: 'monospace', fontSize: '0.95rem', fontWeight: '700', color: active ? color : '#E2E8F0', marginBottom: '0.25rem' }}>
{entry.metric_id}
<div style={{ fontFamily: 'monospace', fontSize: '0.95rem', fontWeight: '700', color: active ? color : '#E2E8F0', marginBottom: '0.25rem', paddingRight: '1.25rem' }}>
{family.metricId}
</div>
{/* Category */}
<div style={{ fontSize: '0.65rem', color: '#475569', marginBottom: '0.625rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{entry.category}
<div style={{ fontSize: '0.65rem', color: '#475569', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{family.category}
</div>
{/* Compliance % */}
<div style={{ fontFamily: 'monospace', fontSize: '1.4rem', fontWeight: '700', color, lineHeight: 1, marginBottom: '0.3rem' }}>
{pctDisplay(entry.compliance_pct)}
{/* Variant pills */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', marginBottom: '0.5rem' }}>
{family.entries.map((entry, i) => (
<VariantPill key={entry.metric_id + '-' + i} entry={entry} />
))}
</div>
{/* Target */}
<div style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace' }}>
target {pctDisplay(entry.target)}
<div style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', marginBottom: '0.5rem' }}>
target {pctDisplay(family.target)}
</div>
{/* Status pill */}
<div style={{
marginTop: '0.625rem', display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
fontSize: '0.62rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.04em',
color, padding: '0.2rem 0.5rem',
background: `${color}12`, borderRadius: '999px',
@@ -103,7 +201,7 @@ function MetricHealthCard({ entry, active, onClick }) {
background: color, flexShrink: 0,
...(isOk ? {} : { boxShadow: `0 0 6px ${color}` }),
}} />
{isOk ? 'OK' : entry.status.replace(' of Target', '')}
{isOk ? 'OK' : family.worstStatus.replace(' of Target', '')}
</div>
</button>
);
@@ -158,6 +256,10 @@ export default function CompliancePage({ onNavigate }) {
const [rollbackConfirm, setRollbackConfirm] = useState(false);
const [rollbackLoading, setRollbackLoading] = useState(false);
const [rollbackResult, setRollbackResult] = useState(null);
const [infoMetric, setInfoMetric] = useState(null);
const [hoveredMetric, setHoveredMetric] = useState(null);
const hoverTimeoutRef = useRef(null);
const hoveredCardRef = useRef(null);
const fetchSummary = useCallback(async (team) => {
try {
@@ -225,10 +327,10 @@ export default function CompliancePage({ onNavigate }) {
// In-memory filters
const filteredDevices = devices
.filter(d => !metricFilter || d.failing_metrics.some(m => m.metric_id === metricFilter))
.filter(d => !metricFilter || d.failing_metrics.some(m => metricFilter.includes(m.metric_id)))
.filter(d => !hostSearch || d.hostname.toLowerCase().includes(hostSearch.toLowerCase()));
const metrics = teamMetrics(summary.entries, activeTeam);
const families = groupByMetricFamily(summary.entries, activeTeam);
const lastUpload = summary.upload;
return (
@@ -336,7 +438,7 @@ export default function CompliancePage({ onNavigate }) {
</div>
{/* ── Metric health cards ──────────────────────────────────── */}
{metrics.length > 0 ? (
{families.length > 0 ? (
<div style={{ marginBottom: '1.5rem' }}>
<div style={{ fontSize: '0.65rem', color: '#334155', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.625rem' }}>
Metric Health click to filter
@@ -348,15 +450,81 @@ export default function CompliancePage({ onNavigate }) {
)}
</div>
<div style={{ display: 'flex', gap: '0.625rem', flexWrap: 'wrap' }}>
{metrics.map(entry => (
<MetricHealthCard
key={entry.metric_id}
entry={entry}
active={metricFilter === entry.metric_id}
onClick={() => setMetricFilter(metricFilter === entry.metric_id ? null : entry.metric_id)}
/>
))}
{families.map(family => {
const familyIds = family.entries.map(e => e.metric_id);
const isActive = metricFilter !== null && metricFilter.length === familyIds.length && familyIds.every(id => metricFilter.includes(id));
return (
<div
key={family.metricId}
onMouseEnter={(e) => {
hoveredCardRef.current = e.currentTarget;
if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = setTimeout(() => setHoveredMetric(family.metricId), 300);
}}
onMouseLeave={() => {
if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
hoveredCardRef.current = null;
setHoveredMetric(null);
}}
style={{ display: 'flex', flex: '1 1 0', minWidth: '160px' }}
>
<MetricHealthCard
family={family}
active={isActive}
onClick={() => setMetricFilter(isActive ? null : familyIds)}
onInfoClick={(metricId) => setInfoMetric(metricId)}
definitionLookup={METRIC_DEFINITIONS}
/>
</div>
);
})}
</div>
{/* Hover tooltip */}
{hoveredMetric && (() => {
const family = families.find(f => f.metricId === hoveredMetric);
if (!family) return null;
const def = METRIC_DEFINITIONS[hoveredMetric];
const rect = hoveredCardRef.current ? hoveredCardRef.current.getBoundingClientRect() : null;
if (!rect) return null;
const tooltipTop = Math.min(rect.bottom + 8, window.innerHeight - 180);
const tooltipLeft = Math.max(8, Math.min(rect.left, window.innerWidth - 320));
return (
<div style={{
position: 'fixed',
top: tooltipTop,
left: tooltipLeft,
zIndex: 50,
width: '300px',
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: '1px solid rgba(20,184,166,0.25)',
borderRadius: '0.5rem',
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
padding: '0.75rem 0.875rem',
pointerEvents: 'none',
}}>
<div style={{ fontFamily: 'monospace', fontSize: '0.82rem', fontWeight: '700', color: '#E2E8F0', marginBottom: '0.4rem', lineHeight: 1.3 }}>
{def ? def.metric_title : (family.entries[0]?.description || hoveredMetric)}
</div>
{def && def.business_justification && (
<div style={{ fontSize: '0.72rem', color: '#94A3B8', marginBottom: '0.3rem', lineHeight: 1.4 }}>
{def.business_justification}
</div>
)}
{def && def.data_sources_required && (
<div style={{ fontSize: '0.65rem', color: '#475569', fontFamily: 'monospace' }}>
Sources: {def.data_sources_required}
</div>
)}
{!def && family.entries[0]?.description && (
<div style={{ fontSize: '0.72rem', color: '#94A3B8', lineHeight: 1.4 }}>
{family.entries[0].description}
</div>
)}
</div>
);
})()}
</div>
) : lastUpload === null ? (
<div style={{
@@ -486,6 +654,16 @@ export default function CompliancePage({ onNavigate }) {
/>
)}
{/* ── Metric info panel ───────────────────────────────────── */}
{infoMetric && (
<MetricInfoPanel
metricId={infoMetric}
definition={METRIC_DEFINITIONS[infoMetric] || null}
summaryEntries={(families.find(f => f.metricId === infoMetric) || {}).entries || []}
onClose={() => setInfoMetric(null)}
/>
)}
{/* ── Rollback confirmation modal ──────────────────────────── */}
{rollbackConfirm && lastUpload && (
<div style={{
@@ -655,3 +833,6 @@ function DeviceRow({ device, selected, onClick }) {
</div>
);
}
// Named exports for testing
export { computeWorstStatus, groupByMetricFamily };

View File

@@ -0,0 +1,161 @@
import React from 'react';
import { X } from 'lucide-react';
const TEAL = '#14B8A6';
const SECTION_FIELDS = [
{ key: 'asset_types', label: 'Asset Types' },
{ key: 'asset_types_in_scope', label: 'Asset Types In Scope' },
{ key: 'application_types_in_scope', label: 'Application Types In Scope' },
{ key: 'environment_in_scope', label: 'Environment In Scope' },
{ key: 'status_in_scope', label: 'Status In Scope' },
{ key: 'instance_types_in_scope', label: 'Instance Types In Scope' },
{ key: 'criticality_levels_in_scope', label: 'Criticality Levels In Scope' },
{ key: 'exclusions', label: 'Exclusions' },
{ key: 'special_conditions', label: 'Special Conditions' },
{ key: 'data_sources_required', label: 'Data Sources Required' },
{ key: 'business_justification', label: 'Business Justification' },
{ key: 'notes', label: 'Notes' },
];
export default function MetricInfoPanel({ metricId, definition, summaryEntries, onClose }) {
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget) {
onClose();
}
};
const title = definition
? definition.metric_title
: (summaryEntries && summaryEntries.length > 0 ? summaryEntries[0].description : metricId);
return (
<div
onClick={handleBackdropClick}
style={{
position: 'fixed',
inset: 0,
zIndex: 60,
background: 'rgba(10, 14, 39, 0.92)',
backdropFilter: 'blur(8px)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-end',
padding: '0',
}}
>
<div style={{
width: '100%',
maxWidth: '480px',
height: '100vh',
overflowY: 'auto',
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
borderLeft: `1px solid ${TEAL}30`,
boxShadow: '0 0 40px rgba(0,0,0,0.7)',
padding: '1.75rem',
position: 'relative',
}}>
{/* Close button */}
<button
onClick={onClose}
style={{
position: 'absolute',
top: '1rem',
right: '1rem',
background: 'none',
border: '1px solid rgba(100,116,139,0.3)',
borderRadius: '0.25rem',
padding: '0.3rem',
cursor: 'pointer',
color: '#64748B',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.color = '#E2E8F0'; e.currentTarget.style.borderColor = 'rgba(100,116,139,0.6)'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#64748B'; e.currentTarget.style.borderColor = 'rgba(100,116,139,0.3)'; }}
>
<X style={{ width: '16px', height: '16px' }} />
</button>
{/* Metric ID */}
<div style={{
fontFamily: 'monospace',
fontSize: '0.72rem',
color: TEAL,
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginBottom: '0.375rem',
}}>
Metric {metricId}
</div>
{/* Title */}
<h3 style={{
fontFamily: 'monospace',
fontSize: '1.05rem',
fontWeight: '700',
color: '#E2E8F0',
margin: '0 0 1.5rem 0',
lineHeight: 1.4,
paddingRight: '2rem',
}}>
{title}
</h3>
{!definition ? (
<div style={{
padding: '1rem',
background: 'rgba(15,23,42,0.6)',
border: '1px solid rgba(100,116,139,0.2)',
borderRadius: '0.375rem',
color: '#94A3B8',
fontSize: '0.8rem',
fontFamily: 'monospace',
}}>
No detailed definition available.
{summaryEntries && summaryEntries.length > 0 && (
<div style={{ marginTop: '0.75rem', color: '#CBD5E1', fontSize: '0.78rem' }}>
{summaryEntries[0].description}
</div>
)}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{SECTION_FIELDS.map(({ key, label }) => (
<div key={key}>
<div style={{
fontFamily: 'monospace',
fontSize: '0.6rem',
fontWeight: '600',
color: '#475569',
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginBottom: '0.3rem',
}}>
{label}
</div>
<div style={{
fontSize: '0.8rem',
color: definition[key] ? '#CBD5E1' : '#475569',
fontFamily: 'monospace',
lineHeight: 1.5,
padding: '0.4rem 0.6rem',
background: 'rgba(15,23,42,0.4)',
borderRadius: '0.25rem',
border: '1px solid rgba(255,255,255,0.04)',
}}>
{definition[key] || '—'}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
// Exported for testing — the list of field keys rendered by the panel
MetricInfoPanel.RENDERED_FIELD_KEYS = SECTION_FIELDS.map(f => f.key);

View File

@@ -0,0 +1,118 @@
import fc from 'fast-check';
import { computeWorstStatus, groupByMetricFamily } from '../CompliancePage';
// ---------------------------------------------------------------------------
// Generators
// ---------------------------------------------------------------------------
const VALID_STATUSES = [
'Below 15% of Target',
'Within 15% of Target',
'Meets/Exceeds Target',
];
const statusArb = fc.constantFrom(...VALID_STATUSES);
const summaryEntryArb = fc.record({
metric_id: fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/),
team: fc.constantFrom('STEAM', 'ACCESS-ENG'),
priority: fc.constantFrom('High', 'Medium', 'Low'),
non_compliant: fc.nat({ max: 500 }),
compliant: fc.nat({ max: 500 }),
total: fc.nat({ max: 1000 }),
compliance_pct: fc.double({ min: 0, max: 1, noNaN: true }),
target: fc.double({ min: 0, max: 1, noNaN: true }),
status: statusArb,
description: fc.string({ minLength: 1, maxLength: 50 }),
category: fc.constantFrom(
'Vulnerability Management',
'Access & MFA',
'Logging & Monitoring',
'End-of-Life OS',
),
});
// ---------------------------------------------------------------------------
// Property 1: Grouping invariant — no entries lost or misplaced
// Validates: Requirements 1.1, 1.2
// ---------------------------------------------------------------------------
describe('Property 1: Grouping invariant — no entries lost or misplaced', () => {
test('every entry appears in exactly one group, groups share metric_id, totals match', () => {
fc.assert(
fc.property(
fc.array(summaryEntryArb, { minLength: 0, maxLength: 30 }),
fc.constantFrom('STEAM', 'ACCESS-ENG'),
(entries, team) => {
const groups = groupByMetricFamily(entries, team);
const teamEntries = entries.filter(
(e) => e.team === team && e.metric_id,
);
// (c) total entries across groups equals team-filtered input count
const totalGrouped = groups.reduce(
(sum, g) => sum + g.entries.length,
0,
);
expect(totalGrouped).toBe(teamEntries.length);
// (b) all entries within a group share the same metric_id
for (const group of groups) {
for (const entry of group.entries) {
expect(entry.metric_id).toBe(group.metricId);
}
}
// (a) every team entry appears in exactly one group
const allGroupedEntries = groups.flatMap((g) => g.entries);
for (const entry of teamEntries) {
const occurrences = allGroupedEntries.filter(
(e) => e === entry,
).length;
expect(occurrences).toBe(1);
}
},
),
{ numRuns: 100 },
);
});
});
// ---------------------------------------------------------------------------
// Property 2: Worst-status computation follows severity ordering
// Validates: Requirements 1.6, 3.1
// ---------------------------------------------------------------------------
const STATUS_SEVERITY = {
'Below 15% of Target': 0,
'Within 15% of Target': 1,
'Meets/Exceeds Target': 2,
};
describe('Property 2: Worst-status computation follows severity ordering', () => {
test('result is the status with the lowest severity rank present', () => {
fc.assert(
fc.property(
fc.array(statusArb, { minLength: 1, maxLength: 20 }),
(statuses) => {
const result = computeWorstStatus(statuses);
// Result must be a valid status
expect(VALID_STATUSES).toContain(result);
// Result must be the minimum severity present
const minSeverity = Math.min(
...statuses.map((s) => STATUS_SEVERITY[s]),
);
expect(STATUS_SEVERITY[result]).toBe(minSeverity);
// If array contains "Below 15% of Target", result must be that
if (statuses.includes('Below 15% of Target')) {
expect(result).toBe('Below 15% of Target');
}
},
),
{ numRuns: 100 },
);
});
});

View File

@@ -0,0 +1,211 @@
import fc from 'fast-check';
import MetricInfoPanel from '../MetricInfoPanel';
// ---------------------------------------------------------------------------
// Generators
// ---------------------------------------------------------------------------
const DEFINITION_KEYS = [
'metric_id',
'metric_title',
'asset_types',
'asset_types_in_scope',
'application_types_in_scope',
'environment_in_scope',
'status_in_scope',
'instance_types_in_scope',
'criticality_levels_in_scope',
'exclusions',
'special_conditions',
'data_sources_required',
'business_justification',
'notes',
];
const metricDefinitionArb = fc.record({
metric_id: fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/),
metric_title: fc.string({ minLength: 1, maxLength: 80 }),
asset_types: fc.string({ minLength: 0, maxLength: 60 }),
asset_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
application_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
environment_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
status_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
instance_types_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
criticality_levels_in_scope: fc.string({ minLength: 0, maxLength: 60 }),
exclusions: fc.string({ minLength: 0, maxLength: 60 }),
special_conditions: fc.string({ minLength: 0, maxLength: 60 }),
data_sources_required: fc.string({ minLength: 0, maxLength: 60 }),
business_justification: fc.string({ minLength: 0, maxLength: 60 }),
notes: fc.string({ minLength: 0, maxLength: 60 }),
});
/**
* Generate an array of metric definitions with unique metric_id values.
*/
const uniqueDefinitionsArb = fc
.array(metricDefinitionArb, { minLength: 1, maxLength: 20 })
.map((defs) => {
const seen = new Set();
return defs.filter((d) => {
if (seen.has(d.metric_id)) return false;
seen.add(d.metric_id);
return true;
});
})
.filter((arr) => arr.length > 0);
// ---------------------------------------------------------------------------
// Property 4: Definition lookup returns correct entry or null
// Validates: Requirements 4.2, 4.6
// ---------------------------------------------------------------------------
describe('Property 4: Definition lookup returns correct entry or null', () => {
test('lookup hits for IDs in the array and misses for IDs not in the array', () => {
fc.assert(
fc.property(
uniqueDefinitionsArb,
fc.stringMatching(/^[1-9]\d{0,2}\.\d{1,2}\.\d{1,2}$/),
(definitions, queryId) => {
// Build lookup map
const lookup = {};
for (const def of definitions) {
lookup[def.metric_id] = def;
}
// Query with IDs from the array — expect hit
for (const def of definitions) {
expect(lookup[def.metric_id]).toBe(def);
}
// Query with a random ID — expect hit if present, miss if not
const existsInArray = definitions.some(
(d) => d.metric_id === queryId,
);
if (existsInArray) {
expect(lookup[queryId]).toBeDefined();
} else {
expect(lookup[queryId]).toBeUndefined();
}
},
),
{ numRuns: 100 },
);
});
});
// ---------------------------------------------------------------------------
// Property 5: Detail panel renders all required definition fields
// Validates: Requirements 5.3
// ---------------------------------------------------------------------------
describe('Property 5: Detail panel renders all required definition fields', () => {
test('RENDERED_FIELD_KEYS includes all required definition keys (excluding metric_id and metric_title)', () => {
const renderedKeys = MetricInfoPanel.RENDERED_FIELD_KEYS;
// Keys that are rendered separately (as title/header), not in the section list
const separatelyRendered = ['metric_id', 'metric_title'];
const requiredSectionKeys = DEFINITION_KEYS.filter(
(k) => !separatelyRendered.includes(k),
);
fc.assert(
fc.property(metricDefinitionArb, (definition) => {
// Verify every required section key is in the rendered set
for (const key of requiredSectionKeys) {
expect(renderedKeys).toContain(key);
}
// Verify the definition object has all keys that will be rendered
for (const key of renderedKeys) {
expect(definition).toHaveProperty(key);
}
}),
{ numRuns: 100 },
);
});
});
// ---------------------------------------------------------------------------
// Property 6: Definitions schema validation — all entries have required fields
// Validates: Requirements 6.2, 8.3, 8.4
// ---------------------------------------------------------------------------
describe('Property 6: Definitions schema validation — all entries have required fields', () => {
test('every entry has all 14 keys, metric_id is non-empty string, optional fields are strings', () => {
fc.assert(
fc.property(
fc.array(metricDefinitionArb, { minLength: 1, maxLength: 15 }),
(definitions) => {
for (const def of definitions) {
// All 14 keys present
for (const key of DEFINITION_KEYS) {
expect(def).toHaveProperty(key);
}
// metric_id is a non-empty string
expect(typeof def.metric_id).toBe('string');
expect(def.metric_id.length).toBeGreaterThan(0);
// Optional fields are strings (not null/undefined)
const optionalFields = [
'exclusions',
'special_conditions',
'notes',
];
for (const field of optionalFields) {
expect(typeof def[field]).toBe('string');
expect(def[field]).not.toBeNull();
expect(def[field]).not.toBeUndefined();
}
}
},
),
{ numRuns: 100 },
);
});
});
// ---------------------------------------------------------------------------
// Property 7: Lookup map construction preserves all definitions
// Validates: Requirements 6.4
// ---------------------------------------------------------------------------
describe('Property 7: Lookup map construction preserves all definitions', () => {
test('map size equals array length and every definition is retrievable', () => {
fc.assert(
fc.property(uniqueDefinitionsArb, (definitions) => {
// Build lookup map
const lookup = {};
for (const def of definitions) {
lookup[def.metric_id] = def;
}
// Map size equals array length
expect(Object.keys(lookup).length).toBe(definitions.length);
// Every definition is retrievable by its metric_id
for (const def of definitions) {
expect(lookup[def.metric_id]).toBe(def);
}
}),
{ numRuns: 100 },
);
});
});
// ---------------------------------------------------------------------------
// Property 8: JSON round-trip preserves metric definition data
// Validates: Requirements 8.1, 8.2
// ---------------------------------------------------------------------------
describe('Property 8: JSON round-trip preserves metric definition data', () => {
test('JSON.parse(JSON.stringify(definition)) produces a deeply equal object', () => {
fc.assert(
fc.property(metricDefinitionArb, (definition) => {
const roundTripped = JSON.parse(JSON.stringify(definition));
expect(roundTripped).toEqual(definition);
}),
{ numRuns: 100 },
);
});
});

File diff suppressed because it is too large Load Diff