// @vitest-environment happy-dom // Second step of the working-directory picker (#95). See the interaction rules // block in spawnForm.ts for what each choice means. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createSpawnForm, type SpawnFormHandle, type SpawnFormValues } from "./spawnForm.js"; import type { Project } from "./types.js"; const MAESTRO: Project = { path: "/home/user/tea/maestro", name: "maestro", is_worktree: false, last_activity: "2026-08-20T10:00:00Z", last_command: "claude", }; const WT_A: Project = { path: "/home/user/tea/maestro/worktrees/bulgarians", name: "bulgarians", is_worktree: true, base_repo: MAESTRO.path, last_activity: "2026-08-22T10:00:00Z", }; const WT_B: Project = { path: "/home/user/tea/maestro/worktrees/spawn-v2", name: "spawn-v2", is_worktree: true, base_repo: MAESTRO.path, last_activity: "2026-08-21T10:00:00Z", }; const OTHER: Project = { path: "/home/user/tea/rev", name: "rev", is_worktree: false, last_activity: "2026-08-19T10:00:00Z", }; function stubFetch(projects: Project[]): string[] { const urls: string[] = []; vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); urls.push(url); const body = url.includes("/api/projects") ? projects : url.includes("/api/names") ? { names: ["amber-fox"] } : url.includes("/api/branches") ? [] : { hostname: "h", version: "0", home_dir: "/home/user", presets: [] }; return new Response(JSON.stringify(body), { status: 200 }); }), ); return urls; } describe("spawnForm worktree step (#95)", () => { let handle: SpawnFormHandle | null = null; let urls: string[] = []; let changes: SpawnFormValues[] = []; beforeEach(() => { vi.useFakeTimers({ now: Date.parse("2026-08-23T12:00:00Z") }); changes = []; }); afterEach(() => { handle?.destroy(); handle = null; vi.unstubAllGlobals(); vi.useRealTimers(); }); const drain = async () => { for (let i = 0; i < 40; i++) await Promise.resolve(); }; async function mount(projects: Project[]): Promise { urls = stubFetch(projects); handle = createSpawnForm({ onSuccess() {}, onChange: (v) => changes.push(v) }); document.body.appendChild(handle.el); await drain(); return handle.el; } const step = (form: HTMLFormElement) => form.querySelector(".wt-step")!; const choices = (form: HTMLFormElement) => [ ...form.querySelectorAll(".wt-choice"), ]; const activeChoice = (form: HTMLFormElement) => form.querySelector(".wt-choice.is-active"); const box = (form: HTMLFormElement, path: string) => form.querySelector(`.project-box[data-cwd="${path}"]`)!; const toggle = (form: HTMLFormElement) => form.querySelector("input[name=worktree_enabled]")!; it("hides the worktree step until a project is picked", async () => { const form = await mount([MAESTRO, WT_A]); expect(step(form).hidden).toBe(true); expect(choices(form)).toEqual([]); box(form, MAESTRO.path).click(); await drain(); expect(step(form).hidden).toBe(false); }); it("keeps the worktree step inside the collapsed Advanced options section", async () => { const form = await mount([MAESTRO, WT_A]); box(form, MAESTRO.path).click(); await drain(); const collapsible = form.querySelector(".wt-collapsible")!; expect(collapsible.contains(step(form))).toBe(true); expect(collapsible.hidden).toBe(true); }); it("lists '+ new' first and selected by default, then the project's worktrees", async () => { const form = await mount([MAESTRO, WT_A, WT_B]); box(form, MAESTRO.path).click(); await drain(); const labels = choices(form).map((c) => c.querySelector(".wt-choice-name")!.textContent); expect(labels).toEqual(["+ new", "bulgarians", "spawn-v2"]); expect(activeChoice(form)!.dataset.kind).toBe("new"); }); it("taking the '+ new' default runs in the repo with the create toggle on", async () => { const form = await mount([MAESTRO, WT_A]); box(form, MAESTRO.path).click(); await drain(); expect(changes.at(-1)!.cwd).toBe(MAESTRO.path); expect(changes.at(-1)!.worktreeEnabled).toBe(true); expect(toggle(form).checked).toBe(true); expect(form.querySelector(".wt-repo")!.textContent).toContain(MAESTRO.path); // The scan already knows the repo — no probe. expect(urls.some((u) => u.includes("/api/gitroot"))).toBe(false); }); it("picking an existing worktree runs in it with the create toggle off (#91)", async () => { const form = await mount([MAESTRO, WT_A, WT_B]); box(form, MAESTRO.path).click(); await drain(); const existing = choices(form).find((c) => c.dataset.cwd === WT_A.path)!; existing.click(); await drain(); expect(changes.at(-1)!.cwd).toBe(WT_A.path); expect(changes.at(-1)!.worktreeEnabled).toBe(false); expect(toggle(form).checked).toBe(false); expect(form.querySelector(".wt-body")!.hidden).toBe(true); expect(existing.classList.contains("is-active")).toBe(true); // Still no probe: the scan reported this worktree's repo membership. expect(urls.some((u) => u.includes("/api/gitroot"))).toBe(false); // The project box stays marked — it is what the worktree list belongs to. expect(box(form, MAESTRO.path).classList.contains("is-active")).toBe(true); }); it("switching back to '+ new' re-enables the create toggle", async () => { const form = await mount([MAESTRO, WT_A]); box(form, MAESTRO.path).click(); await drain(); choices(form) .find((c) => c.dataset.cwd === WT_A.path)! .click(); await drain(); choices(form) .find((c) => c.dataset.kind === "new")! .click(); await drain(); expect(changes.at(-1)!.cwd).toBe(MAESTRO.path); expect(changes.at(-1)!.worktreeEnabled).toBe(true); expect(toggle(form).checked).toBe(true); }); it("repopulates and resets to '+ new' when another project is picked", async () => { const form = await mount([MAESTRO, WT_A, OTHER]); box(form, MAESTRO.path).click(); await drain(); choices(form) .find((c) => c.dataset.cwd === WT_A.path)! .click(); await drain(); box(form, OTHER.path).click(); await drain(); expect(choices(form).map((c) => c.dataset.kind)).toEqual(["new"]); expect(activeChoice(form)!.dataset.kind).toBe("new"); expect(changes.at(-1)!.cwd).toBe(OTHER.path); expect(changes.at(-1)!.worktreeEnabled).toBe(true); }); it("defaults to the worktree itself when the box is a linked worktree (#91)", async () => { // No main checkout in the scan, so the orphan worktree is its own box. const form = await mount([WT_A]); box(form, WT_A.path).click(); await drain(); expect(activeChoice(form)!.dataset.cwd).toBe(WT_A.path); expect(changes.at(-1)!.cwd).toBe(WT_A.path); expect(changes.at(-1)!.worktreeEnabled).toBe(false); expect(toggle(form).checked).toBe(false); expect(form.querySelector(".wt-body")!.hidden).toBe(true); }); it("hides the worktree step again when the operator takes the raw-input hatch", async () => { const form = await mount([MAESTRO, WT_A]); box(form, MAESTRO.path).click(); await drain(); expect(step(form).hidden).toBe(false); form.querySelector(".project-other")!.click(); await drain(); expect(step(form).hidden).toBe(true); expect(choices(form)).toEqual([]); expect(form.querySelector(".project-box.is-active")).toBeNull(); }); it("restores the picked worktree from a draft seed", async () => { urls = stubFetch([MAESTRO, WT_A, WT_B]); handle = createSpawnForm({ onSuccess() {}, seed: { cwd: WT_A.path, cwdRawVisible: false, worktreeEnabled: false }, }); document.body.appendChild(handle.el); await drain(); const form = handle.el; expect(step(form).hidden).toBe(false); expect(activeChoice(form)!.dataset.cwd).toBe(WT_A.path); expect(box(form, MAESTRO.path).classList.contains("is-active")).toBe(true); expect(toggle(form).checked).toBe(false); }); it("lands a 'spawn similar' seed on the cloned project with '+ new' active", async () => { urls = stubFetch([MAESTRO, WT_A, WT_B]); handle = createSpawnForm({ onSuccess() {}, seed: { cwd: MAESTRO.path, cwdRawVisible: false, worktreeEnabled: true }, }); document.body.appendChild(handle.el); await drain(); const form = handle.el; expect(box(form, MAESTRO.path).classList.contains("is-active")).toBe(true); expect(activeChoice(form)!.dataset.kind).toBe("new"); expect(toggle(form).checked).toBe(true); expect(form.querySelector(".cwd-raw-wrap")!.hidden).toBe(true); }); it("falls back to the raw input when the seeded directory matches no project", async () => { urls = stubFetch([MAESTRO, WT_A]); handle = createSpawnForm({ onSuccess() {}, seed: { cwd: "/home/user/gone", cwdRawVisible: false, worktreeEnabled: false }, }); document.body.appendChild(handle.el); await drain(); const form = handle.el; const raw = form.querySelector(".cwd-raw")!; expect(form.querySelector(".cwd-raw-wrap")!.hidden).toBe(false); expect(raw.value).toBe("/home/user/gone"); }); });