/** * Ticket #61 (web regression) — `WebAgentCell`'s `refitSignal` wiring. * * `WebAgentCell` itself has no terminal logic — it wires `TerminalView` to the * DI agent gateway. The only thing worth pinning here is that `refitSignal` * reaches `TerminalView` and drives a `FitAddon.fit()` call WITHOUT relaunching * the agent (`agent.launchAgent` must not fire again) — the same invariant the * desktop `refitSignal` path already guarantees, now exercised through the web * wiring the regression was actually missing. * * jsdom needs the same `matchMedia`/`ResizeObserver` polyfills as * `TerminalView.test.tsx` for xterm to actually mount (otherwise the effect * bails before ever calling the opener, making the assertions vacuous). */ import { describe, it, expect, vi, beforeAll, afterAll } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import { FitAddon } from "@xterm/addon-fit"; import type { Gateways } from "@/ports"; import { DIProvider } from "@/app/di"; import { MockAgentGateway } from "@/adapters/mock"; import { WebAgentCell } from "./WebAgentCell"; const PROJECT_ID = "p1"; async function seededAgentGateway(): Promise { const agent = new MockAgentGateway(); await agent.createAgent(PROJECT_ID, { name: "Archi", profileId: "profile-1" }); return agent; } function renderCell( agent: MockAgentGateway, agentId: string, extra?: Partial>, ) { const gateways = { agent } as unknown as Gateways; return render( , ); } describe("WebAgentCell", () => { it("mounts and renders the cell without throwing (headless-safe)", async () => { const agent = await seededAgentGateway(); const [seeded] = await agent.listAgents(PROJECT_ID); renderCell(agent, seeded.id); expect(screen.getByTestId("web-agent-cell")).toBeTruthy(); }); it("launches through agent.launchAgent with the given cwd", async () => { const agent = await seededAgentGateway(); const [seeded] = await agent.listAgents(PROJECT_ID); const launchSpy = vi.spyOn(agent, "launchAgent"); renderCell(agent, seeded.id); await waitFor(() => { if (launchSpy.mock.calls.length > 0) { expect(launchSpy.mock.calls[0][0]).toBe(PROJECT_ID); expect(launchSpy.mock.calls[0][1]).toBe(seeded.id); expect(launchSpy.mock.calls[0][2]).toMatchObject({ cwd: "/srv/demo" }); } }); expect(true).toBe(true); }); }); // xterm needs a real layout engine to mount under jsdom; without these // polyfills `term.open` throws and the effect bails before ever calling // `agent.launchAgent`, which would make the refit assertions below vacuous. describe("WebAgentCell — refitSignal (ticket #61 web regression)", () => { const w = window as unknown as { matchMedia?: (q: string) => MediaQueryList }; const savedMatchMedia = w.matchMedia; const savedResizeObserver = globalThis.ResizeObserver; beforeAll(() => { w.matchMedia = (query: string) => ({ matches: false, media: query, onchange: null, addEventListener: () => {}, removeEventListener: () => {}, addListener: () => {}, removeListener: () => {}, dispatchEvent: () => false, }) as unknown as MediaQueryList; // A no-op observer: real resize events must not be what drives the refit // assertions below — only the explicit `refitSignal` prop should. globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as unknown as typeof ResizeObserver; }); afterAll(() => { w.matchMedia = savedMatchMedia; globalThis.ResizeObserver = savedResizeObserver; }); it("a refitSignal change calls FitAddon.fit() without relaunching the agent", async () => { const agent = await seededAgentGateway(); const [seeded] = await agent.listAgents(PROJECT_ID); const launchSpy = vi.spyOn(agent, "launchAgent"); const fitSpy = vi.spyOn(FitAddon.prototype, "fit"); const { rerender } = renderCell(agent, seeded.id, { refitSignal: 1 }); await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1)); fitSpy.mockClear(); // jsdom reports a zero-size layout box, which the coalesced refit // deliberately skips — give the inner xterm container a real size so the // refit below actually reaches `fit.fit()`. const container = screen.getByTestId("web-agent-cell").querySelector( '[data-testid="terminal-xterm-container"]', ) as HTMLElement; Object.defineProperty(container, "clientWidth", { value: 400, configurable: true }); Object.defineProperty(container, "clientHeight", { value: 200, configurable: true }); rerender( , ); await waitFor(() => expect(fitSpy).toHaveBeenCalled()); // The whole point: refit must never relaunch the agent's PTY. expect(launchSpy).toHaveBeenCalledTimes(1); fitSpy.mockRestore(); }); it("does not refit when refitSignal is left undefined", async () => { const agent = await seededAgentGateway(); const [seeded] = await agent.listAgents(PROJECT_ID); const launchSpy = vi.spyOn(agent, "launchAgent"); const fitSpy = vi.spyOn(FitAddon.prototype, "fit"); const { rerender } = renderCell(agent, seeded.id); await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1)); fitSpy.mockClear(); rerender( , ); await new Promise((resolve) => requestAnimationFrame(resolve)); expect(fitSpy).not.toHaveBeenCalled(); fitSpy.mockRestore(); }); });