// @vitest-environment happy-dom // Enter submits the draft form from any field, at any point (#97). The two // exceptions: a picker button that isn't yet the active choice takes the // Enter to *choose* it instead (the next Enter submits), and the project // filter must resolve to the highlighted project rather than sending // whatever cwd was already set. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createSpawnForm, type SpawnFormHandle } 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", }; 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 OTHER: Project = { path: "/home/user/tea/rev", name: "rev", is_worktree: false, last_activity: "2026-08-19T10:00:00Z", }; function stubFetch(projects: Project[]): void { vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); const body = url.includes("/api/projects") ? projects : url.includes("/api/names") ? { names: ["amber-fox"] } : url.includes("/api/branches") ? [] : url.includes("/api/dirs") ? { path: "/opt", entries: ["nomatch-zzz"], truncated: false } : { hostname: "h", version: "0", home_dir: "/home/user", presets: [] }; return new Response(JSON.stringify(body), { status: 200 }); }), ); } describe("spawnForm Enter-to-submit (#97)", () => { let handle: SpawnFormHandle | null = null; beforeEach(() => { vi.useFakeTimers({ now: Date.parse("2026-08-23T12:00:00Z") }); }); 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 { stubFetch(projects); handle = createSpawnForm({ onSuccess() {} }); document.body.appendChild(handle.el); await drain(); return handle.el; } function submitSpy(): ReturnType { return vi.fn(); } function pressEnter(el: HTMLElement): { defaultPrevented: boolean } { const evt = new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }); el.dispatchEvent(evt); return { defaultPrevented: evt.defaultPrevented }; } const box = (form: HTMLFormElement, path: string) => form.querySelector(`.project-box[data-cwd="${path}"]`)!; it("toggles the worktree checkbox on Enter instead of submitting (TUI parity)", async () => { const form = await mount([MAESTRO]); box(form, MAESTRO.path).click(); await drain(); const toggle = form.querySelector("input[name=worktree_enabled]")!; expect(toggle.checked).toBe(true); const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(toggle); expect(toggle.checked).toBe(false); expect(spy).not.toHaveBeenCalled(); pressEnter(toggle); expect(toggle.checked).toBe(true); expect(spy).not.toHaveBeenCalled(); }); describe("submits immediately from ordinary fields", () => { it("name field", async () => { const form = await mount([MAESTRO]); box(form, MAESTRO.path).click(); await drain(); const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(form.querySelector("input[name=name]")!); expect(spy).toHaveBeenCalledTimes(1); }); it("worktree branch field", async () => { const form = await mount([MAESTRO]); box(form, MAESTRO.path).click(); await drain(); const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(form.querySelector("input[name=worktree_branch]")!); expect(spy).toHaveBeenCalledTimes(1); }); it("custom command field", async () => { const form = await mount([MAESTRO]); box(form, MAESTRO.path).click(); await drain(); form.querySelector('.cmd-tiles .tile[data-id="custom"]')!.click(); const raw = form.querySelector(".cmd-raw")!; raw.value = "bash -l"; raw.dispatchEvent(new Event("input")); const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(raw); expect(spy).toHaveBeenCalledTimes(1); }); it("raw cwd field once the autocomplete dropdown is closed", async () => { const form = await mount([MAESTRO]); form.querySelector(".project-other")!.click(); const raw = form.querySelector("input[name=cwd_raw]")!; raw.value = "/opt/xyz-nothing-here"; raw.dispatchEvent(new Event("input")); await vi.advanceTimersByTimeAsync(300); const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(raw); expect(spy).toHaveBeenCalledTimes(1); }); it("does not submit while the raw cwd autocomplete has a highlighted suggestion", async () => { const form = await mount([MAESTRO]); form.querySelector(".project-other")!.click(); const raw = form.querySelector("input[name=cwd_raw]")!; raw.focus(); raw.value = "/opt/"; raw.dispatchEvent(new Event("input", { bubbles: true })); await vi.advanceTimersByTimeAsync(150); const spy = submitSpy(); form.addEventListener("submit", spy); const { defaultPrevented } = pressEnter(raw); expect(defaultPrevented).toBe(true); expect(spy).not.toHaveBeenCalled(); expect(raw.value).toBe("/opt/nomatch-zzz/"); }); }); describe("two-step picker: first Enter chooses, second submits", () => { it("project box", async () => { const form = await mount([MAESTRO]); const b = box(form, MAESTRO.path); b.focus(); const spy = submitSpy(); form.addEventListener("submit", spy); const first = pressEnter(b); expect(first.defaultPrevented).toBe(true); expect(b.classList.contains("is-active")).toBe(true); expect(spy).not.toHaveBeenCalled(); pressEnter(b); expect(spy).toHaveBeenCalledTimes(1); }); it("worktree choice", async () => { const form = await mount([MAESTRO, WT_A]); box(form, MAESTRO.path).click(); await drain(); const choice = form.querySelector(`.wt-choice[data-cwd="${WT_A.path}"]`)!; expect(choice.classList.contains("is-active")).toBe(false); // "+ new" is the default const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(choice); expect(choice.classList.contains("is-active")).toBe(true); expect(spy).not.toHaveBeenCalled(); pressEnter(choice); expect(spy).toHaveBeenCalledTimes(1); }); }); describe("project filter", () => { it("submits with the highlighted (first matching) project, not the filter text", async () => { const form = await mount([MAESTRO, OTHER]); const filter = form.querySelector(".project-filter")!; filter.value = "rev"; filter.dispatchEvent(new Event("input")); const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(filter); expect(spy).toHaveBeenCalledTimes(1); expect(box(form, OTHER.path).classList.contains("is-active")).toBe(true); }); it("does not clobber an already-chosen worktree when the filter still matches it", async () => { const form = await mount([MAESTRO, WT_A]); box(form, MAESTRO.path).click(); await drain(); form.querySelector(`.wt-choice[data-cwd="${WT_A.path}"]`)!.click(); await drain(); const filter = form.querySelector(".project-filter")!; filter.value = "maestro"; filter.dispatchEvent(new Event("input")); const spy = submitSpy(); form.addEventListener("submit", spy); pressEnter(filter); expect(spy).toHaveBeenCalledTimes(1); expect( form.querySelector(`.wt-choice[data-cwd="${WT_A.path}"]`)!.classList.contains( "is-active", ), ).toBe(true); }); }); });