openDraft's reuse check was `!target.draft`, dead after first render: createSpawnForm auto-syncs its picked defaults into the leaf's draft once its project load settles, so every empty pane has a non-null draft a second later. `n` and Spawn always split instead of reusing the pane on screen. Reuse now checks only `!target.sessionId`, matching the codebase's own definition of "empty" pane elsewhere. Reusing also drops the leaf's stale form instance so a new seed is read by the rebuilt form instead of being ignored by the old one.
205 lines
6.4 KiB
TypeScript
205 lines
6.4 KiB
TypeScript
// @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<void> {
|
|
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");
|
|
});
|
|
});
|