517 lines
24 KiB
Markdown
517 lines
24 KiB
Markdown
# Design Document: Compliance Metric Grouping
|
||
|
||
## Overview
|
||
|
||
The Compliance Metric Grouping feature consolidates the AEO Compliance page's metric health cards from one-per-summary-entry to one-per-metric-family. A metric family is the set of summary entries that share the same base `metric_id` (e.g., `5.2.5`). The same base ID can appear multiple times in the summary data because the backend parser produces one entry per team/variant row in the xlsx Summary sheet.
|
||
|
||
The feature adds three new capabilities on top of the grouping:
|
||
|
||
1. **Variant pills** inside each grouped card showing per-variant compliance percentages and status indicators
|
||
2. **Hover tooltip** (300ms delay) displaying metric title, business justification, and data sources from a static definitions file
|
||
3. **Info panel** opened via an info icon on each card, showing the full metric definition (scope, filters, exclusions, notes)
|
||
|
||
A static JSON file (`metricDefinitions.json`) ships with the frontend containing structured metric definition data for all tracked metrics. No new backend endpoints are needed.
|
||
|
||
### Design Decisions
|
||
|
||
- **Grouping is frontend-only.** The backend summary endpoint returns flat entries. The frontend groups them by `metric_id` at render time. This avoids backend changes and keeps the grouping logic testable as a pure function.
|
||
- **`metricFilter` changes from a single string to an array.** Currently `metricFilter` is a single `metric_id` string or `null`. With grouping, clicking a card sets the filter to the array of all `metric_id` values in that family. For single-entry families this is a one-element array. The device table filter checks `metricFilter.includes(m.metric_id)` instead of `m.metric_id === metricFilter`.
|
||
- **Worst-status drives card color.** Each grouped card computes the most severe status across its variants using a defined severity ordering. This gives engineers an at-a-glance signal when any variant is failing.
|
||
- **Definitions file is static, not an API.** The metric definitions table has ~130 rows that change infrequently. A static JSON import avoids API round-trips and keeps the tooltip/panel responsive. The file can be regenerated from the source xlsx definitions table when metrics change.
|
||
- **MetricInfoPanel is a new component.** The detail panel is complex enough (12+ fields, dark-themed sections) to warrant its own component file rather than inlining it in CompliancePage.js. The hover tooltip, being lightweight, stays inline.
|
||
- **Info icon click uses `stopPropagation`.** The info icon sits inside the card button. Clicking it opens the detail panel without triggering the card's metric filter toggle.
|
||
|
||
## Architecture
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[CompliancePage.js] -->|imports| B[metricDefinitions.json]
|
||
A -->|renders| C[MetricHealthCard - grouped]
|
||
A -->|renders| D[MetricInfoPanel]
|
||
A -->|renders| E[HoverTooltip - inline]
|
||
A -->|fetches| F[GET /api/compliance/summary?team=X]
|
||
A -->|fetches| G[GET /api/compliance/items?team=X&status=Y]
|
||
|
||
F -->|returns| H[summary.entries array]
|
||
H -->|groupByMetricFamily| I[Map of metricId → entries array]
|
||
I -->|one card per group| C
|
||
|
||
C -->|click outside info icon| J[setMetricFilter - array of IDs]
|
||
C -->|click info icon| K[setInfoMetric - opens MetricInfoPanel]
|
||
C -->|hover 300ms| E
|
||
|
||
J -->|filters| G2[filteredDevices]
|
||
B -->|lookup by metric_id| E
|
||
B -->|lookup by metric_id| D
|
||
```
|
||
|
||
### Component Hierarchy
|
||
|
||
```
|
||
CompliancePage
|
||
├── PageHeader (unchanged)
|
||
├── TeamTabs (unchanged)
|
||
├── MetricHealthSection
|
||
│ ├── SectionHeader ("Metric Health — click to filter" + clear button)
|
||
│ └── MetricHealthCard (one per metric family)
|
||
│ ├── CardTitle (base metric_id + category)
|
||
│ ├── VariantPill[] (one per summary entry in family)
|
||
│ ├── WorstStatusPill (computed from all variants)
|
||
│ ├── TargetDisplay (shared target %)
|
||
│ ├── InfoIcon (lucide-react Info, top-right)
|
||
│ └── HoverTooltip (inline, 300ms delay)
|
||
├── MetricInfoPanel (slide-out/overlay, opened by info icon click)
|
||
├── ComplianceChartsPanel (unchanged)
|
||
├── DeviceTable (unchanged, filter logic updated)
|
||
├── ComplianceDetailPanel (unchanged)
|
||
├── ComplianceUploadModal (unchanged)
|
||
└── RollbackModal (unchanged)
|
||
```
|
||
|
||
### Data Flow
|
||
|
||
1. `CompliancePage` fetches summary entries from `/api/compliance/summary?team=X` on mount and team change.
|
||
2. The `groupByMetricFamily(entries)` helper groups the flat entries array into a `Map<string, SummaryEntry[]>` keyed by base `metric_id`.
|
||
3. One `MetricHealthCard` renders per map entry. Each card receives the full array of entries for that family.
|
||
4. The card computes `worstStatus` from the entries' status fields and uses it for border/pill coloring.
|
||
5. On card click (outside info icon), `metricFilter` is set to the array of `metric_id` values in that family (or cleared if already active).
|
||
6. The device table filter changes from `d.failing_metrics.some(m => m.metric_id === metricFilter)` to `d.failing_metrics.some(m => metricFilter.includes(m.metric_id))`.
|
||
7. On hover (300ms), a tooltip renders using data from the `metricDefinitions.json` lookup map, falling back to the summary entry description.
|
||
8. On info icon click, `infoMetric` state is set, opening `MetricInfoPanel` with the full definition.
|
||
|
||
## Components and Interfaces
|
||
|
||
### groupByMetricFamily (pure helper function)
|
||
|
||
```javascript
|
||
// In CompliancePage.js — replaces the current teamMetrics() helper
|
||
// Input: entries (array of SummaryEntry from the summary endpoint), team (string)
|
||
// Output: array of { metricId, entries, category, target, worstStatus }
|
||
|
||
function groupByMetricFamily(allEntries, team) {
|
||
const teamEntries = allEntries.filter(e => e.team === team);
|
||
const familyMap = {};
|
||
|
||
for (const entry of teamEntries) {
|
||
const baseId = entry.metric_id; // already the base ID from the parser
|
||
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)),
|
||
}));
|
||
}
|
||
```
|
||
|
||
### computeWorstStatus (pure helper function)
|
||
|
||
```javascript
|
||
// Input: array of status strings
|
||
// Output: the most severe status string
|
||
// Severity order: "Below 15% of Target" > "Within 15% of Target" > "Meets/Exceeds Target"
|
||
|
||
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;
|
||
}
|
||
```
|
||
|
||
### MetricHealthCard (redesigned)
|
||
|
||
```javascript
|
||
// Props:
|
||
// family: { metricId, entries, category, target, worstStatus }
|
||
// active: boolean (is this family's filter currently active)
|
||
// onClick: () => void (toggle metric filter)
|
||
// onInfoClick: (metricId) => void (open detail panel)
|
||
// definitionLookup: Map<string, MetricDefinition> (for tooltip)
|
||
//
|
||
// Renders:
|
||
// - Base metric ID as title
|
||
// - Category label
|
||
// - One VariantPill per entry in family.entries
|
||
// - Shared target percentage
|
||
// - Worst-status pill with border color
|
||
// - Info icon (top-right, stopPropagation on click)
|
||
// - HoverTooltip (300ms delay, positioned near card)
|
||
```
|
||
|
||
### VariantPill (new inline sub-component)
|
||
|
||
```javascript
|
||
// Props:
|
||
// entry: SummaryEntry (single variant)
|
||
//
|
||
// Renders:
|
||
// - Label: entry.team or entry.description (distinguishing text)
|
||
// - Compliance percentage in monospace
|
||
// - Background tint from entry's status color at ~12% opacity
|
||
// - Glow dot if status !== "Meets/Exceeds Target"
|
||
//
|
||
// Layout: inline-flex, wraps via parent flexWrap
|
||
```
|
||
|
||
### HoverTooltip (inline in CompliancePage.js)
|
||
|
||
```javascript
|
||
// State managed in CompliancePage:
|
||
// hoveredMetric: string | null
|
||
// hoverTimeout: ref (setTimeout ID)
|
||
// tooltipPosition: { top, left } (computed from card bounding rect)
|
||
//
|
||
// On mouseEnter on MetricHealthCard:
|
||
// Set timeout for 300ms → set hoveredMetric to family.metricId
|
||
// On mouseLeave:
|
||
// Clear timeout, set hoveredMetric to null
|
||
//
|
||
// Renders (when hoveredMetric matches):
|
||
// - Fixed-position div near the card
|
||
// - Metric title (from definitionLookup or entry.description)
|
||
// - Business justification (from definitionLookup)
|
||
// - Data sources required (from definitionLookup)
|
||
// - Dark card background, subtle border, shadow per DESIGN_SYSTEM.md
|
||
// - Falls back to summary entry description if no definition found
|
||
```
|
||
|
||
### MetricInfoPanel (new component file)
|
||
|
||
```javascript
|
||
// frontend/src/components/pages/MetricInfoPanel.js
|
||
// Props:
|
||
// metricId: string (base metric ID)
|
||
// definition: MetricDefinition | null (from lookup)
|
||
// summaryEntries: SummaryEntry[] (the family's entries, for fallback)
|
||
// onClose: () => void
|
||
//
|
||
// Renders:
|
||
// - Overlay/slide-out panel with dark theme
|
||
// - Close button (X icon, top-right)
|
||
// - Metric title (h3, monospace)
|
||
// - Sections with monospace uppercase labels:
|
||
// - 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
|
||
// - If definition is null: "No detailed definition available" + summary description fallback
|
||
// - Click outside or close button → onClose()
|
||
```
|
||
|
||
### Integration with CompliancePage.js
|
||
|
||
```javascript
|
||
// New imports:
|
||
import { Info } from 'lucide-react';
|
||
import MetricInfoPanel from './MetricInfoPanel';
|
||
import metricDefinitionsRaw from '../../data/metricDefinitions.json';
|
||
|
||
// Build lookup map once at module level:
|
||
const METRIC_DEFINITIONS = {};
|
||
for (const def of metricDefinitionsRaw) {
|
||
METRIC_DEFINITIONS[def.metric_id] = def;
|
||
}
|
||
|
||
// State changes in CompliancePage:
|
||
// - metricFilter: null → null | string[] (array of metric IDs)
|
||
// - New state: infoMetric (string | null) — which metric's info panel is open
|
||
// - New state: hoveredMetric (string | null) — which metric is being hovered
|
||
// - New ref: hoverTimeoutRef — for 300ms delay
|
||
|
||
// Filter logic change:
|
||
// Old: .filter(d => !metricFilter || d.failing_metrics.some(m => m.metric_id === metricFilter))
|
||
// New: .filter(d => !metricFilter || d.failing_metrics.some(m => metricFilter.includes(m.metric_id)))
|
||
|
||
// Card rendering change:
|
||
// Old: metrics.map(entry => <MetricHealthCard entry={entry} ... />)
|
||
// New: families.map(family => <MetricHealthCard family={family} ... />)
|
||
```
|
||
|
||
## Data Models
|
||
|
||
### SummaryEntry (from backend, unchanged)
|
||
|
||
```javascript
|
||
{
|
||
metric_id: string, // e.g. "5.2.5" — base ID, no suffix
|
||
team: string, // e.g. "STEAM", "ACCESS-ENG"
|
||
priority: string,
|
||
non_compliant: number,
|
||
compliant: number,
|
||
total: number,
|
||
compliance_pct: number, // 0.0–1.0
|
||
target: number, // 0.0–1.0
|
||
status: string, // "Meets/Exceeds Target" | "Within 15% of Target" | "Below 15% of Target"
|
||
description: string,
|
||
category: string // from compliance_config.json metric_categories
|
||
}
|
||
```
|
||
|
||
### MetricFamily (computed client-side)
|
||
|
||
```javascript
|
||
{
|
||
metricId: string, // base metric ID (e.g. "5.2.5")
|
||
entries: SummaryEntry[], // all summary entries for this base ID
|
||
category: string, // from first entry
|
||
target: number, // from first entry (shared across variants)
|
||
worstStatus: string // computed worst status across all entries
|
||
}
|
||
```
|
||
|
||
### MetricDefinition (from metricDefinitions.json)
|
||
|
||
```javascript
|
||
{
|
||
metric_id: string, // e.g. "5.2.5"
|
||
metric_title: string, // e.g. "MFA for Privileged Access"
|
||
asset_types: string, // e.g. "Servers, Network Devices"
|
||
asset_types_in_scope: string,
|
||
application_types_in_scope: string,
|
||
environment_in_scope: string,
|
||
status_in_scope: string,
|
||
instance_types_in_scope: string,
|
||
criticality_levels_in_scope: string,
|
||
exclusions: string, // empty string if none
|
||
special_conditions: string, // empty string if none
|
||
data_sources_required: string,
|
||
business_justification: string,
|
||
notes: string // empty string if none
|
||
}
|
||
```
|
||
|
||
### metricDefinitions.json (file structure)
|
||
|
||
```json
|
||
[
|
||
{
|
||
"metric_id": "1.1.1",
|
||
"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": ""
|
||
}
|
||
]
|
||
```
|
||
|
||
All entries use the same set of keys. Optional fields with no value use empty strings, never `null` or omitted keys.
|
||
|
||
### Status Severity Map
|
||
|
||
```javascript
|
||
const STATUS_SEVERITY = {
|
||
'Below 15% of Target': 0, // worst
|
||
'Within 15% of Target': 1,
|
||
'Meets/Exceeds Target': 2, // best
|
||
};
|
||
```
|
||
|
||
### Updated metricFilter State
|
||
|
||
```javascript
|
||
// Old: metricFilter: string | null
|
||
// New: metricFilter: string[] | null
|
||
//
|
||
// null = no filter (show all devices)
|
||
// string[] = show devices with failing_metrics matching any ID in the array
|
||
```
|
||
|
||
## Correctness Properties
|
||
|
||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||
|
||
### Property 1: Grouping invariant — no entries lost or misplaced
|
||
|
||
*For any* array of summary entries and any team string, grouping by metric family SHALL produce groups where (a) every entry appears in exactly one group, (b) all entries within a group share the same `metric_id`, and (c) the total number of entries across all groups equals the number of entries for that team in the input.
|
||
|
||
**Validates: Requirements 1.1, 1.2**
|
||
|
||
### Property 2: Worst-status computation follows severity ordering
|
||
|
||
*For any* non-empty array of status strings drawn from `{"Below 15% of Target", "Within 15% of Target", "Meets/Exceeds Target"}`, the computed worst status SHALL be the status with the lowest severity rank present in the array. If the array contains "Below 15% of Target", the result SHALL be "Below 15% of Target" regardless of other values.
|
||
|
||
**Validates: Requirements 1.6, 3.1**
|
||
|
||
### Property 3: Device filtering with metric family includes all matching devices
|
||
|
||
*For any* array of device objects (each with a `failing_metrics` array of `{metric_id}` objects) and *for any* non-empty array of filter metric IDs, the filtered result SHALL contain exactly those devices that have at least one `failing_metrics` entry whose `metric_id` is included in the filter array. No matching device is excluded and no non-matching device is included.
|
||
|
||
**Validates: Requirements 1.8, 7.1, 7.2**
|
||
|
||
### Property 4: Definition lookup returns correct entry or null
|
||
|
||
*For any* array of metric definition objects with unique `metric_id` values, building a lookup map and querying it with a `metric_id` that exists in the array SHALL return the corresponding definition object. Querying with a `metric_id` not in the array SHALL return `undefined`.
|
||
|
||
**Validates: Requirements 4.2, 4.6**
|
||
|
||
### Property 5: Detail panel renders all required definition fields
|
||
|
||
*For any* valid metric definition object (with all 14 fields present), the set of field keys rendered by the detail panel SHALL include: `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`, and `notes`.
|
||
|
||
**Validates: Requirements 5.3**
|
||
|
||
### Property 6: Definitions schema validation — all entries have required fields
|
||
|
||
*For any* entry in the metric definitions array, the entry SHALL have all 14 required keys present, and the `metric_id` field SHALL be a non-empty string. Optional fields (`exclusions`, `special_conditions`, `notes`) SHALL be present as strings (empty string if no value), never omitted or null.
|
||
|
||
**Validates: Requirements 6.2, 8.3, 8.4**
|
||
|
||
### Property 7: Lookup map construction preserves all definitions
|
||
|
||
*For any* array of metric definition objects with unique `metric_id` values, building a lookup map keyed by `metric_id` SHALL produce a map with exactly as many entries as the input array, and every input definition SHALL be retrievable by its `metric_id`.
|
||
|
||
**Validates: Requirements 6.4**
|
||
|
||
### Property 8: JSON round-trip preserves metric definition data
|
||
|
||
*For any* valid metric definition object, `JSON.parse(JSON.stringify(definition))` SHALL produce an object deeply equal to the original.
|
||
|
||
**Validates: Requirements 8.1, 8.2**
|
||
|
||
## Error Handling
|
||
|
||
### Metric Definitions File
|
||
|
||
| Error Scenario | Handling |
|
||
|---|---|
|
||
| `metricDefinitions.json` fails to import (malformed JSON) | Build-time error caught by Create React App. The file is validated at development time. |
|
||
| Metric ID not found in definitions lookup | Tooltip falls back to `entry.description` from summary data. Info panel shows "No detailed definition available" with summary description. |
|
||
| Definition entry has empty optional fields | Rendered sections show "—" placeholder for empty strings. No error thrown. |
|
||
|
||
### Grouping Logic
|
||
|
||
| Error Scenario | Handling |
|
||
|---|---|
|
||
| Summary entries array is empty | `groupByMetricFamily` returns empty array. No cards rendered. Existing "No compliance data" empty state shown. |
|
||
| Summary entry has missing or empty `metric_id` | Entry is skipped during grouping (filtered out). |
|
||
| All entries for a team have the same `metric_id` | Single family group with multiple variant pills. Works correctly. |
|
||
|
||
### Hover Tooltip
|
||
|
||
| Error Scenario | Handling |
|
||
|---|---|
|
||
| User moves mouse away before 300ms | Timeout cleared, tooltip never shown. No side effects. |
|
||
| Tooltip would render off-screen | Position clamped to viewport bounds using `getBoundingClientRect()`. |
|
||
| Rapid hover/unhover across multiple cards | Previous timeout cleared on each `mouseLeave`. Only the currently hovered card's tooltip can appear. |
|
||
|
||
### Info Panel
|
||
|
||
| Error Scenario | Handling |
|
||
|---|---|
|
||
| Info icon click while tooltip is visible | Tooltip dismissed (mouseLeave fires). Panel opens. No conflict. |
|
||
| Multiple rapid info icon clicks | `infoMetric` state is set to the latest clicked metric. Only one panel open at a time. |
|
||
| Click outside panel while scrolled | Overlay backdrop captures click, closes panel. Scroll position preserved. |
|
||
|
||
### Device Table Filtering
|
||
|
||
| Error Scenario | Handling |
|
||
|---|---|
|
||
| `metricFilter` is set to an array but no devices match | Empty state message shown: "No non-compliant devices". |
|
||
| Device has `failing_metrics` with IDs not in any family | Device only shown when no filter is active or when its metric IDs match the active filter. |
|
||
|
||
## Testing Strategy
|
||
|
||
### Unit Tests (Example-Based)
|
||
|
||
Unit tests cover specific rendering, interaction, and integration scenarios:
|
||
|
||
**Grouping and display:**
|
||
- Groups entries with the same `metric_id` into one card
|
||
- Single-entry families render one variant pill
|
||
- Multi-entry families render one pill per entry
|
||
- Card title shows base metric ID and category from first entry
|
||
- Card shows shared target percentage
|
||
|
||
**Worst-status computation:**
|
||
- All "Meets/Exceeds Target" → card shows "OK" with success color
|
||
- Mix of statuses → card uses the worst status color
|
||
- Single "Below 15% of Target" among passing variants → card shows danger color
|
||
|
||
**Variant pills:**
|
||
- Each pill shows the entry's team label and compliance percentage
|
||
- Pill background tint matches the entry's individual status color
|
||
- Non-passing variants show a glow dot
|
||
|
||
**Hover tooltip:**
|
||
- Tooltip appears after 300ms hover delay
|
||
- Tooltip shows metric title, business justification, data sources from definitions
|
||
- Tooltip disappears on mouse leave
|
||
- Tooltip falls back to summary description when no definition exists
|
||
- Tooltip does not interfere with card click
|
||
|
||
**Info panel:**
|
||
- Info icon click opens MetricInfoPanel with correct metric definition
|
||
- Info icon click does not trigger card's metric filter toggle (stopPropagation)
|
||
- Panel displays all 12+ definition fields with section labels
|
||
- Panel shows fallback message when no definition exists
|
||
- Panel closes on outside click or close button
|
||
|
||
**Device table filtering:**
|
||
- Clicking a grouped card sets filter to all metric IDs in that family
|
||
- Filtered device table shows devices matching any ID in the family
|
||
- Clicking the same card again clears the filter
|
||
- Clear filter button resets to show all devices
|
||
- Active card shows highlighted styling
|
||
|
||
**Definitions file:**
|
||
- File imports without error
|
||
- Lookup map contains all metric IDs from the file
|
||
- All entries have the required 14 fields
|
||
|
||
### Property-Based Tests
|
||
|
||
Property-based tests use [fast-check](https://github.com/dubzzz/fast-check) to verify universal properties across generated inputs. Each test runs a minimum of 100 iterations.
|
||
|
||
| Property | Test Description | Tag |
|
||
|---|---|---|
|
||
| Property 1 | Generate random arrays of summary entries with varying metric_id values and team strings. Group them and verify: all entries accounted for, entries within each group share the same metric_id, group count equals unique metric_id count. | Feature: compliance-metric-grouping, Property 1: Grouping invariant — no entries lost or misplaced |
|
||
| Property 2 | Generate random non-empty arrays of status strings from the valid set. Compute worst status and verify it matches the minimum severity rank in the array. | Feature: compliance-metric-grouping, Property 2: Worst-status computation follows severity ordering |
|
||
| Property 3 | Generate random device arrays (each with random failing_metrics) and random filter ID arrays. Filter and verify the result contains exactly the devices with at least one matching metric. | Feature: compliance-metric-grouping, Property 3: Device filtering with metric family includes all matching devices |
|
||
| Property 4 | Generate random arrays of metric definitions with unique IDs. Build lookup map, query with IDs from the array (expect hit) and IDs not in the array (expect miss). | Feature: compliance-metric-grouping, Property 4: Definition lookup returns correct entry or null |
|
||
| Property 5 | Generate random metric definition objects with all 14 fields. Extract the rendered field keys and verify all required keys are present. | Feature: compliance-metric-grouping, Property 5: Detail panel renders all required definition fields |
|
||
| Property 6 | Generate random arrays of metric definition objects. Verify every entry has all 14 keys present, metric_id is a non-empty string, and optional fields are strings (not null/undefined). | Feature: compliance-metric-grouping, Property 6: Definitions schema validation — all entries have required fields |
|
||
| Property 7 | Generate random definition arrays with unique IDs. Build lookup map and verify map size equals array length, and every definition is retrievable by its metric_id. | Feature: compliance-metric-grouping, Property 7: Lookup map construction preserves all definitions |
|
||
| Property 8 | Generate random metric definition objects with string values. Round-trip through JSON.stringify then JSON.parse and verify deep equality. | Feature: compliance-metric-grouping, Property 8: JSON round-trip preserves metric definition data |
|
||
|
||
### Test Configuration
|
||
|
||
- **Library:** fast-check (JavaScript property-based testing)
|
||
- **Runner:** Jest (via react-scripts test)
|
||
- **Iterations:** Minimum 100 per property test (`fc.assert(property, { numRuns: 100 })`)
|
||
- **Tag format:** Comment at top of each property test referencing the design property
|