Ports the vanilla spawn form/modal to src/components/spawn/: SpawnModal, SpawnForm, DirAutocomplete, BranchPicker, and a useFastForward hook for the one-keystroke spawn. Pure logic modules (spawnForm.ts's buildSpawnRequest, fastForward.ts, spawnSimilar.ts, projectList.ts, toolIcon.ts) are reused as-is rather than re-derived. Exports supportsStructuredView from spawnForm.ts so the React form can share the structured-view-eligibility check with the vanilla one.
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import type { DaemonInfo, Project, Session } from "../../types.js";
|
|
import { SpawnModal } from "./SpawnModal.js";
|
|
|
|
vi.mock("../../api.js", () => ({
|
|
createSession: vi.fn(),
|
|
fetchBranches: vi.fn(),
|
|
fetchNames: vi.fn(),
|
|
getDaemonInfo: vi.fn(),
|
|
gitRoot: vi.fn(),
|
|
listDir: vi.fn(),
|
|
projects: vi.fn(),
|
|
}));
|
|
|
|
import { createSession, fetchNames, getDaemonInfo, listDir, projects as fetchProjects } from "../../api.js";
|
|
|
|
function info(): DaemonInfo {
|
|
return { hostname: "h", version: "1", home_dir: "/home/n", presets: [{ id: "claude", label: "Claude", command: "claude" }] };
|
|
}
|
|
|
|
function project(over: Partial<Project> = {}): Project {
|
|
return { path: "/repos/p", name: "p", ...over };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(getDaemonInfo).mockResolvedValue(info());
|
|
vi.mocked(fetchProjects).mockResolvedValue([project()]);
|
|
vi.mocked(fetchNames).mockResolvedValue({ names: ["amber-fox"] });
|
|
vi.mocked(listDir).mockResolvedValue({ path: "", entries: [] });
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
async function settle() {
|
|
await waitFor(() => expect(screen.queryByText("Loading projects…")).toBeNull());
|
|
}
|
|
|
|
describe("SpawnModal", () => {
|
|
it("mounts the spawn form inside a dialog", async () => {
|
|
render(<SpawnModal onSuccess={vi.fn()} onClose={vi.fn()} />);
|
|
await settle();
|
|
|
|
expect(screen.getByRole("dialog")).not.toBeNull();
|
|
expect(screen.getByRole("button", { name: "Spawn" })).not.toBeNull();
|
|
});
|
|
|
|
it("titles a seeded open as a clone", async () => {
|
|
render(<SpawnModal seed={{ command: "claude" }} onSuccess={vi.fn()} onClose={vi.fn()} />);
|
|
await settle();
|
|
|
|
expect(screen.getByText("Spawn similar")).not.toBeNull();
|
|
});
|
|
|
|
it("titles a plain open as a new session", async () => {
|
|
render(<SpawnModal onSuccess={vi.fn()} onClose={vi.fn()} />);
|
|
await settle();
|
|
|
|
expect(screen.getByText("Spawn session")).not.toBeNull();
|
|
});
|
|
|
|
it("closes on Escape, on Cancel and on a backdrop click", async () => {
|
|
const onClose = vi.fn();
|
|
const { unmount } = render(<SpawnModal onSuccess={vi.fn()} onClose={onClose} />);
|
|
await settle();
|
|
|
|
fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
expect(onClose).toHaveBeenCalledTimes(2);
|
|
unmount();
|
|
|
|
render(<SpawnModal onSuccess={vi.fn()} onClose={onClose} />);
|
|
await settle();
|
|
fireEvent.click(document.querySelector(".modal-backdrop")!);
|
|
expect(onClose).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("closes before handing the created session to its caller", async () => {
|
|
const created: Session = {
|
|
id: "s1",
|
|
cwd: "/repos/p",
|
|
command: "claude",
|
|
env: {},
|
|
metadata: {},
|
|
status: "Running",
|
|
created_at: "2026-01-01T00:00:00Z",
|
|
};
|
|
vi.mocked(createSession).mockResolvedValue(created);
|
|
const calls: string[] = [];
|
|
const onClose = vi.fn(() => calls.push("close"));
|
|
const onSuccess = vi.fn(() => calls.push("success"));
|
|
render(<SpawnModal onSuccess={onSuccess} onClose={onClose} />);
|
|
await settle();
|
|
fireEvent.click(screen.getByText("p"));
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Spawn" }));
|
|
|
|
await waitFor(() => expect(onSuccess).toHaveBeenCalledWith(created));
|
|
expect(calls).toEqual(["close", "success"]);
|
|
});
|
|
});
|