mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-21 21:13:31 +01:00
MAESTRO: improve responsive wrapping and layout
This commit is contained in:
@@ -19,7 +19,7 @@ This phase performs final integration, accessibility audit, responsive testing,
|
||||
|
||||
- [x] T046 Wire all components together in `frontend/src/app/page.tsx` — integrate HeaderInput, FileDropZone, AnalysisControls, AnalyseButton, ProgressIndicator, ReportContainer, CaptchaChallenge into the single-view application with correct data flow. Ensure: input feeds to analysis hook, progress hook drives progress indicator, result feeds to report container, 429 errors trigger CAPTCHA modal, cache hook restores state on mount. Notes: added AnalysisControls + CAPTCHA retry flow, extended analysis hook for bypass token handling, confirmed cache restore.
|
||||
- [x] T047 Verify WCAG 2.1 AA compliance across all components (NFR-03) — ARIA labels, keyboard nav order, focus indicators, colour contrast ratios (dark theme). Fix violations. Test with screen reader simulation. Ensure all interactive elements have visible focus states. Notes: added keyboard-accessible file picker with ARIA descriptions, focus-visible outlines on drop zone/summary/search fields, boosted low-contrast text from 40% to 60%, linked CAPTCHA dialog description, added file picker tests; ran `npx vitest run src/__tests__/FileDropZone.test.tsx`.
|
||||
- [ ] T048 [P] Verify responsive layout 320px–2560px (NFR-04) at breakpoints: 320px, 768px, 1024px, 1440px, 2560px. No horizontal scroll, no overlapping elements, readable text. Fix any layout issues discovered
|
||||
- [x] T048 [P] Verify responsive layout 320px–2560px (NFR-04) at breakpoints: 320px, 768px, 1024px, 1440px, 2560px. No horizontal scroll, no overlapping elements, readable text. Fix any layout issues discovered. Notes: stacked control cards on small screens, added min-w-0 + flex-wrap on report UI, and break-words handling for long header values, hop chain hostnames/IPs, and search pills to prevent overflow.
|
||||
- [ ] T049 [P] Run full linting pass — `ruff check backend/` and `ruff format backend/` zero errors; `npx eslint src/` and `npx prettier --check src/` zero errors; no `any` types in TypeScript. Fix all violations
|
||||
- [ ] T050 [P] Run full test suites and verify coverage — `pytest backend/tests/ --cov` ≥80% new modules (NFR-06); `npx vitest run --coverage` ≥80% new components (NFR-07). Add missing tests if coverage is below threshold
|
||||
- [ ] T051 [P] Verify initial page load <3s on simulated 4G (constitution P7). Use Lighthouse with Slow 4G preset. Target score ≥90. Fix blocking resources or missing lazy-loading if score is below target
|
||||
|
||||
@@ -88,4 +88,16 @@ describe("HopChainVisualisation", () => {
|
||||
|
||||
expect(connectors.length).toBe(hopChain.length - 1);
|
||||
});
|
||||
|
||||
it("adds wrapping classes for long hostnames and IPs", () => {
|
||||
const { container } = render(<HopChainVisualisation hopChain={hopChain} />);
|
||||
|
||||
const firstHop = getByTestId(container, "hop-chain-node-0");
|
||||
const spanNodes = Array.from(firstHop.querySelectorAll("span"));
|
||||
const hostnameNode = spanNodes.find((node) => node.textContent === "mail.sender.example");
|
||||
const ipNode = spanNodes.find((node) => node.textContent === "192.0.2.10");
|
||||
|
||||
expect(hostnameNode?.className ?? "").toContain("break-words");
|
||||
expect(ipNode?.className ?? "").toContain("break-all");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,4 +127,25 @@ describe("TestResultCard", () => {
|
||||
const errorIndicator = getByTestId(container, `test-result-error-${result.testId}`);
|
||||
expect(errorIndicator.textContent ?? "").toMatch(/SpamAssassin database timeout/);
|
||||
});
|
||||
|
||||
it("adds wrapping classes for long header values", () => {
|
||||
const result = buildResult({
|
||||
testId: 505,
|
||||
headerValue: "X".repeat(200),
|
||||
});
|
||||
const { container } = render(<TestResultCard result={result} />);
|
||||
|
||||
const details = container.querySelector(`#test-result-details-${result.testId}`);
|
||||
if (!details) {
|
||||
throw new Error("Expected test details container to be rendered.");
|
||||
}
|
||||
|
||||
const headerValue = details.querySelector("span.font-mono");
|
||||
if (!headerValue) {
|
||||
throw new Error("Expected header value to be rendered.");
|
||||
}
|
||||
|
||||
expect(headerValue.className).toContain("break-words");
|
||||
expect(headerValue.className).toContain("whitespace-pre-wrap");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function AnalysisControls({ config, onChange }: AnalysisControlsP
|
||||
<span className="font-mono text-[10px] text-text/50">US2</span>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<div className="flex items-center justify-between gap-4 rounded-xl border border-info/10 bg-background/40 p-4">
|
||||
<div className="flex flex-col items-start gap-3 rounded-xl border border-info/10 bg-background/40 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 rounded-full border border-info/20 bg-background/60 p-2 text-xs text-info/80">
|
||||
<FontAwesomeIcon icon={faGlobe} />
|
||||
@@ -83,7 +83,7 @@ export default function AnalysisControls({ config, onChange }: AnalysisControlsP
|
||||
{resolvedConfig.resolve ? "On" : "Off"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4 rounded-xl border border-info/10 bg-background/40 p-4">
|
||||
<div className="flex flex-col items-start gap-3 rounded-xl border border-info/10 bg-background/40 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 rounded-full border border-info/20 bg-background/60 p-2 text-xs text-info/80">
|
||||
<FontAwesomeIcon icon={faCode} />
|
||||
|
||||
@@ -52,9 +52,11 @@ export default function AnalysisResults({ report }: AnalysisResultsProps) {
|
||||
className="rounded-2xl border border-info/10 bg-background/40 p-4"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold text-text/90">{result.testName}</span>
|
||||
<span className="text-xs text-text/50">
|
||||
<div className="min-w-0 flex flex-col">
|
||||
<span className="break-words text-sm font-semibold text-text/90">
|
||||
{result.testName}
|
||||
</span>
|
||||
<span className="break-words text-xs text-text/50">
|
||||
Test #{result.testId} · {result.headerName}
|
||||
</span>
|
||||
</div>
|
||||
@@ -69,10 +71,10 @@ export default function AnalysisResults({ report }: AnalysisResultsProps) {
|
||||
</div>
|
||||
|
||||
{result.analysis ? (
|
||||
<p className="mt-3 text-sm text-text/70">{result.analysis}</p>
|
||||
<p className="mt-3 break-words text-sm text-text/70">{result.analysis}</p>
|
||||
) : null}
|
||||
{result.description ? (
|
||||
<p className="mt-1 text-xs text-text/50">{result.description}</p>
|
||||
<p className="mt-1 break-words text-xs text-text/50">{result.description}</p>
|
||||
) : null}
|
||||
|
||||
{result.status === "error" ? (
|
||||
|
||||
@@ -142,7 +142,10 @@ export default function ProgressIndicator({
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span data-testid="progress-current-test" className="text-sm font-semibold text-text/80">
|
||||
<span
|
||||
data-testid="progress-current-test"
|
||||
className="break-words text-sm font-semibold text-text/80"
|
||||
>
|
||||
{progress?.currentTest ?? "Preparing analysis"}
|
||||
</span>
|
||||
<span data-testid="progress-percentage" className="text-sm font-semibold text-text/70">
|
||||
@@ -170,7 +173,7 @@ export default function ProgressIndicator({
|
||||
>
|
||||
<p className="font-semibold text-spam">Timeout reached at {timeoutSeconds} seconds.</p>
|
||||
<p className="mt-2 text-text/70">Incomplete tests:</p>
|
||||
<p data-testid="timeout-tests" className="mt-1 font-mono text-text/70">
|
||||
<p data-testid="timeout-tests" className="mt-1 break-words font-mono text-text/70">
|
||||
{incompleteTests.length > 0 ? incompleteTests.join(", ") : "None"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -47,22 +47,24 @@ export default function HopChainVisualisation({ hopChain }: HopChainVisualisatio
|
||||
<span className="flex h-10 w-10 items-center justify-center rounded-xl border border-info/20 bg-info/10 text-info">
|
||||
<FontAwesomeIcon icon={faServer} />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-2">
|
||||
<span className="text-sm font-semibold text-text/90">{node.hostname}</span>
|
||||
<span className="break-words text-sm font-semibold text-text/90">
|
||||
{node.hostname}
|
||||
</span>
|
||||
{node.ip ? (
|
||||
<span className="font-mono text-xs text-text/60">{node.ip}</span>
|
||||
<span className="break-all font-mono text-xs text-text/60">{node.ip}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-4 text-xs text-text/60">
|
||||
{node.timestamp ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-2 break-words">
|
||||
<FontAwesomeIcon icon={faClock} className="text-[10px]" />
|
||||
{node.timestamp}
|
||||
</span>
|
||||
) : null}
|
||||
{node.serverInfo ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-2 break-words">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="text-[10px]" />
|
||||
{node.serverInfo}
|
||||
</span>
|
||||
|
||||
@@ -40,14 +40,14 @@ export default function ReportSearchBar({
|
||||
className="rounded-2xl border border-info/10 bg-surface/50 p-4 shadow-[0_0_30px_rgba(15,23,42,0.18)]"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="flex flex-1 items-center gap-2 rounded-full border border-info/20 bg-background/40 px-3 py-2 text-sm text-text/70 focus-within:border-info/40 focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-info">
|
||||
<label className="flex min-w-0 flex-1 items-center gap-2 rounded-full border border-info/20 bg-background/40 px-3 py-2 text-sm text-text/70 focus-within:border-info/40 focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-info">
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} className="text-xs text-text/60" />
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1 bg-transparent text-xs text-text/80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
||||
className="min-w-0 flex-1 bg-transparent text-xs text-text/80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
||||
placeholder="Search test names, headers, or analysis"
|
||||
data-testid="report-search-input"
|
||||
aria-label="Search report results"
|
||||
@@ -77,7 +77,7 @@ export default function ReportSearchBar({
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] text-text/50">
|
||||
<span>{hasQuery ? "Matches for" : "Showing all results"}</span>
|
||||
{hasQuery ? (
|
||||
<span className="rounded-full border border-accent/30 bg-accent/10 px-2 py-0.5 font-mono text-[10px] text-accent">
|
||||
<span className="max-w-full break-words rounded-full border border-accent/30 bg-accent/10 px-2 py-0.5 font-mono text-[10px] text-accent">
|
||||
{query}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -35,11 +35,11 @@ export default function SecurityAppliancesSummary({ appliances }: SecurityApplia
|
||||
<div
|
||||
key={`${appliance.vendor}-${appliance.name}`}
|
||||
data-testid={`security-appliance-${index}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-3 py-1 text-xs text-text/70"
|
||||
className="inline-flex max-w-full flex-wrap items-center gap-2 rounded-full border border-info/20 bg-background/40 px-3 py-1 text-xs text-text/70"
|
||||
>
|
||||
<FontAwesomeIcon icon={faShield} className="text-[10px] text-info" />
|
||||
<span className="font-semibold">{appliance.vendor}</span>
|
||||
<span className="text-text/50">{appliance.name}</span>
|
||||
<span className="break-words font-semibold">{appliance.vendor}</span>
|
||||
<span className="break-words text-text/50">{appliance.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -101,10 +101,10 @@ export default function TestResultCard({ result, highlightQuery = "" }: TestResu
|
||||
aria-controls={detailsId}
|
||||
onClick={toggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex w-full items-center justify-between gap-4 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
||||
className="flex w-full flex-wrap items-center justify-between gap-4 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold text-text/90">
|
||||
<div className="min-w-0 flex flex-col">
|
||||
<span className="break-words text-sm font-semibold text-text/90">
|
||||
{highlightText(result.testName, highlightQuery)}
|
||||
</span>
|
||||
<span className="text-xs text-text/50">Test #{result.testId}</span>
|
||||
@@ -136,19 +136,21 @@ export default function TestResultCard({ result, highlightQuery = "" }: TestResu
|
||||
<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/60">Header</span>
|
||||
<span className="text-xs text-text/60">
|
||||
<span className="break-words text-xs text-text/60">
|
||||
{highlightText(result.headerName, highlightQuery)}
|
||||
</span>
|
||||
<span className="font-mono text-sm text-text/80">{result.headerValue}</span>
|
||||
<span className="break-words whitespace-pre-wrap font-mono text-sm text-text/80">
|
||||
{result.headerValue}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{result.analysis ? (
|
||||
<p className="mt-3 text-sm text-text/70">
|
||||
<p className="mt-3 break-words text-sm text-text/70">
|
||||
{highlightText(result.analysis, highlightQuery)}
|
||||
</p>
|
||||
) : null}
|
||||
{result.description ? (
|
||||
<p className="mt-1 text-xs text-text/50">{result.description}</p>
|
||||
<p className="mt-1 break-words text-xs text-text/50">{result.description}</p>
|
||||
) : null}
|
||||
|
||||
{result.status === "error" ? (
|
||||
|
||||
Reference in New Issue
Block a user