/** * Frontend seam test: drive a real React component against a fixture derived * from the actual RPC response (the canonical `CONTRACT_FIXTURES` shared with * the Rust side via the snapshot file). * * The first test renders `SettingsPage` against the well-formed fixture and * asserts the form populates with values from the RPC response — proving the * backend ↔ frontend wire shape lines up end-to-end without hand-rolled * fixtures. * * The second test feeds a *malformed* RPC response (a frame missing the * required envelope `ok` field) and asserts the `rpc.ts` client surfaces a * visible error in the rendered UI instead of leaving the page empty. */ import { afterEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { SettingsPage } from "./SettingsPage"; import { CONTRACT_FIXTURES } from "../api/rpcContract"; import snapshot from "../api/rpcContract.snapshot.json"; afterEach(() => { vi.restoreAllMocks(); }); interface MockSocket { url: string; onopen: ((ev: Event) => void) | null; onmessage: ((ev: { data: string }) => void) | null; onerror: ((ev: Event) => void) | null; onclose: ((ev: CloseEvent) => void) | null; readyState: number; send(data: string): void; close(): void; } /** * Install a `WebSocket` shim that hands each registered method a single * canned frame. Callers register either a normal RPC result or a * deliberately malformed frame body (returned verbatim — i.e. the body * literally has no `ok` field, simulating a server bug). */ function installSeamWs(replies: { [method: string]: { kind: "ok"; result: unknown } | { kind: "raw"; body: object }; }) { const instances: MockSocket[] = []; class SeamWs implements MockSocket { static readonly CONNECTING = 0; static readonly OPEN = 1; static readonly CLOSING = 2; static readonly CLOSED = 3; url: string; onopen: ((ev: Event) => void) | null = null; onmessage: ((ev: { data: string }) => void) | null = null; onerror: ((ev: Event) => void) | null = null; onclose: ((ev: CloseEvent) => void) | null = null; readyState = 0; constructor(url: string) { this.url = url; instances.push(this); queueMicrotask(() => { this.readyState = 1; this.onopen?.(new Event("open")); }); } send(data: string) { let frame: { correlation_id?: string; method?: string; }; try { frame = JSON.parse(data); } catch { return; } const { correlation_id, method } = frame; if (!correlation_id || !method) return; queueMicrotask(() => { const reply = replies[method]; if (!reply) { this.onmessage?.({ data: JSON.stringify({ kind: "rpc_response", version: 1, correlation_id, ok: false, error: `no fixture for ${method}`, code: "NOT_FOUND", }), }); return; } if (reply.kind === "ok") { this.onmessage?.({ data: JSON.stringify({ kind: "rpc_response", version: 1, correlation_id, ok: true, result: reply.result, }), }); return; } // raw: deliberately malformed envelope (no `ok` field) this.onmessage?.({ data: JSON.stringify({ kind: "rpc_response", version: 1, correlation_id, ...reply.body, }), }); }); } close() { this.readyState = 3; } } vi.stubGlobal("WebSocket", SeamWs); return instances; } describe("SettingsPage seam test", () => { it("renders ProjectSettings from the typed RPC contract fixture", async () => { // Sanity: the in-source fixture mirrors the on-disk snapshot file. If // this trips, the contract has drifted from the Rust side. expect(CONTRACT_FIXTURES["settings.put_project"].result).toEqual( snapshot["settings.put_project"].result, ); const fixture = CONTRACT_FIXTURES["settings.put_project"].result; installSeamWs({ "settings.get_project": { kind: "ok", result: fixture }, }); const onBack = vi.fn(); render(); await waitFor(() => { expect(screen.getByDisplayValue(String(fixture.max_retries))).toBeInTheDocument(); }); // Field driven directly by the RPC payload populates the form. expect( screen.getByDisplayValue(String(fixture.watcher_sweep_interval_secs)), ).toBeInTheDocument(); expect( screen.getByDisplayValue(String(fixture.watcher_done_retention_secs)), ).toBeInTheDocument(); }); it("shows a visible error when the RPC response is malformed", async () => { // `body` lacks the envelope `ok` field. The fixed `rpc.ts` client // should reject loudly with a `MALFORMED` error instead of letting // the page render empty. installSeamWs({ "settings.get_project": { kind: "raw", body: { result: { not_actually_settings: true } }, }, }); const onBack = vi.fn(); render(); await waitFor(() => { expect(screen.getByText(/Malformed RPC response/i)).toBeInTheDocument(); }); // And critically — no empty form is rendered. expect(screen.queryByText(/default qa/i)).not.toBeInTheDocument(); }); it("user can edit and the new value flows through settings.put_project RPC", async () => { const fixture = CONTRACT_FIXTURES["settings.put_project"].result; const updated = { ...fixture, max_retries: 9 }; installSeamWs({ "settings.get_project": { kind: "ok", result: fixture }, "settings.put_project": { kind: "ok", result: updated }, }); const onBack = vi.fn(); render(); const maxRetriesInput = (await screen.findByDisplayValue( String(fixture.max_retries), )) as HTMLInputElement; fireEvent.change(maxRetriesInput, { target: { value: "9" } }); fireEvent.click(screen.getByRole("button", { name: /save/i })); await waitFor(() => { expect(screen.getByDisplayValue("9")).toBeInTheDocument(); }); }); });