// @vitest-environment happy-dom import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { emptyLayout, leaves, type Layout } from "./layout.js"; import { splitLeaf } from "./layout.js"; import { createMultiView, type MultiView } from "./multiview.js"; import type { ConnectionPool } from "./connections.js"; import type { SidebarHandle } from "./catalog.js"; function stubFetch(): void { vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); const body = url.includes("/api/info") ? { hostname: "h", version: "0", home_dir: "/home/user", presets: [] } : url.includes("/api/projects") ? [] : url.includes("/api/names") ? { names: ["allegro-cello"] } : undefined; return new Response(JSON.stringify(body), { status: 200 }); }), ); } function fakePool(): ConnectionPool { return { setVisible() {}, setFocus() {}, } as unknown as ConnectionPool; } function fakeSidebar(): SidebarHandle { return { el: document.createElement("aside"), setActive() {}, setHighlights() {}, dispose() {}, }; } function makeMv(layout: Layout): MultiView { return createMultiView({ pool: fakePool(), layout, sidebar: fakeSidebar(), infoFor: () => ({ name: "sess", command: "bash" }), onDisconnect: () => {}, renderWelcome: () => document.createElement("div"), }); } /** Drain the microtask queue so the draft form's async seed application lands. */ async function flush(): Promise { for (let i = 0; i < 20; i++) await Promise.resolve(); } describe("multiview openDraft (#62)", () => { beforeEach(() => { stubFetch(); document.body.replaceChildren(); }); afterEach(() => { vi.unstubAllGlobals(); }); it("turns an empty tab into a draft pane", async () => { const layout = emptyLayout(); const mv = makeMv(layout); document.body.appendChild(mv.el); mv.openDraft(); await flush(); const ls = leaves(layout.tabs[0].root); expect(ls).toHaveLength(1); expect(ls[0].sessionId).toBeNull(); expect(ls[0].draft).not.toBeNull(); expect(mv.el.querySelector(".mv-draft")).toBeTruthy(); }); it("splits the focused session pane into a fresh draft", async () => { const layout = emptyLayout(); const tab = layout.tabs[0]; const first = splitLeaf(tab, "", "s1", "row", true); tab.focusedLeafId = first; const mv = makeMv(layout); document.body.appendChild(mv.el); mv.render(); mv.openDraft(); await flush(); const ls = leaves(tab.root); expect(ls).toHaveLength(2); expect(ls[0].sessionId).toBe("s1"); const draftLeaf = ls.find((l) => l.sessionId === null)!; expect(draftLeaf.draft).not.toBeNull(); expect(tab.focusedLeafId).toBe(draftLeaf.id); expect(mv.el.querySelectorAll(".mv-pane")).toHaveLength(2); }); it("reuses the focused pane when it is already empty", async () => { const layout = emptyLayout(); const tab = layout.tabs[0]; const first = splitLeaf(tab, "", "s1", "row", true); const empty = splitLeaf(tab, first, null, "row", true); tab.focusedLeafId = empty; const mv = makeMv(layout); document.body.appendChild(mv.el); mv.render(); mv.openDraft(); await flush(); const ls = leaves(tab.root); expect(ls).toHaveLength(2); const target = ls.find((l) => l.id === empty)!; expect(target.draft).not.toBeNull(); expect(tab.focusedLeafId).toBe(empty); }); it("keeps the draft form instance when an unrelated session closes (#68)", async () => { const layout = emptyLayout(); const tab = layout.tabs[0]; const s1 = splitLeaf(tab, "", "s1", "row", true); const draftLeaf = splitLeaf(tab, s1, null, "row", true); tab.focusedLeafId = draftLeaf; const mv = makeMv(layout); document.body.appendChild(mv.el); mv.render(); await flush(); const nameInput = mv.el.querySelector(".mv-draft input[name=name]") as HTMLInputElement; nameInput.value = "wip"; nameInput.dispatchEvent(new Event("input")); await flush(); expect(leaves(tab.root).find((l) => l.id === draftLeaf)?.draft?.name).toBe("wip"); const formBefore = mv.el.querySelector(".mv-draft"); mv.closeSession("s1"); await flush(); expect(mv.el.querySelector(".mv-draft")).toBe(formBefore); expect((mv.el.querySelector(".mv-draft input[name=name]") as HTMLInputElement).value).toBe("wip"); }); it("reuses an already-synced empty pane instead of splitting (#72)", async () => { const layout = emptyLayout(); const tab = layout.tabs[0]; const first = splitLeaf(tab, "", "s1", "row", true); const empty = splitLeaf(tab, first, null, "row", true); tab.focusedLeafId = empty; const mv = makeMv(layout); document.body.appendChild(mv.el); mv.render(); // Let the mounted form's loadProjects() settle and auto-sync its picked // defaults into leaf.draft before openDraft runs (#72's dead-branch window). await flush(); expect(leaves(tab.root).find((l) => l.id === empty)?.draft).not.toBeNull(); mv.openDraft(); await flush(); expect(leaves(tab.root)).toHaveLength(2); expect(tab.focusedLeafId).toBe(empty); }); it("keeps a seed after reusing an already-synced empty pane (#72)", async () => { const layout = emptyLayout(); const tab = layout.tabs[0]; const first = splitLeaf(tab, "", "s1", "row", true); const empty = splitLeaf(tab, first, null, "row", true); tab.focusedLeafId = empty; const mv = makeMv(layout); document.body.appendChild(mv.el); mv.render(); await flush(); mv.openDraft({ cwd: "/home/user/proj", command: "claude" }); await flush(); const leaf = leaves(tab.root).find((l) => l.id === empty)!; expect(leaf.draft?.cwd).toBe("/home/user/proj"); expect(leaf.draft?.command).toBe("claude"); const cwdRaw = mv.el.querySelector(".mv-draft .cwd-raw") as HTMLInputElement; expect(cwdRaw.value).toBe("/home/user/proj"); }); it("seeds a clone's cwd and command into the draft", async () => { const layout = emptyLayout(); const mv = makeMv(layout); document.body.appendChild(mv.el); mv.openDraft({ cwd: "/home/user/proj", command: "claude" }); await flush(); const leaf = leaves(layout.tabs[0].root)[0]; expect(leaf.draft?.cwd).toBe("/home/user/proj"); expect(leaf.draft?.command).toBe("claude"); }); });