From 1a041a0d59e943afe3b977e5f89995740cad8502 Mon Sep 17 00:00:00 2001 From: Mariusz Banach Date: Wed, 18 Feb 2026 02:20:42 +0100 Subject: [PATCH] MAESTRO: wire analyse submit to SSE stream --- ...er-analyzer-Phase-05-Analysis-Execution.md | 2 +- frontend/src/__tests__/HomePage.test.tsx | 101 ++++++++++++++++++ frontend/src/app/page.tsx | 36 +++---- 3 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 frontend/src/__tests__/HomePage.test.tsx 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 7a6e035..71357be 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 @@ -41,7 +41,7 @@ Frontend uses `fetch` with `ReadableStream` reader (not native `EventSource`, wh - [x] `pytest backend/tests/api/test_analysis_router.py` passes (all paths: happy, error, oversized, partial failure, timeout) - [x] All vitest tests pass: `npx vitest run src/__tests__/ProgressIndicator.test.tsx src/__tests__/useAnalysis.test.ts` -- [ ] Submitting headers triggers backend analysis with SSE streaming +- [x] Submitting headers triggers backend analysis with SSE streaming - [ ] Progress bar updates in real-time showing current test name and percentage - [ ] Countdown timer counts down from 30 seconds - [ ] Partial failures show inline error indicators per FR-25 diff --git a/frontend/src/__tests__/HomePage.test.tsx b/frontend/src/__tests__/HomePage.test.tsx new file mode 100644 index 0000000..498b938 --- /dev/null +++ b/frontend/src/__tests__/HomePage.test.tsx @@ -0,0 +1,101 @@ +import type { ReactElement } from "react"; +import { act } from "react-dom/test-utils"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import Home from "../app/page"; + +const { submitSpy, cancelSpy } = vi.hoisted(() => ({ + submitSpy: vi.fn().mockResolvedValue(undefined), + cancelSpy: vi.fn(), +})); + +vi.mock("../hooks/useAnalysis", () => ({ + __esModule: true, + default: () => ({ + status: "idle", + progress: null, + result: null, + error: null, + submit: submitSpy, + cancel: cancelSpy, + }), +})); + +type RenderResult = { + container: HTMLDivElement; +}; + +const cleanups: Array<() => void> = []; + +const render = (ui: ReactElement): RenderResult => { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render(ui); + }); + + cleanups.push(() => { + act(() => { + root.unmount(); + }); + container.remove(); + }); + + return { container }; +}; + +const getTextarea = (container: HTMLElement): HTMLTextAreaElement => { + const textarea = container.querySelector("textarea"); + if (!textarea) { + throw new Error("Expected header textarea to be rendered."); + } + return textarea as HTMLTextAreaElement; +}; + +const getAnalyseButton = (container: HTMLElement): HTMLButtonElement => { + const buttons = Array.from(container.querySelectorAll("button")); + const button = buttons.find((candidate) => + (candidate.textContent ?? "").toLowerCase().includes("analyse"), + ); + if (!button) { + throw new Error("Expected analyse button to be rendered."); + } + return button as HTMLButtonElement; +}; + +afterEach(() => { + while (cleanups.length > 0) { + const cleanup = cleanups.pop(); + if (cleanup) { + cleanup(); + } + } + submitSpy.mockClear(); + cancelSpy.mockClear(); +}); + +describe("Home page", () => { + it("submits analysis when analyse is clicked", () => { + const { container } = render(); + const textarea = getTextarea(container); + + act(() => { + textarea.value = "Received: from mail.example.com"; + textarea.dispatchEvent(new Event("input", { bubbles: true })); + }); + + const button = getAnalyseButton(container); + act(() => { + button.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(submitSpy).toHaveBeenCalledTimes(1); + expect(submitSpy).toHaveBeenCalledWith({ + headers: "Received: from mail.example.com", + config: { testIds: [], resolve: false, decodeAll: false }, + }); + }); +}); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 6c91a15..5c608f4 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,41 +1,35 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useState } from "react"; import AnalyseButton from "../components/AnalyseButton"; import FileDropZone from "../components/FileDropZone"; import HeaderInput from "../components/HeaderInput"; +import useAnalysis from "../hooks/useAnalysis"; import { MAX_HEADER_INPUT_BYTES } from "../lib/header-validation"; +import type { AnalysisConfig } from "../types/analysis"; + +const defaultConfig: AnalysisConfig = { + testIds: [], + resolve: false, + decodeAll: false, +}; export default function Home() { const [headerInput, setHeaderInput] = useState(""); - const [isAnalyzing, setIsAnalyzing] = useState(false); + const { status, submit } = useAnalysis(); const hasHeaderInput = headerInput.trim().length > 0; const isOversized = headerInput.length > MAX_HEADER_INPUT_BYTES; const canAnalyse = hasHeaderInput && !isOversized; - const analyseTimeoutRef = useRef(null); + const isLoading = status === "submitting" || status === "analysing"; - useEffect(() => { - return () => { - if (analyseTimeoutRef.current !== null) { - window.clearTimeout(analyseTimeoutRef.current); - } - }; - }, []); - - const handleAnalyse = () => { + const handleAnalyse = useCallback(() => { if (!canAnalyse) { return; } - setIsAnalyzing(true); - if (analyseTimeoutRef.current !== null) { - window.clearTimeout(analyseTimeoutRef.current); - } - analyseTimeoutRef.current = window.setTimeout(() => { - setIsAnalyzing(false); - }, 800); - }; + void submit({ headers: headerInput, config: defaultConfig }); + }, [canAnalyse, headerInput, submit]); return (
@@ -70,7 +64,7 @@ export default function Home() {