A session's rail glows AwaitingInput even after the operator has already looked at it. Clients now report pane focus (POST /:id/focus); the daemon clears the glow to Idle after 5s of continuous focus and keeps it cleared until the session does new work, so a fresh Active cycle is required to glow again.
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { ConnectionPool } from "./connections.js";
|
|
|
|
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();
|
|
});
|
|
});
|