// @vitest-environment happy-dom import { afterEach, describe, expect, it, vi } from "vitest"; import { ConnectionPool } from "./connections.js"; // The pane's own bytes-out path, stubbed: the escape hatch only needs to know // the pool reaches the focused connection with them. const sent: { id: string; data: string }[] = []; vi.mock("./attach.js", () => ({ createAttachConnection: (id: string) => ({ sessionId: id, el: document.createElement("div"), onMount() {}, onUnmount() {}, focusTerminal() {}, sendInput(data: string) { sent.push({ id, data }); }, updateInfo() {}, dispose() {}, }), })); afterEach(() => { vi.unstubAllGlobals(); }); const A = "aaaaaaaa-0000-0000-0000-0000000000ff"; const B = "bbbbbbbb-0000-0000-0000-0000000000ff"; function mockFetch() { const fetchMock = vi.fn(() => Promise.resolve(new Response(null, { status: 204 }))); vi.stubGlobal("fetch", fetchMock); return fetchMock; } describe("ConnectionPool.setFocus", () => { it("reports the new session focused, none unfocused, the first time", () => { const fetchMock = mockFetch(); const pool = new ConnectionPool(); pool.setFocus(A); expect(fetchMock).toHaveBeenCalledOnce(); const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; expect(url).toBe(`/api/sessions/${A}/focus`); expect(init.method).toBe("POST"); expect(JSON.parse(init.body as string)).toEqual({ focused: true }); }); it("unfocuses the previous session and focuses the new one on a switch", () => { const fetchMock = mockFetch(); const pool = new ConnectionPool(); pool.setFocus(A); fetchMock.mockClear(); pool.setFocus(B); expect(fetchMock).toHaveBeenCalledTimes(2); const bodies = fetchMock.mock.calls.map(([url, init]: unknown[]) => [ url, JSON.parse((init as RequestInit).body as string), ]); expect(bodies).toContainEqual([`/api/sessions/${A}/focus`, { focused: false }]); expect(bodies).toContainEqual([`/api/sessions/${B}/focus`, { focused: true }]); }); it("reports unfocused when focus clears to null", () => { const fetchMock = mockFetch(); const pool = new ConnectionPool(); pool.setFocus(A); fetchMock.mockClear(); pool.setFocus(null); expect(fetchMock).toHaveBeenCalledOnce(); const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; expect(url).toBe(`/api/sessions/${A}/focus`); expect(JSON.parse(init.body as string)).toEqual({ focused: false }); }); it("is a no-op when the focused session doesn't change", () => { const fetchMock = mockFetch(); const pool = new ConnectionPool(); pool.setFocus(A); fetchMock.mockClear(); pool.setFocus(A); expect(fetchMock).not.toHaveBeenCalled(); }); it("does not let a fetch rejection break the pool", async () => { vi.stubGlobal( "fetch", vi.fn(() => Promise.reject(new Error("offline"))), ); const pool = new ConnectionPool(); expect(() => pool.setFocus(A)).not.toThrow(); // Let the rejected promise's `.catch` settle before the test ends. await Promise.resolve(); await Promise.resolve(); }); }); describe("ConnectionPool.sendToFocused", () => { it("types the bytes into the focused session's PTY", () => { mockFetch(); sent.length = 0; const pool = new ConnectionPool(); const pane = document.createElement("div"); pool.mount(A, pane); pool.setFocus(A); expect(pool.sendToFocused("\x0b")).toBe(true); expect(sent).toEqual([{ id: A, data: "\x0b" }]); }); it("is a no-op with nothing focused", () => { mockFetch(); sent.length = 0; const pool = new ConnectionPool(); expect(pool.sendToFocused("\x0b")).toBe(false); expect(sent).toEqual([]); }); });