Closes #250: kill confirmation/force-kill (dialogs/KillDialog), the ? shortcuts overlay (dialogs/HelpOverlay), the general-purpose right-click menu (common/ContextMenu, closes on scroll/resize unlike the sidebar's own), the mobile drawer with swipe support (layout/Drawer), and the visual-viewport hook (hooks/useViewport). Each is a standalone, tested port of its vanilla-DOM source; wiring into the shell is a later issue.
75 lines
2.8 KiB
TypeScript
75 lines
2.8 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { KillDialog } from "./KillDialog.js";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
describe("KillDialog", () => {
|
|
it("focuses the kill button, so Enter confirms", () => {
|
|
render(<KillDialog name="alpha" blockers={null} onConfirm={() => Promise.resolve()} onCancel={vi.fn()} />);
|
|
expect(document.activeElement).toBe(screen.getByRole("button", { name: "Kill" }));
|
|
});
|
|
|
|
it("focuses the force button too", () => {
|
|
render(
|
|
<KillDialog
|
|
name="alpha"
|
|
blockers={null}
|
|
force
|
|
reason="worktree is dirty."
|
|
onConfirm={() => Promise.resolve()}
|
|
onCancel={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(document.activeElement).toBe(screen.getByRole("button", { name: "Force kill" }));
|
|
});
|
|
|
|
it("names the blockers in the confirm body", () => {
|
|
render(
|
|
<KillDialog
|
|
name="alpha"
|
|
blockers={{ uncommitted_files: 2, untracked_files: 0, unpushed_commits: 0 }}
|
|
onConfirm={() => Promise.resolve()}
|
|
onCancel={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText(/will be refused unless it is forced/)).not.toBeNull();
|
|
});
|
|
|
|
it("cancels on Cancel, on Escape and on a backdrop click", () => {
|
|
const onCancel = vi.fn();
|
|
const first = render(<KillDialog name="alpha" blockers={null} onConfirm={() => Promise.resolve()} onCancel={onCancel} />);
|
|
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
first.unmount();
|
|
|
|
const second = render(<KillDialog name="alpha" blockers={null} onConfirm={() => Promise.resolve()} onCancel={onCancel} />);
|
|
fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
|
|
expect(onCancel).toHaveBeenCalledTimes(2);
|
|
second.unmount();
|
|
|
|
render(<KillDialog name="alpha" blockers={null} onConfirm={() => Promise.resolve()} onCancel={onCancel} />);
|
|
fireEvent.click(screen.getByRole("dialog"));
|
|
expect(onCancel).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("disables the button while pending and shows a rejection inline, so the press can be retried", async () => {
|
|
let reject: (e: Error) => void = () => {};
|
|
const onConfirm = vi.fn(() => new Promise<void>((_, r) => (reject = r)));
|
|
render(<KillDialog name="alpha" blockers={null} onConfirm={onConfirm} onCancel={vi.fn()} />);
|
|
|
|
const kill = screen.getByRole("button", { name: "Kill" }) as HTMLButtonElement;
|
|
fireEvent.click(kill);
|
|
expect(onConfirm).toHaveBeenCalledOnce();
|
|
expect(kill.disabled).toBe(true);
|
|
|
|
reject(new Error("daemon unreachable"));
|
|
await waitFor(() => expect(kill.disabled).toBe(false));
|
|
expect(screen.getByText("Failed to kill: daemon unreachable")).not.toBeNull();
|
|
});
|
|
});
|