The browser app had no tests and no way to run any: package.json shipped dev/build/preview/typecheck, and CI installed Node and pnpm but never ran anything with them. Its densest logic was module-private, so even with a runner none of it was reachable. Extracts four pure modules out of DOM-heavy ones — tree.ts (directory grouping), keys.ts (terminal escape encoding), paths.ts (completion arithmetic), html.ts (one escaper, replacing four copies). Extraction rather than `export` because attach.ts imports xterm's CSS at module level, which a node test cannot load. CatalogStore takes an injectable socket factory, scheduler and refetch, so the reducer and the backoff schedule run without a server. snapshot() now returns a copy: it returned the live object while events mutated it in place, so listeners could not diff and every event rebuilt the whole tree. 36 tests across 6 files, and a `web` CI job running typecheck, test and build. vitest is pinned to 2.x — 4.x requires vite >= 6 and this is vite 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
emptyLayout,
|
|
leaves,
|
|
locateSession,
|
|
openSessions,
|
|
removeLeaf,
|
|
splitLeaf,
|
|
type Tab,
|
|
} from "./layout.js";
|
|
|
|
function tabWithOneLeaf(sessionId: string | null = null): Tab {
|
|
const layout = emptyLayout();
|
|
const tab = layout.tabs[0];
|
|
splitLeaf(tab, "", sessionId, "row", true);
|
|
return tab;
|
|
}
|
|
|
|
describe("splitLeaf", () => {
|
|
it("seeds an empty tab with a single leaf", () => {
|
|
const tab = tabWithOneLeaf("s1");
|
|
expect(leaves(tab.root)).toHaveLength(1);
|
|
expect(leaves(tab.root)[0].sessionId).toBe("s1");
|
|
});
|
|
|
|
it("splits a leaf in two and returns the new id", () => {
|
|
const tab = tabWithOneLeaf("s1");
|
|
const target = leaves(tab.root)[0].id;
|
|
|
|
const fresh = splitLeaf(tab, target, "s2", "row", true);
|
|
|
|
const ids = leaves(tab.root).map((l) => l.id);
|
|
expect(ids).toHaveLength(2);
|
|
expect(ids).toContain(fresh);
|
|
});
|
|
|
|
it("honours the side the caller asked for", () => {
|
|
const tab = tabWithOneLeaf("s1");
|
|
const target = leaves(tab.root)[0].id;
|
|
const fresh = splitLeaf(tab, target, "s2", "row", false);
|
|
|
|
expect(leaves(tab.root)[0].id).toBe(fresh);
|
|
});
|
|
});
|
|
|
|
describe("removeLeaf", () => {
|
|
it("collapses the parent split into the surviving sibling", () => {
|
|
const tab = tabWithOneLeaf("s1");
|
|
const first = leaves(tab.root)[0].id;
|
|
splitLeaf(tab, first, "s2", "row", true);
|
|
|
|
removeLeaf(tab, first);
|
|
|
|
const remaining = leaves(tab.root);
|
|
expect(remaining).toHaveLength(1);
|
|
expect(remaining[0].sessionId).toBe("s2");
|
|
expect(tab.root?.kind).toBe("leaf");
|
|
});
|
|
|
|
it("empties the tab when the last leaf goes", () => {
|
|
const tab = tabWithOneLeaf("s1");
|
|
removeLeaf(tab, leaves(tab.root)[0].id);
|
|
expect(tab.root).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("locateSession / openSessions", () => {
|
|
it("finds the tab and leaf showing a session", () => {
|
|
const layout = emptyLayout();
|
|
const tab = layout.tabs[0];
|
|
splitLeaf(tab, "", "s1", "row", true);
|
|
|
|
const hit = locateSession(layout, "s1");
|
|
expect(hit?.tab.id).toBe(tab.id);
|
|
expect(hit?.leaf.sessionId).toBe("s1");
|
|
expect(locateSession(layout, "nope")).toBeNull();
|
|
});
|
|
|
|
it("reports every open session once", () => {
|
|
const layout = emptyLayout();
|
|
const tab = layout.tabs[0];
|
|
splitLeaf(tab, "", "s1", "row", true);
|
|
const first = leaves(tab.root)[0].id;
|
|
splitLeaf(tab, first, "s2", "row", true);
|
|
|
|
expect(openSessions(layout)).toEqual(new Set(["s1", "s2"]));
|
|
});
|
|
});
|