Testing & Validation Page
Test coverage heatmap, test results table with filtering, requirements-test traceability matrix, and FMEA risk priority analysis for the Drone Flight Controller.
Table of contents
src/pages/testing/index.tsxsrc/pages/testing/components/coverage-heatmap.tsxsrc/pages/testing/components/test-results-table.tsxsrc/pages/testing/components/requirement-test-matrix.tsxsrc/pages/testing/components/fmea-risk-table.tsx
src/pages/testing/index.tsx
Main testing page with metric cards, coverage heatmap, test results table, and tabbed bottom section for requirements matrix and FMEA table.
import { useState, useMemo } from "react";
import {
FlaskConical,
CheckCircle2,
XCircle,
Percent,
AlertTriangle,
} from "lucide-react";
import { useTestCoverage } from "@/hooks/use-testing";
import { PageHeader } from "@/components/layout/page-header";
import { MetricCard } from "@/components/shared/metric-card";
import { Badge } from "@/components/ui/badge";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { CoverageHeatmap } from "./components/coverage-heatmap";
import { TestResultsTable } from "./components/test-results-table";
import { RequirementTestMatrix } from "./components/requirement-test-matrix";
import { FMEARiskTable } from "./components/fmea-risk-table";
// ---------------------------------------------------------------------------
// Mock test data types (from the GET /api/v1/testing/coverage handler)
// ---------------------------------------------------------------------------
interface TestCase {
id: string;
name: string;
category: string;
requirementId: string;
status: "passed" | "failed" | "pending" | "skipped";
lastRun?: string;
duration?: number;
results?: TestResult[];
}
interface TestResult {
runId: string;
timestamp: string;
status: "passed" | "failed" | "skipped";
duration: number;
notes?: string;
}
// ---------------------------------------------------------------------------
// Mock test cases for Drone Flight Controller (DFC-v2.1)
// ---------------------------------------------------------------------------
const MOCK_TEST_CASES: TestCase[] = [
// Power
{ id: "TC-001", name: "Battery voltage regulation", category: "Power", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-10T14:30:00Z", duration: 2340, results: [{ runId: "R-001", timestamp: "2025-12-10T14:30:00Z", status: "passed", duration: 2340 }, { runId: "R-002", timestamp: "2025-12-08T09:15:00Z", status: "failed", duration: 2100, notes: "Ripple exceeded 50mV threshold" }] },
{ id: "TC-002", name: "Overcurrent protection trigger", category: "Power", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-09T11:00:00Z", duration: 1580 },
{ id: "TC-003", name: "Power sequencing verification", category: "Power", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-10T16:00:00Z", duration: 3200 },
{ id: "TC-004", name: "Low battery failsafe", category: "Power", requirementId: "REQ-002", status: "failed", lastRun: "2025-12-10T10:00:00Z", duration: 4500, results: [{ runId: "R-003", timestamp: "2025-12-10T10:00:00Z", status: "failed", duration: 4500, notes: "Motor cutoff delayed by 200ms" }] },
{ id: "TC-005", name: "Thermal shutdown test", category: "Power", requirementId: "REQ-002", status: "pending" },
// Communication
{ id: "TC-006", name: "WiFi range test (open field)", category: "Communication", requirementId: "REQ-003", status: "passed", lastRun: "2025-12-09T15:00:00Z", duration: 18000 },
{ id: "TC-007", name: "LoRa link budget verification", category: "Communication", requirementId: "REQ-003", status: "passed", lastRun: "2025-12-08T12:00:00Z", duration: 24000 },
{ id: "TC-008", name: "MAVLink packet integrity", category: "Communication", requirementId: "REQ-003", status: "passed", lastRun: "2025-12-10T09:00:00Z", duration: 8600 },
{ id: "TC-009", name: "Telemetry latency measurement", category: "Communication", requirementId: "REQ-004", status: "failed", lastRun: "2025-12-10T11:30:00Z", duration: 12000, results: [{ runId: "R-004", timestamp: "2025-12-10T11:30:00Z", status: "failed", duration: 12000, notes: "P99 latency 85ms, threshold 50ms" }] },
{ id: "TC-010", name: "Failover WiFi to LoRa", category: "Communication", requirementId: "REQ-004", status: "pending" },
// Sensors
{ id: "TC-011", name: "IMU calibration accuracy", category: "Sensors", requirementId: "REQ-005", status: "passed", lastRun: "2025-12-10T08:00:00Z", duration: 5400 },
{ id: "TC-012", name: "GPS cold start TTFF", category: "Sensors", requirementId: "REQ-005", status: "passed", lastRun: "2025-12-09T16:00:00Z", duration: 45000 },
{ id: "TC-013", name: "Barometer altitude accuracy", category: "Sensors", requirementId: "REQ-005", status: "passed", lastRun: "2025-12-10T13:00:00Z", duration: 7200 },
{ id: "TC-014", name: "Magnetometer calibration", category: "Sensors", requirementId: "REQ-006", status: "failed", lastRun: "2025-12-10T15:00:00Z", duration: 3600, results: [{ runId: "R-005", timestamp: "2025-12-10T15:00:00Z", status: "failed", duration: 3600, notes: "Hard iron offset exceeded 200uT" }] },
{ id: "TC-015", name: "Sensor fusion convergence", category: "Sensors", requirementId: "REQ-006", status: "pending" },
// Motor Control
{ id: "TC-016", name: "ESC throttle linearity", category: "Motor Control", requirementId: "REQ-007", status: "passed", lastRun: "2025-12-09T14:00:00Z", duration: 9600 },
{ id: "TC-017", name: "Motor RPM feedback accuracy", category: "Motor Control", requirementId: "REQ-007", status: "passed", lastRun: "2025-12-09T16:30:00Z", duration: 6000 },
{ id: "TC-018", name: "PID controller step response", category: "Motor Control", requirementId: "REQ-007", status: "passed", lastRun: "2025-12-10T10:30:00Z", duration: 14400 },
{ id: "TC-019", name: "Motor desync recovery", category: "Motor Control", requirementId: "REQ-008", status: "skipped" },
{ id: "TC-020", name: "Emergency motor stop", category: "Motor Control", requirementId: "REQ-008", status: "passed", lastRun: "2025-12-10T12:00:00Z", duration: 1200 },
// Firmware
{ id: "TC-021", name: "Bootloader integrity check", category: "Firmware", requirementId: "REQ-002", status: "passed", lastRun: "2025-12-08T10:00:00Z", duration: 600 },
{ id: "TC-022", name: "OTA update verification", category: "Firmware", requirementId: "REQ-002", status: "passed", lastRun: "2025-12-09T09:00:00Z", duration: 28800 },
{ id: "TC-023", name: "Watchdog recovery test", category: "Firmware", requirementId: "REQ-002", status: "passed", lastRun: "2025-12-10T07:00:00Z", duration: 4800 },
{ id: "TC-024", name: "Flash wear leveling", category: "Firmware", requirementId: "REQ-006", status: "pending" },
{ id: "TC-025", name: "RTOS task priority inversion", category: "Firmware", requirementId: "REQ-006", status: "pending" },
// Environmental
{ id: "TC-026", name: "Operating temp range (-10 to 55C)", category: "Environmental", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-07T08:00:00Z", duration: 86400 },
{ id: "TC-027", name: "Vibration endurance (MIL-STD-810G)", category: "Environmental", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-06T08:00:00Z", duration: 172800 },
{ id: "TC-028", name: "IP54 ingress protection", category: "Environmental", requirementId: "REQ-001", status: "failed", lastRun: "2025-12-10T09:00:00Z", duration: 43200, results: [{ runId: "R-006", timestamp: "2025-12-10T09:00:00Z", status: "failed", duration: 43200, notes: "Water ingress at motor mount seal" }] },
{ id: "TC-029", name: "EMC pre-compliance scan", category: "Environmental", requirementId: "REQ-008", status: "pending" },
{ id: "TC-030", name: "Salt spray corrosion (48h)", category: "Environmental", requirementId: "REQ-008", status: "pending" },
// Additional tests to reach 45 total
{ id: "TC-031", name: "Battery charge cycle lifespan", category: "Power", requirementId: "REQ-002", status: "passed", lastRun: "2025-12-05T08:00:00Z", duration: 259200 },
{ id: "TC-032", name: "USB-C PD negotiation", category: "Power", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-10T14:00:00Z", duration: 1800 },
{ id: "TC-033", name: "GPS multipath rejection", category: "Sensors", requirementId: "REQ-005", status: "pending" },
{ id: "TC-034", name: "LoRa adaptive data rate", category: "Communication", requirementId: "REQ-004", status: "skipped" },
{ id: "TC-035", name: "Motor vibration spectrum analysis", category: "Motor Control", requirementId: "REQ-007", status: "pending" },
{ id: "TC-036", name: "Firmware rollback safety", category: "Firmware", requirementId: "REQ-002", status: "passed", lastRun: "2025-12-09T13:00:00Z", duration: 7200 },
{ id: "TC-037", name: "Altitude hold accuracy", category: "Sensors", requirementId: "REQ-005", status: "passed", lastRun: "2025-12-10T11:00:00Z", duration: 14400 },
{ id: "TC-038", name: "WiFi channel congestion handling", category: "Communication", requirementId: "REQ-003", status: "pending" },
{ id: "TC-039", name: "Motor over-temp protection", category: "Motor Control", requirementId: "REQ-008", status: "passed", lastRun: "2025-12-10T13:30:00Z", duration: 7200 },
{ id: "TC-040", name: "Humidity exposure (95% RH)", category: "Environmental", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-04T08:00:00Z", duration: 172800 },
{ id: "TC-041", name: "Real-time clock drift", category: "Firmware", requirementId: "REQ-006", status: "passed", lastRun: "2025-12-10T08:00:00Z", duration: 86400 },
{ id: "TC-042", name: "Power consumption idle mode", category: "Power", requirementId: "REQ-001", status: "passed", lastRun: "2025-12-09T10:00:00Z", duration: 3600 },
{ id: "TC-043", name: "GNSS antenna gain pattern", category: "Sensors", requirementId: "REQ-005", status: "pending" },
{ id: "TC-044", name: "CAN bus error handling", category: "Communication", requirementId: "REQ-004", status: "passed", lastRun: "2025-12-10T15:30:00Z", duration: 4200 },
{ id: "TC-045", name: "Drop test (1.5m onto concrete)", category: "Environmental", requirementId: "REQ-008", status: "pending" },
];
// ---------------------------------------------------------------------------
// Page component
// ---------------------------------------------------------------------------
export default function TestingPage() {
const { data: coverage, isLoading, isError, error } = useTestCoverage();
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
const [statusFilter, setStatusFilter] = useState<string | null>(null);
// Derive stats from mock data
const stats = useMemo(() => {
const total = MOCK_TEST_CASES.length;
const passed = MOCK_TEST_CASES.filter((t) => t.status === "passed").length;
const failed = MOCK_TEST_CASES.filter((t) => t.status === "failed").length;
const coveragePct = total > 0 ? Math.round((passed / total) * 100) : 0;
return { total, passed, failed, coveragePct };
}, []);
// Filtered test cases
const filteredTests = useMemo(() => {
return MOCK_TEST_CASES.filter((t) => {
if (categoryFilter && t.category !== categoryFilter) return false;
if (statusFilter && t.status !== statusFilter) return false;
return true;
});
}, [categoryFilter, statusFilter]);
return (
<div className="space-y-6">
{/* ---- Page header ---- */}
<PageHeader
title="Testing & Validation"
description="Test coverage tracking, results management, and FMEA risk analysis"
>
<Badge
variant="outline"
className={
stats.coveragePct >= 70
? "border-emerald-500 text-emerald-600"
: stats.coveragePct >= 40
? "border-amber-500 text-amber-600"
: "border-red-500 text-red-600"
}
>
{stats.coveragePct}% Coverage
</Badge>
</PageHeader>
{/* ---- Metric cards ---- */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<MetricCard
title="Total Tests"
value={stats.total}
icon={FlaskConical}
/>
<MetricCard
title="Passed"
value={stats.passed}
icon={CheckCircle2}
change={`${Math.round((stats.passed / stats.total) * 100)}%`}
/>
<MetricCard
title="Failed"
value={stats.failed}
icon={XCircle}
/>
<MetricCard
title="Coverage"
value={`${stats.coveragePct}%`}
icon={Percent}
/>
</div>
{/* ---- Error state ---- */}
{isError && (
<Card className="border-destructive">
<CardContent className="py-8 text-center">
<AlertTriangle className="mx-auto h-10 w-10 text-destructive mb-3" />
<p className="text-sm text-muted-foreground">
Failed to load testing data.{" "}
{error instanceof Error ? error.message : "Unknown error."}
</p>
</CardContent>
</Card>
)}
{/* ---- Loading state ---- */}
{isLoading && (
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<Skeleton className="h-80 rounded-xl" />
<Skeleton className="h-80 rounded-xl" />
</div>
)}
{/* ---- Two-column layout: Heatmap + Results table ---- */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<CoverageHeatmap
testCases={MOCK_TEST_CASES}
onCategoryClick={(category) => {
setCategoryFilter(
categoryFilter === category ? null : category,
);
}}
activeCategory={categoryFilter}
/>
<TestResultsTable
testCases={filteredTests}
categoryFilter={categoryFilter}
statusFilter={statusFilter}
onCategoryFilterChange={setCategoryFilter}
onStatusFilterChange={setStatusFilter}
/>
</div>
{/* ---- Bottom section: Requirements Matrix + FMEA ---- */}
<Tabs defaultValue="requirements-matrix">
<TabsList>
<TabsTrigger value="requirements-matrix">
Requirements-Test Matrix
</TabsTrigger>
<TabsTrigger value="fmea">FMEA Risk Table</TabsTrigger>
</TabsList>
<TabsContent value="requirements-matrix" className="mt-4">
<RequirementTestMatrix testCases={MOCK_TEST_CASES} />
</TabsContent>
<TabsContent value="fmea" className="mt-4">
<FMEARiskTable />
</TabsContent>
</Tabs>
</div>
);
}
src/pages/testing/components/coverage-heatmap.tsx
Grid of color-coded cells showing test coverage by category.
import { useMemo } from "react";
import {
Card,
CardHeader,
CardTitle,
CardContent,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
interface TestCase {
id: string;
name: string;
category: string;
status: "passed" | "failed" | "pending" | "skipped";
}
interface CoverageHeatmapProps {
testCases: TestCase[];
onCategoryClick: (category: string) => void;
activeCategory: string | null;
}
// ---------------------------------------------------------------------------
// Category definitions
// ---------------------------------------------------------------------------
const CATEGORIES = [
"Power",
"Communication",
"Sensors",
"Motor Control",
"Firmware",
"Environmental",
] as const;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
interface CategoryStats {
category: string;
total: number;
passed: number;
percentage: number;
}
function coverageColor(pct: number): string {
if (pct < 40) return "bg-red-500/80 hover:bg-red-500/90";
if (pct < 70) return "bg-amber-500/80 hover:bg-amber-500/90";
return "bg-emerald-500/80 hover:bg-emerald-500/90";
}
function coverageBorder(pct: number, isActive: boolean): string {
if (isActive) return "ring-2 ring-primary ring-offset-2";
if (pct < 40) return "ring-1 ring-red-400/30";
if (pct < 70) return "ring-1 ring-amber-400/30";
return "ring-1 ring-emerald-400/30";
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function CoverageHeatmap({
testCases,
onCategoryClick,
activeCategory,
}: CoverageHeatmapProps) {
const stats = useMemo<CategoryStats[]>(() => {
return CATEGORIES.map((category) => {
const categoryTests = testCases.filter((t) => t.category === category);
const total = categoryTests.length;
const passed = categoryTests.filter((t) => t.status === "passed").length;
const percentage = total > 0 ? Math.round((passed / total) * 100) : 0;
return { category, total, passed, percentage };
});
}, [testCases]);
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Coverage by Category</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{stats.map((s) => (
<button
key={s.category}
type="button"
onClick={() => onCategoryClick(s.category)}
className={cn(
"flex flex-col items-center justify-center",
"rounded-xl p-4 text-white transition-all cursor-pointer",
"min-h-[100px]",
coverageColor(s.percentage),
coverageBorder(s.percentage, activeCategory === s.category),
)}
>
<span className="text-xs font-medium opacity-90 mb-1">
{s.category}
</span>
<span className="text-2xl font-bold tabular-nums">
{s.percentage}%
</span>
<span className="text-xs opacity-80 mt-1">
{s.passed}/{s.total} tests
</span>
</button>
))}
</div>
<p className="mt-3 text-xs text-muted-foreground text-center">
Click a category to filter the results table
</p>
</CardContent>
</Card>
);
}
src/pages/testing/components/test-results-table.tsx
TanStack Table showing all test cases with sorting, filtering, and expandable rows.
import { useState, useMemo } from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getExpandedRowModel,
flexRender,
type ColumnDef,
type SortingState,
type ExpandedState,
} from "@tanstack/react-table";
import {
CheckCircle2,
XCircle,
Clock,
MinusCircle,
ChevronDown,
ChevronRight,
ArrowUpDown,
} from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardContent,
} from "@/components/ui/card";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { formatRelativeTime } from "@/lib/format";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface TestResult {
runId: string;
timestamp: string;
status: "passed" | "failed" | "skipped";
duration: number;
notes?: string;
}
interface TestCase {
id: string;
name: string;
category: string;
requirementId: string;
status: "passed" | "failed" | "pending" | "skipped";
lastRun?: string;
duration?: number;
results?: TestResult[];
}
interface TestResultsTableProps {
testCases: TestCase[];
categoryFilter: string | null;
statusFilter: string | null;
onCategoryFilterChange: (category: string | null) => void;
onStatusFilterChange: (status: string | null) => void;
}
// ---------------------------------------------------------------------------
// Status rendering
// ---------------------------------------------------------------------------
const STATUS_ICONS: Record<
TestCase["status"],
{ icon: typeof CheckCircle2; color: string; label: string }
> = {
passed: { icon: CheckCircle2, color: "text-emerald-500", label: "Passed" },
failed: { icon: XCircle, color: "text-red-500", label: "Failed" },
pending: { icon: Clock, color: "text-gray-400", label: "Pending" },
skipped: { icon: MinusCircle, color: "text-amber-500", label: "Skipped" },
};
function StatusBadge({ status }: { status: TestCase["status"] }) {
const config = STATUS_ICONS[status];
const Icon = config.icon;
return (
<Badge
variant="outline"
className={cn("gap-1 text-xs", config.color)}
>
<Icon className="h-3 w-3" />
{config.label}
</Badge>
);
}
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
if (ms < 3_600_000) return `${(ms / 60_000).toFixed(1)}m`;
return `${(ms / 3_600_000).toFixed(1)}h`;
}
// ---------------------------------------------------------------------------
// Columns
// ---------------------------------------------------------------------------
const columns: ColumnDef<TestCase>[] = [
{
id: "expander",
header: () => null,
cell: ({ row }) => {
if (!row.original.results?.length) return null;
return (
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={row.getToggleExpandedHandler()}
aria-label={row.getIsExpanded() ? "Collapse row" : "Expand row"}
>
{row.getIsExpanded() ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
);
},
size: 32,
enableSorting: false,
},
{
accessorKey: "id",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
ID
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<code className="text-xs font-mono">{getValue<string>()}</code>
),
size: 80,
},
{
accessorKey: "name",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<span className="text-sm">{getValue<string>()}</span>
),
size: 250,
},
{
accessorKey: "category",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Category
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<Badge variant="secondary" className="text-xs">
{getValue<string>()}
</Badge>
),
size: 130,
},
{
accessorKey: "requirementId",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Req ID
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<code className="text-xs font-mono">{getValue<string>()}</code>
),
size: 80,
},
{
accessorKey: "status",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => <StatusBadge status={getValue<TestCase["status"]>()} />,
size: 100,
},
{
accessorKey: "lastRun",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Last Run
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => {
const v = getValue<string | undefined>();
return v ? (
<span className="text-xs text-muted-foreground">
{formatRelativeTime(v)}
</span>
) : (
<span className="text-xs text-muted-foreground">--</span>
);
},
size: 120,
},
{
accessorKey: "duration",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Duration
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => {
const v = getValue<number | undefined>();
return v !== undefined ? (
<span className="text-xs tabular-nums">{formatDuration(v)}</span>
) : (
<span className="text-xs text-muted-foreground">--</span>
);
},
size: 80,
},
];
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function TestResultsTable({
testCases,
categoryFilter,
statusFilter,
onCategoryFilterChange,
onStatusFilterChange,
}: TestResultsTableProps) {
const [sorting, setSorting] = useState<SortingState>([]);
const [expanded, setExpanded] = useState<ExpandedState>({});
const table = useReactTable({
data: testCases,
columns,
state: { sorting, expanded },
onSortingChange: setSorting,
onExpandedChange: setExpanded,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getRowCanExpand: (row) => (row.original.results?.length ?? 0) > 0,
});
const categories = useMemo(
() => [...new Set(testCases.map((t) => t.category))].sort(),
[testCases],
);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between gap-4 flex-wrap">
<CardTitle className="text-base">Test Results</CardTitle>
<div className="flex items-center gap-2">
{/* Category filter */}
<Select
value={categoryFilter ?? "all"}
onValueChange={(v) =>
onCategoryFilterChange(v === "all" ? null : v)
}
>
<SelectTrigger className="w-[150px] h-8 text-xs">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{categories.map((c) => (
<SelectItem key={c} value={c}>
{c}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Status filter */}
<Select
value={statusFilter ?? "all"}
onValueChange={(v) =>
onStatusFilterChange(v === "all" ? null : v)
}
>
<SelectTrigger className="w-[120px] h-8 text-xs">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="passed">Passed</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="skipped">Skipped</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent className="overflow-x-auto">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
style={{ width: header.getSize() }}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-32 text-center text-muted-foreground"
>
No test cases match the current filters.
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<>
<TableRow
key={row.id}
className={cn(
row.getIsExpanded() && "bg-muted/30",
)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
{/* Expanded row: test result history */}
{row.getIsExpanded() && row.original.results && (
<TableRow key={`${row.id}-expanded`}>
<TableCell colSpan={columns.length} className="p-0">
<div className="bg-muted/20 px-8 py-3 border-t">
<p className="text-xs font-medium text-muted-foreground mb-2">
Run History
</p>
<div className="space-y-2">
{row.original.results.map((result) => {
const config = STATUS_ICONS[result.status];
const Icon = config.icon;
return (
<div
key={result.runId}
className="flex items-center gap-3 text-xs"
>
<Icon
className={cn("h-3.5 w-3.5", config.color)}
/>
<span className="font-mono text-muted-foreground">
{result.runId}
</span>
<span className="text-muted-foreground">
{formatRelativeTime(result.timestamp)}
</span>
<span className="tabular-nums">
{formatDuration(result.duration)}
</span>
{result.notes && (
<span className="text-muted-foreground italic truncate max-w-[300px]">
{result.notes}
</span>
)}
</div>
);
})}
</div>
</div>
</TableCell>
</TableRow>
)}
</>
))
)}
</TableBody>
</Table>
<p className="mt-3 text-xs text-muted-foreground text-right">
Showing {table.getRowModel().rows.length} of {testCases.length} test
cases
</p>
</CardContent>
</Card>
);
}
src/pages/testing/components/requirement-test-matrix.tsx
Traceability matrix linking requirements to test categories with coverage indicators and tooltips.
import { useMemo } from "react";
import {
Card,
CardHeader,
CardTitle,
CardContent,
} from "@/components/ui/card";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@/components/ui/table";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface TestCase {
id: string;
name: string;
category: string;
requirementId: string;
status: "passed" | "failed" | "pending" | "skipped";
}
interface RequirementTestMatrixProps {
testCases: TestCase[];
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const REQUIREMENTS = [
{ id: "REQ-001", title: "Power Management" },
{ id: "REQ-002", title: "Safety & Recovery" },
{ id: "REQ-003", title: "Communication Range" },
{ id: "REQ-004", title: "Telemetry & Data Link" },
{ id: "REQ-005", title: "Navigation Sensors" },
{ id: "REQ-006", title: "Data Integrity" },
{ id: "REQ-007", title: "Motor Control" },
{ id: "REQ-008", title: "Environmental Robustness" },
] as const;
const CATEGORIES = [
"Power",
"Communication",
"Sensors",
"Motor Control",
"Firmware",
"Environmental",
] as const;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
interface CellData {
total: number;
passed: number;
testNames: string[];
coverage: "covered" | "partial" | "uncovered";
}
function coverageDotColor(cov: CellData["coverage"]): string {
switch (cov) {
case "covered":
return "bg-emerald-500";
case "partial":
return "bg-amber-500";
case "uncovered":
return "bg-red-500";
}
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function RequirementTestMatrix({
testCases,
}: RequirementTestMatrixProps) {
const matrix = useMemo(() => {
const result: Record<string, Record<string, CellData>> = {};
for (const req of REQUIREMENTS) {
result[req.id] = {};
for (const cat of CATEGORIES) {
const tests = testCases.filter(
(t) => t.requirementId === req.id && t.category === cat,
);
const total = tests.length;
const passed = tests.filter((t) => t.status === "passed").length;
let coverage: CellData["coverage"] = "uncovered";
if (total > 0 && passed === total) coverage = "covered";
else if (total > 0 && passed > 0) coverage = "partial";
else if (total > 0) coverage = "partial";
result[req.id][cat] = {
total,
passed,
testNames: tests.map((t) => t.name),
coverage: total === 0 ? "uncovered" : coverage,
};
}
}
return result;
}, [testCases]);
return (
<Card>
<CardHeader>
<CardTitle className="text-base">
Requirements-Test Traceability Matrix
</CardTitle>
</CardHeader>
<CardContent className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[120px]">Requirement</TableHead>
<TableHead className="min-w-[100px]">Title</TableHead>
{CATEGORIES.map((cat) => (
<TableHead key={cat} className="text-center min-w-[90px]">
{cat}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{REQUIREMENTS.map((req) => (
<TableRow key={req.id}>
<TableCell>
<code className="text-xs font-mono">{req.id}</code>
</TableCell>
<TableCell className="text-sm">{req.title}</TableCell>
{CATEGORIES.map((cat) => {
const cell = matrix[req.id]?.[cat];
if (!cell || cell.total === 0) {
return (
<TableCell key={cat} className="text-center">
<span className="text-xs text-muted-foreground">
--
</span>
</TableCell>
);
}
return (
<TableCell key={cat} className="text-center">
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex items-center gap-1.5 cursor-default">
<span
className={cn(
"h-2.5 w-2.5 rounded-full",
coverageDotColor(cell.coverage),
)}
/>
<span className="text-xs tabular-nums">
{cell.passed}/{cell.total}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[250px]">
<p className="text-xs font-medium mb-1">
{req.id} — {cat}
</p>
<p className="text-xs text-muted-foreground mb-1">
{cell.passed} of {cell.total} tests passing
</p>
{cell.testNames.length > 0 && (
<ul className="text-xs text-muted-foreground list-disc list-inside">
{cell.testNames.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
)}
</TooltipContent>
</Tooltip>
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
{/* Legend */}
<div className="flex items-center gap-4 mt-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-emerald-500" />
Covered
</div>
<div className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
Partial
</div>
<div className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-red-500" />
Uncovered
</div>
</div>
</CardContent>
</Card>
);
}
src/pages/testing/components/fmea-risk-table.tsx
FMEA (Failure Mode and Effects Analysis) table with RPN color coding, sorting, and realistic drone flight controller entries.
import { useState } from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
flexRender,
type ColumnDef,
type SortingState,
} from "@tanstack/react-table";
import { ArrowUpDown } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardContent,
} from "@/components/ui/card";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface FMEAEntry {
id: string;
component: string;
failureMode: string;
effect: string;
cause: string;
severity: number;
occurrence: number;
detection: number;
rpn: number;
action: string;
status: "open" | "mitigated" | "closed";
}
// ---------------------------------------------------------------------------
// Mock FMEA data for DFC-v2.1
// ---------------------------------------------------------------------------
const FMEA_DATA: FMEAEntry[] = [
{
id: "FMEA-001",
component: "Motor Driver (DRV8313)",
failureMode: "Overheat during sustained max throttle",
effect: "Motor output degradation, potential thermal shutdown mid-flight",
cause: "Insufficient PCB copper pour for heat dissipation",
severity: 8,
occurrence: 3,
detection: 4,
rpn: 96,
action: "Add thermal vias under IC pad, increase copper pour area to 400mm2",
status: "mitigated",
},
{
id: "FMEA-002",
component: "IMU (BMI270)",
failureMode: "Gyroscope drift exceeding 0.5 deg/s",
effect: "Attitude estimation error, flight instability in GPS-denied mode",
cause: "Temperature gradient across sensor package",
severity: 7,
occurrence: 4,
detection: 5,
rpn: 140,
action: "Implement runtime temperature compensation, add insulation gasket",
status: "open",
},
{
id: "FMEA-003",
component: "GPS (MAX-M10S)",
failureMode: "Signal loss in urban canyon environment",
effect: "Position hold failure, drift during autonomous mission",
cause: "Multipath interference and antenna placement near motor noise",
severity: 6,
occurrence: 5,
detection: 3,
rpn: 90,
action: "Relocate GPS antenna to top of frame, add SAW filter for L1 band",
status: "open",
},
{
id: "FMEA-004",
component: "Battery Pack (4S LiPo)",
failureMode: "Overcurrent draw exceeding 60A",
effect: "Battery cell damage, potential thermal runaway",
cause: "All motors at maximum thrust during aggressive maneuver",
severity: 9,
occurrence: 2,
detection: 3,
rpn: 54,
action: "Add hardware current limiter, implement firmware throttle ceiling",
status: "mitigated",
},
{
id: "FMEA-005",
component: "Flash (W25Q128)",
failureMode: "Data corruption during write operation",
effect: "Flight log loss, configuration parameter reset to defaults",
cause: "Power interruption during erase cycle, wear leveling failure",
severity: 7,
occurrence: 2,
detection: 6,
rpn: 84,
action: "Implement CRC32 checksums on all stored data blocks, add write journaling",
status: "open",
},
{
id: "FMEA-006",
component: "WiFi (ESP32-S3)",
failureMode: "Connection drop during video stream",
effect: "Operator loses FPV feed, must rely on telemetry only",
cause: "Channel congestion in 2.4GHz ISM band at public venues",
severity: 4,
occurrence: 5,
detection: 4,
rpn: 80,
action: "Enable automatic 5GHz fallback, implement adaptive bitrate streaming",
status: "mitigated",
},
{
id: "FMEA-007",
component: "Power Regulator (TPS62823)",
failureMode: "Output ripple exceeding 50mV peak-to-peak",
effect: "ADC noise floor increase, degraded sensor readings",
cause: "Insufficient output capacitance, PCB layout parasitic inductance",
severity: 6,
occurrence: 3,
detection: 5,
rpn: 90,
action: "Add 22uF ceramic cap at sensor rail, shorten output traces to <5mm",
status: "open",
},
{
id: "FMEA-008",
component: "LoRa (SX1262)",
failureMode: "Range degradation below 2km threshold",
effect: "Loss of long-range telemetry backup, failsafe RTH may trigger",
cause: "Antenna impedance mismatch due to enclosure proximity effects",
severity: 5,
occurrence: 3,
detection: 5,
rpn: 75,
action: "Re-tune matching network with VNA after enclosure assembly, add 50 ohm test point",
status: "closed",
},
{
id: "FMEA-009",
component: "SDRAM (IS42S16400J)",
failureMode: "Single-bit errors during burst read",
effect: "Image processing artifacts, occasional frame buffer corruption",
cause: "Signal integrity issues on data bus at 133MHz, crosstalk from adjacent motor traces",
severity: 8,
occurrence: 2,
detection: 7,
rpn: 112,
action: "Add series termination resistors on data lines, increase trace spacing to 3W",
status: "open",
},
{
id: "FMEA-010",
component: "Barometer (BMP390)",
failureMode: "Altitude reading drift >2m over 10 minutes",
effect: "Altitude hold mode inaccuracy, potential ground collision at low altitude",
cause: "Airflow turbulence from propellers entering sensor port",
severity: 5,
occurrence: 3,
detection: 4,
rpn: 60,
action: "Add foam baffle around barometer port, implement moving average filter with wind compensation",
status: "mitigated",
},
{
id: "FMEA-011",
component: "Magnetometer (MMC5983MA)",
failureMode: "Hard iron interference from motor current",
effect: "Compass heading error >15 degrees, incorrect yaw reference",
cause: "Sensor placement too close to high-current motor power traces",
severity: 4,
occurrence: 6,
detection: 4,
rpn: 96,
action: "Relocate magnetometer to GPS mast extension, add runtime hard/soft iron calibration",
status: "open",
},
{
id: "FMEA-012",
component: "Co-processor (STM32F103)",
failureMode: "Watchdog timeout causing ESC communication loss",
effect: "Motor output freeze for <100ms, altitude perturbation",
cause: "ISR priority conflict with I2C sensor polling on shared bus",
severity: 7,
occurrence: 2,
detection: 3,
rpn: 42,
action: "Move ESC comms to dedicated SPI bus, separate ISR priority groups",
status: "closed",
},
];
// ---------------------------------------------------------------------------
// RPN color helpers
// ---------------------------------------------------------------------------
function rpnColor(rpn: number): string {
if (rpn < 50) return "text-emerald-600 bg-emerald-100 dark:bg-emerald-900/30";
if (rpn < 100) return "text-amber-600 bg-amber-100 dark:bg-amber-900/30";
if (rpn < 200) return "text-orange-600 bg-orange-100 dark:bg-orange-900/30";
return "text-red-600 bg-red-100 dark:bg-red-900/30";
}
function statusBadgeVariant(
status: FMEAEntry["status"],
): "destructive" | "outline" | "default" {
switch (status) {
case "open":
return "destructive";
case "mitigated":
return "outline";
case "closed":
return "default";
}
}
function statusLabel(status: FMEAEntry["status"]): string {
switch (status) {
case "open":
return "Open";
case "mitigated":
return "Mitigated";
case "closed":
return "Closed";
}
}
// ---------------------------------------------------------------------------
// Columns
// ---------------------------------------------------------------------------
const columns: ColumnDef<FMEAEntry>[] = [
{
accessorKey: "component",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Component
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<span className="text-sm font-medium">{getValue<string>()}</span>
),
size: 180,
},
{
accessorKey: "failureMode",
header: "Failure Mode",
cell: ({ getValue }) => (
<span className="text-xs">{getValue<string>()}</span>
),
size: 220,
},
{
accessorKey: "effect",
header: "Effect",
cell: ({ getValue }) => (
<span className="text-xs text-muted-foreground">{getValue<string>()}</span>
),
size: 200,
},
{
accessorKey: "cause",
header: "Cause",
cell: ({ getValue }) => (
<span className="text-xs text-muted-foreground">{getValue<string>()}</span>
),
size: 200,
},
{
accessorKey: "severity",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
S
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<span className="text-xs tabular-nums font-medium">{getValue<number>()}</span>
),
size: 48,
},
{
accessorKey: "occurrence",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
O
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<span className="text-xs tabular-nums font-medium">{getValue<number>()}</span>
),
size: 48,
},
{
accessorKey: "detection",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
D
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => (
<span className="text-xs tabular-nums font-medium">{getValue<number>()}</span>
),
size: 48,
},
{
accessorKey: "rpn",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
RPN
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => {
const rpn = getValue<number>();
return (
<Badge className={cn("text-xs tabular-nums font-bold", rpnColor(rpn))}>
{rpn}
</Badge>
);
},
size: 70,
sortDescFirst: true,
},
{
accessorKey: "action",
header: "Recommended Action",
cell: ({ getValue }) => (
<span className="text-xs">{getValue<string>()}</span>
),
size: 250,
},
{
accessorKey: "status",
header: ({ column }) => (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<ArrowUpDown className="ml-1 h-3 w-3" />
</Button>
),
cell: ({ getValue }) => {
const status = getValue<FMEAEntry["status"]>();
return (
<Badge variant={statusBadgeVariant(status)} className="text-xs">
{statusLabel(status)}
</Badge>
);
},
size: 90,
},
];
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function FMEARiskTable() {
const [sorting, setSorting] = useState<SortingState>([
{ id: "rpn", desc: true },
]);
const table = useReactTable({
data: FMEA_DATA,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base">
FMEA Risk Analysis — DFC-v2.1
</CardTitle>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span>
<strong>{FMEA_DATA.filter((e) => e.status === "open").length}</strong>{" "}
open
</span>
<span>
<strong>{FMEA_DATA.filter((e) => e.status === "mitigated").length}</strong>{" "}
mitigated
</span>
<span>
<strong>{FMEA_DATA.filter((e) => e.status === "closed").length}</strong>{" "}
closed
</span>
</div>
</div>
</CardHeader>
<CardContent className="overflow-x-auto">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
style={{ width: header.getSize() }}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
{/* RPN Legend */}
<div className="flex items-center gap-4 mt-4 text-xs text-muted-foreground flex-wrap">
<span className="font-medium">RPN Scale:</span>
<span className="flex items-center gap-1">
<span className="inline-block h-3 w-3 rounded bg-emerald-500" />
{"<50 (Low)"}
</span>
<span className="flex items-center gap-1">
<span className="inline-block h-3 w-3 rounded bg-amber-500" />
50-99 (Medium)
</span>
<span className="flex items-center gap-1">
<span className="inline-block h-3 w-3 rounded bg-orange-500" />
100-199 (High)
</span>
<span className="flex items-center gap-1">
<span className="inline-block h-3 w-3 rounded bg-red-500" />
{"200+ (Critical)"}
</span>
</div>
</CardContent>
</Card>
);
}