diff --git a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md index 5e44e33..76ac8a9 100644 --- a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md +++ b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md @@ -45,7 +45,7 @@ Frontend uses `fetch` with `ReadableStream` reader (not native `EventSource`, wh - [x] Progress bar updates in real-time showing current test name and percentage - [x] Countdown timer counts down from 30 seconds - [x] Partial failures show inline error indicators per FR-25 (Added AnalysisResults rendering with inline error badges.) -- [ ] Timeout at 30s displays partial results with notification listing incomplete tests +- [x] Timeout at 30s displays partial results with notification listing incomplete tests - [ ] Empty input returns 400, oversized >1MB returns 413 - [ ] Linting passes on both sides - [ ] Run `/speckit.analyze` to verify consistency diff --git a/frontend/src/__tests__/HomePage.test.tsx b/frontend/src/__tests__/HomePage.test.tsx index 498b938..111ae80 100644 --- a/frontend/src/__tests__/HomePage.test.tsx +++ b/frontend/src/__tests__/HomePage.test.tsx @@ -4,22 +4,29 @@ import { createRoot } from "react-dom/client"; import { afterEach, describe, expect, it, vi } from "vitest"; import Home from "../app/page"; +import type { AnalysisProgress, AnalysisReport } from "../types/analysis"; -const { submitSpy, cancelSpy } = vi.hoisted(() => ({ - submitSpy: vi.fn().mockResolvedValue(undefined), - cancelSpy: vi.fn(), -})); +const { submitSpy, cancelSpy, useAnalysisState } = vi.hoisted(() => { + const submitSpy = vi.fn().mockResolvedValue(undefined); + const cancelSpy = vi.fn(); + + return { + submitSpy, + cancelSpy, + useAnalysisState: { + status: "idle", + progress: null, + result: null, + error: null, + submit: submitSpy, + cancel: cancelSpy, + }, + }; +}); vi.mock("../hooks/useAnalysis", () => ({ __esModule: true, - default: () => ({ - status: "idle", - progress: null, - result: null, - error: null, - submit: submitSpy, - cancel: cancelSpy, - }), + default: () => useAnalysisState, })); type RenderResult = { @@ -66,6 +73,21 @@ const getAnalyseButton = (container: HTMLElement): HTMLButtonElement => { return button as HTMLButtonElement; }; +const baseProgress: AnalysisProgress = { + currentIndex: 2, + totalTests: 4, + currentTest: "SpamAssassin Rule Hits", + elapsedMs: 29000, + percentage: 78, +}; + +const resetUseAnalysisState = (): void => { + useAnalysisState.status = "idle"; + useAnalysisState.progress = null; + useAnalysisState.result = null; + useAnalysisState.error = null; +}; + afterEach(() => { while (cleanups.length > 0) { const cleanup = cleanups.pop(); @@ -75,6 +97,7 @@ afterEach(() => { } submitSpy.mockClear(); cancelSpy.mockClear(); + resetUseAnalysisState(); }); describe("Home page", () => { @@ -98,4 +121,35 @@ describe("Home page", () => { config: { testIds: [], resolve: false, decodeAll: false }, }); }); + + it("shows timeout notification with incomplete tests and partial results", () => { + const timeoutReport: AnalysisReport = { + results: [], + hopChain: [], + securityAppliances: [], + metadata: { + totalTests: 4, + passedTests: 1, + failedTests: 0, + skippedTests: 0, + elapsedMs: 30000, + timedOut: true, + incompleteTests: ["Mimecast Fingerprint", "Proofpoint TAP"], + }, + }; + + useAnalysisState.status = "timeout"; + useAnalysisState.progress = baseProgress; + useAnalysisState.result = timeoutReport; + + const { container } = render(); + const alert = container.querySelector('[role="alert"]'); + expect(alert).not.toBeNull(); + const timeoutTests = container.querySelector('[data-testid="timeout-tests"]'); + expect(timeoutTests?.textContent ?? "").toMatch(/Mimecast Fingerprint/); + expect(timeoutTests?.textContent ?? "").toMatch(/Proofpoint TAP/); + + const results = container.querySelector('[data-testid="analysis-results"]'); + expect(results).not.toBeNull(); + }); }); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 7e14ef7..a95b8a0 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -24,7 +24,8 @@ export default function Home() { const isOversized = headerInput.length > MAX_HEADER_INPUT_BYTES; const canAnalyse = hasHeaderInput && !isOversized; const isLoading = status === "submitting" || status === "analysing"; - const showProgress = status === "analysing"; + const showProgress = status === "analysing" || status === "timeout"; + const incompleteTests = result?.metadata.incompleteTests ?? []; const handleAnalyse = useCallback(() => { if (!canAnalyse) { @@ -86,6 +87,7 @@ export default function Home() { status={status} progress={progress} timeoutSeconds={30} + incompleteTests={incompleteTests} /> ) : null}