mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-22 05:23:31 +01:00
MAESTRO: add report test result card
This commit is contained in:
@@ -39,7 +39,7 @@ ReportContainer
|
|||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
- [x] T030 [US4] Write failing tests (TDD Red) in `frontend/src/__tests__/report/TestResultCard.test.tsx` (each severity level, expand/collapse, error indicator), `HopChainVisualisation.test.tsx` (render with sample hops), `ReportSearchBar.test.tsx` (filter simulation), `ReportExport.test.tsx` (export trigger), `SecurityAppliancesSummary.test.tsx` (render with sample appliances, empty state), `ReportContainer.test.tsx` (full report with mixed results)
|
- [x] T030 [US4] Write failing tests (TDD Red) in `frontend/src/__tests__/report/TestResultCard.test.tsx` (each severity level, expand/collapse, error indicator), `HopChainVisualisation.test.tsx` (render with sample hops), `ReportSearchBar.test.tsx` (filter simulation), `ReportExport.test.tsx` (export trigger), `SecurityAppliancesSummary.test.tsx` (render with sample appliances, empty state), `ReportContainer.test.tsx` (full report with mixed results)
|
||||||
- [ ] T031 [P] [US4] Create `frontend/src/components/report/TestResultCard.tsx` — collapsible card per test result. Severity-coloured indicator (red=spam, amber=suspicious, green=clean per FR-09), header name, monospace value, analysis text. Failed tests show error indicator (FR-25). Expand/collapse with animation, keyboard accessible (NFR-02). Verify `TestResultCard.test.tsx` passes (TDD Green)
|
- [x] T031 [P] [US4] Create `frontend/src/components/report/TestResultCard.tsx` — collapsible card per test result. Severity-coloured indicator (red=spam, amber=suspicious, green=clean per FR-09), header name, monospace value, analysis text. Failed tests show error indicator (FR-25). Expand/collapse with animation, keyboard accessible (NFR-02). Verify `TestResultCard.test.tsx` passes (TDD Green)
|
||||||
- [ ] T032 [P] [US4] Create `frontend/src/components/report/HopChainVisualisation.tsx` — vertical flow diagram of mail server hop chain (FR-08): hostname, IP, timestamp, server version, connecting arrows. FontAwesome server/network icons. Responsive. Verify `HopChainVisualisation.test.tsx` passes (TDD Green)
|
- [ ] T032 [P] [US4] Create `frontend/src/components/report/HopChainVisualisation.tsx` — vertical flow diagram of mail server hop chain (FR-08): hostname, IP, timestamp, server version, connecting arrows. FontAwesome server/network icons. Responsive. Verify `HopChainVisualisation.test.tsx` passes (TDD Green)
|
||||||
- [ ] T033 [P] [US4] Create `frontend/src/components/report/ReportSearchBar.tsx` — search/filter bar above report (FR-20). Filters by text match against test name, header name, or analysis text. Highlights matches, shows count. FontAwesome search icon, Escape to clear. Verify `ReportSearchBar.test.tsx` passes (TDD Green)
|
- [ ] T033 [P] [US4] Create `frontend/src/components/report/ReportSearchBar.tsx` — search/filter bar above report (FR-20). Filters by text match against test name, header name, or analysis text. Highlights matches, shows count. FontAwesome search icon, Escape to clear. Verify `ReportSearchBar.test.tsx` passes (TDD Green)
|
||||||
- [ ] T034 [P] [US4] Create `frontend/src/components/report/ReportExport.tsx` — export as HTML (styled standalone page) or JSON (raw data) per FR-21. FontAwesome download icons, triggers browser download. Verify `ReportExport.test.tsx` passes (TDD Green)
|
- [ ] T034 [P] [US4] Create `frontend/src/components/report/ReportExport.tsx` — export as HTML (styled standalone page) or JSON (raw data) per FR-21. FontAwesome download icons, triggers browser download. Verify `ReportExport.test.tsx` passes (TDD Green)
|
||||||
|
|||||||
129
frontend/src/components/report/TestResultCard.tsx
Normal file
129
frontend/src/components/report/TestResultCard.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faBan,
|
||||||
|
faCheck,
|
||||||
|
faCircleInfo,
|
||||||
|
faTriangleExclamation,
|
||||||
|
faChevronDown,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import type { TestResult, TestSeverity } from "../../types/analysis";
|
||||||
|
|
||||||
|
const severityStyles: Record<
|
||||||
|
TestSeverity,
|
||||||
|
{ label: string; className: string; icon: typeof faBan }
|
||||||
|
> = {
|
||||||
|
spam: { label: "Spam", className: "text-spam border-spam/40", icon: faBan },
|
||||||
|
suspicious: {
|
||||||
|
label: "Suspicious",
|
||||||
|
className: "text-suspicious border-suspicious/40",
|
||||||
|
icon: faTriangleExclamation,
|
||||||
|
},
|
||||||
|
clean: { label: "Clean", className: "text-clean border-clean/40", icon: faCheck },
|
||||||
|
info: { label: "Info", className: "text-accent border-accent/40", icon: faCircleInfo },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getErrorMessage = (result: TestResult): string => {
|
||||||
|
const message = result.error?.trim();
|
||||||
|
return message && message.length > 0 ? message : "Unknown failure.";
|
||||||
|
};
|
||||||
|
|
||||||
|
type TestResultCardProps = {
|
||||||
|
result: TestResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TestResultCard({ result }: TestResultCardProps) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const severityStyle = severityStyles[result.severity];
|
||||||
|
const detailsId = `test-result-details-${result.testId}`;
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
setIsExpanded((previous) => !previous);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
data-testid={`test-result-${result.testId}`}
|
||||||
|
className="rounded-2xl border border-info/10 bg-surface/50 p-4 shadow-[0_0_30px_rgba(15,23,42,0.18)]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid={`test-result-toggle-${result.testId}`}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-controls={detailsId}
|
||||||
|
onClick={toggle}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="flex w-full items-center justify-between gap-4 text-left"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-semibold text-text/90">{result.testName}</span>
|
||||||
|
<span className="text-xs text-text/50">Test #{result.testId}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
data-testid={`test-result-severity-${result.testId}`}
|
||||||
|
className={`flex items-center gap-2 rounded-full border px-2 py-1 text-[11px] uppercase tracking-[0.2em] ${severityStyle.className}`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={severityStyle.icon} className="text-[10px]" />
|
||||||
|
{severityStyle.label}
|
||||||
|
</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronDown}
|
||||||
|
className={`text-xs text-text/60 transition-transform duration-300 ${
|
||||||
|
isExpanded ? "rotate-180" : "rotate-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={detailsId}
|
||||||
|
className={`mt-3 grid transition-[grid-template-rows,opacity] duration-300 ease-out ${
|
||||||
|
isExpanded ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div className="rounded-xl border border-info/10 bg-background/40 p-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-[10px] uppercase tracking-[0.2em] text-text/40">
|
||||||
|
Header
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-text/60">{result.headerName}</span>
|
||||||
|
<span className="font-mono text-sm text-text/80">{result.headerValue}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.analysis ? (
|
||||||
|
<p className="mt-3 text-sm text-text/70">{result.analysis}</p>
|
||||||
|
) : null}
|
||||||
|
{result.description ? (
|
||||||
|
<p className="mt-1 text-xs text-text/50">{result.description}</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{result.status === "error" ? (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
data-testid={`test-result-error-${result.testId}`}
|
||||||
|
className="mt-3 flex items-start gap-2 rounded-xl border border-spam/30 bg-spam/10 p-3 text-xs text-spam"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTriangleExclamation} className="mt-0.5" />
|
||||||
|
<span>
|
||||||
|
<span className="font-semibold">Error:</span> {getErrorMessage(result)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user