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.
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import type { BranchInfo } from "../../types.js";
|
|
import { BranchPicker, type BranchPickerProps } from "./BranchPicker.js";
|
|
|
|
afterEach(cleanup);
|
|
|
|
function props(over: Partial<BranchPickerProps> = {}): BranchPickerProps {
|
|
return {
|
|
enabled: true,
|
|
onToggleEnabled: vi.fn(),
|
|
hint: null,
|
|
baseRepo: "/repo",
|
|
mode: "new",
|
|
onModeChange: vi.fn(),
|
|
newBranch: "",
|
|
onNewBranchChange: vi.fn(),
|
|
branches: undefined,
|
|
existingBranch: "",
|
|
onExistingBranchChange: vi.fn(),
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe("BranchPicker", () => {
|
|
it("shows the hint instead of the body while unresolved", () => {
|
|
render(<BranchPicker {...props({ hint: "Choose a working directory first." })} />);
|
|
expect(screen.getByText("Choose a working directory first.")).not.toBeNull();
|
|
expect(screen.queryByText("New branch name")).toBeNull();
|
|
});
|
|
|
|
it("shows the new-branch field by default and the repo line once resolved", () => {
|
|
render(<BranchPicker {...props()} />);
|
|
expect(screen.getByText("repo: /repo")).not.toBeNull();
|
|
expect(screen.getByPlaceholderText("feat/foo")).not.toBeNull();
|
|
expect(screen.queryByText("Pick a branch")).toBeNull();
|
|
});
|
|
|
|
it("switches to the existing-branch select and reports the mode change", () => {
|
|
const onModeChange = vi.fn();
|
|
render(<BranchPicker {...props({ onModeChange })} />);
|
|
|
|
fireEvent.click(screen.getByLabelText("Existing branch"));
|
|
|
|
expect(onModeChange).toHaveBeenCalledWith("existing");
|
|
});
|
|
|
|
it("disables a branch already held by another worktree, and the main checkout's HEAD", () => {
|
|
const branches: BranchInfo[] = [
|
|
{ name: "main", is_head: true },
|
|
{ name: "feat/a", is_head: false, worktree: "/repos/a" },
|
|
{ name: "feat/b", is_head: false },
|
|
];
|
|
render(<BranchPicker {...props({ mode: "existing", branches })} />);
|
|
|
|
const main = screen.getByRole("option", { name: /main — held by the main checkout/ }) as HTMLOptionElement;
|
|
const a = screen.getByRole("option", { name: /feat\/a — held by \/repos\/a/ }) as HTMLOptionElement;
|
|
const b = screen.getByRole("option", { name: "feat/b" }) as HTMLOptionElement;
|
|
expect(main.disabled).toBe(true);
|
|
expect(a.disabled).toBe(true);
|
|
expect(b.disabled).toBe(false);
|
|
});
|
|
|
|
it("shows a placeholder option when no local branches were found", () => {
|
|
render(<BranchPicker {...props({ mode: "existing", branches: [] })} />);
|
|
expect(screen.getByText("No local branches found.")).not.toBeNull();
|
|
});
|
|
|
|
it("toggles the worktree checkbox", () => {
|
|
const onToggleEnabled = vi.fn();
|
|
render(<BranchPicker {...props({ onToggleEnabled })} />);
|
|
|
|
fireEvent.click(screen.getByLabelText("Create git worktree"));
|
|
|
|
expect(onToggleEnabled).toHaveBeenCalledWith(false);
|
|
});
|
|
});
|