// frontend/src/shell/f1-loop.test.tsx import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse } from "msw"; import { act } from "react"; import { server } from "../test/msw"; import { workspaceSummaryFixture } from "../test/workspace-fixtures"; import { FakeEventSource } from "../test/fakeEventSource"; import { App } from "../App"; import { useSessionStore } from "../store/sessionStore"; beforeEach(() => { window.__THOTHII_CONFIG__ = { shell: { mode: "full", defaultLocale: "en" } }; localStorage.clear(); FakeEventSource.instances = []; (globalThis as any).EventSource = FakeEventSource; useSessionStore.getState().resetSession(); }); afterEach(() => { delete window.__THOTHII_CONFIG__; }); test("F1: create session -> widget via SSE -> respond -> POST /response", async () => { let responded: any = null; server.use( http.post("/api/sessions", () => HttpResponse.json({ id: "s1" })), http.get("/api/sessions", () => HttpResponse.json([])), http.get("/api/settings", () => HttpResponse.json({ workspace: "default", provider: "zai", model: "glm-5.2", thinking: "low", })), http.get("/api/workspaces", () => HttpResponse.json([workspaceSummaryFixture("default")])), http.get("/api/models", () => HttpResponse.json({ models: [{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }] })), http.post("/api/sessions/s1/response", async ({ request }) => { responded = await request.json(); return new HttpResponse(null, { status: 204 }); }), ); render(); // Type the first question directly in the sticky composer and send it const composer = await screen.findByRole("textbox", { name: /new question/i }); await userEvent.type(composer, "Quante vendite?"); await userEvent.click(screen.getByRole("button", { name: /send/i })); // Wait for the session to be created and SSE to connect await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); // Optimistic lifecycle: a brand-new question paints F1 immediately (before any gate). await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F1")); // Simulate the backend emitting the F1 widget via a NAMED SSE event // (`event: ui_request`) — the same addEventListener path production uses. act(() => FakeEventSource.instances[0].emitNamed("ui_request", { type: "ui_request", ui_request: { id: "u1", widget: "select", title: "Disambigua", options: [{ id: "a", label: "interpretazione A" }], }, }) ); await userEvent.click(await screen.findByRole("button", { name: /interpretazione A/ })); expect(responded).toEqual({ ui_response: { id: "u1", kind: "select", choices: ["a"] } }); });