Files
kandev/apps/web/components/task/plan-selection-popover.test.tsx
T
2026-09-10 22:33:56 +01:00

230 lines
7.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
const touch = vi.hoisted(() => ({ enabled: false }));
vi.mock("@/hooks/use-compact-task-chrome", () => ({
useTouchDrawer: () => touch.enabled,
}));
import { PlanSelectionPopover } from "./plan-selection-popover";
const SAVED_FEEDBACK = "Keep this feedback";
const COMMENT_PLACEHOLDER = "Add your comment or instruction...";
afterEach(() => {
touch.enabled = false;
cleanup();
});
// eslint-disable-next-line max-lines-per-function -- Desktop and touch popover contracts share one render harness.
describe("PlanSelectionPopover", () => {
it("keeps entered feedback open when the submitter rejects the save", () => {
const onClose = vi.fn();
render(
<PlanSelectionPopover
selectedText="settled answer"
position={{ x: 100, y: 100 }}
onAdd={() => false}
onClose={onClose}
/>,
);
const input = screen.getByPlaceholderText(COMMENT_PLACEHOLDER);
fireEvent.change(input, { target: { value: SAVED_FEEDBACK } });
fireEvent.click(screen.getByRole("button", { name: "Add" }));
expect(onClose).not.toHaveBeenCalled();
expect((input as HTMLTextAreaElement).value).toBe(SAVED_FEEDBACK);
});
it("waits for persistence and keeps entered feedback when the async save fails", async () => {
const onClose = vi.fn();
const onAdd = vi.fn().mockResolvedValue(false);
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
onAdd={onAdd}
onClose={onClose}
errorMessage="Could not save this comment. Try again."
/>,
);
const input = screen.getByPlaceholderText(COMMENT_PLACEHOLDER);
fireEvent.change(input, { target: { value: SAVED_FEEDBACK } });
fireEvent.click(screen.getByRole("button", { name: "Add" }));
await waitFor(() => expect(onAdd).toHaveBeenCalled());
expect(onClose).not.toHaveBeenCalled();
expect((input as HTMLTextAreaElement).value).toBe(SAVED_FEEDBACK);
expect(screen.getByRole("alert").textContent).toContain("Could not save this comment");
});
it("disables Run and exposes the primary-session reason", () => {
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
onAdd={() => true}
onAddAndRun={() => true}
runDisabledReason="This task has no primary session available for Run."
onClose={() => undefined}
/>,
);
expect(screen.getByRole("button", { name: "Run" }).hasAttribute("disabled")).toBe(true);
expect(screen.getByText("This task has no primary session available for Run.")).toBeTruthy();
});
it("keeps an edited comment open when async deletion is rejected", async () => {
const onClose = vi.fn();
const onDelete = vi.fn().mockResolvedValue(false);
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
editingComment="Keep this"
onAdd={() => true}
onDelete={onDelete}
onClose={onClose}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Delete comment" }));
await waitFor(() => expect(onDelete).toHaveBeenCalled());
expect(onClose).not.toHaveBeenCalled();
});
it("uses a scroll-contained drawer with touch-sized actions on coarse pointers", () => {
touch.enabled = true;
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
onAdd={() => true}
onAddAndRun={() => true}
onClose={() => undefined}
/>,
);
expect(screen.getByTestId("plan-comment-drawer").className).toContain("max-h-[82dvh]");
expect(screen.getByRole("button", { name: "Add" }).className).toContain("min-h-11");
expect(screen.getByRole("button", { name: "Run" }).className).toContain("min-h-11");
});
it("ignores repeated keyboard submission events", () => {
const onAdd = vi.fn();
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
onAdd={onAdd}
onClose={() => undefined}
/>,
);
const input = screen.getByPlaceholderText(COMMENT_PLACEHOLDER);
fireEvent.change(input, { target: { value: SAVED_FEEDBACK } });
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true, repeat: true });
expect(onAdd).not.toHaveBeenCalled();
});
it("disables delete while an edit submission is pending", async () => {
let finish!: () => void;
const onAdd = vi.fn(() => new Promise<void>((resolve) => (finish = resolve)));
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
editingComment={SAVED_FEEDBACK}
onAdd={onAdd}
onDelete={() => true}
onClose={() => undefined}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Update" }));
await waitFor(() =>
expect(screen.getByRole("button", { name: "Delete comment" }).hasAttribute("disabled")).toBe(
true,
),
);
finish();
});
it("ignores desktop dismissal while a save is pending", async () => {
let finish!: (accepted: boolean) => void;
const onClose = vi.fn();
const onAdd = vi.fn(() => new Promise<boolean>((resolve) => (finish = resolve)));
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
onAdd={onAdd}
onClose={onClose}
/>,
);
const input = screen.getByPlaceholderText(COMMENT_PLACEHOLDER);
fireEvent.change(input, { target: { value: SAVED_FEEDBACK } });
fireEvent.click(screen.getByRole("button", { name: "Add" }));
await waitFor(() => expect(onAdd).toHaveBeenCalled());
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.mouseDown(document.body);
expect(onClose).not.toHaveBeenCalled();
await act(async () => finish(false));
});
it("ignores touch drawer dismissal while a save is pending", async () => {
touch.enabled = true;
let finish!: (accepted: boolean) => void;
const onClose = vi.fn();
const onAdd = vi.fn(() => new Promise<boolean>((resolve) => (finish = resolve)));
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
onAdd={onAdd}
onClose={onClose}
/>,
);
const input = screen.getByPlaceholderText(COMMENT_PLACEHOLDER);
fireEvent.change(input, { target: { value: SAVED_FEEDBACK } });
fireEvent.click(screen.getByRole("button", { name: "Add" }));
await waitFor(() => expect(onAdd).toHaveBeenCalled());
fireEvent.keyDown(document, { key: "Escape" });
expect(onClose).not.toHaveBeenCalled();
await act(async () => finish(false));
});
it("ignores desktop dismissal while deletion is pending", async () => {
let finish!: (accepted: boolean) => void;
const onClose = vi.fn();
const onDelete = vi.fn(() => new Promise<boolean>((resolve) => (finish = resolve)));
render(
<PlanSelectionPopover
selectedText="plan step"
position={{ x: 100, y: 100 }}
editingComment={SAVED_FEEDBACK}
onAdd={() => true}
onDelete={onDelete}
onClose={onClose}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Delete comment" }));
await waitFor(() => expect(onDelete).toHaveBeenCalled());
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.mouseDown(document.body);
expect(onClose).not.toHaveBeenCalled();
await act(async () => finish(false));
});
});