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

133 lines
4.5 KiB
TypeScript

import { act, renderHook, waitFor } from "@testing-library/react";
import type { PlanComment } from "@/lib/state/slices/comments";
import { describe, expect, it, vi } from "vitest";
import type { Editor } from "@tiptap/core";
import { getAvailableTaskEditor, usePlanSelectionCommentActions } from "./task-plan-panel";
import { usePlanSelection } from "./use-plan-selection";
const TASK_ID = "task-1";
const OTHER_TASK_ID = "task-2";
const primaryComment: PlanComment = {
id: "primary-comment",
sessionId: "",
taskId: TASK_ID,
planId: "plan-1",
version: 1,
source: "plan",
text: "Keep this feedback",
selectedText: "Primary",
from: 1,
to: 8,
createdAt: "2026-09-02T00:00:00.000Z",
status: "pending",
};
describe("plan selection task ownership", () => {
it("keeps comment editing state when only the selected session changes", async () => {
const setEditingCommentId = vi.fn();
const view = renderHook(
({ taskId, sessionId, comments }) => {
void sessionId;
return usePlanSelection(taskId, "plan-1", { comments, setEditingCommentId });
},
{
initialProps: {
taskId: TASK_ID,
sessionId: "session-primary",
comments: [primaryComment],
},
},
);
act(() => {
view.result.current.handleCommentHighlightClick(primaryComment.id, { x: 20, y: 20 });
});
expect(view.result.current.textSelection?.text).toBe(primaryComment.selectedText);
setEditingCommentId.mockClear();
view.rerender({ taskId: TASK_ID, sessionId: "session-secondary", comments: [primaryComment] });
expect(view.result.current.textSelection?.text).toBe(primaryComment.selectedText);
expect(setEditingCommentId).not.toHaveBeenCalled();
view.rerender({ taskId: OTHER_TASK_ID, sessionId: "session-other", comments: [] });
await waitFor(() => expect(view.result.current.textSelection).toBeNull());
expect(setEditingCommentId).toHaveBeenLastCalledWith(null);
});
it("clears comment editing state when the current plan is replaced", async () => {
const setEditingCommentId = vi.fn();
const view = renderHook(
({ planId }) =>
usePlanSelection(TASK_ID, planId, {
comments: [primaryComment],
setEditingCommentId,
}),
{ initialProps: { planId: "plan-1" } },
);
act(() => {
view.result.current.handleCommentHighlightClick(primaryComment.id, { x: 20, y: 20 });
});
setEditingCommentId.mockClear();
view.rerender({ planId: "plan-2" });
await waitFor(() => expect(view.result.current.textSelection).toBeNull());
expect(setEditingCommentId).toHaveBeenLastCalledWith(null);
});
// @covers AC-UI-PLAN-EDITOR-TASK-SWITCH-001.2
// @covers AC-UI-PLAN-EDITOR-TASK-SWITCH-001.5
it("does not expose an editor that belongs to the outgoing task", () => {
const editor = { isDestroyed: false } as Editor;
expect(getAvailableTaskEditor({ taskId: "task-a", editor }, "task-b")).toBeNull();
expect(getAvailableTaskEditor({ taskId: "task-b", editor }, "task-b")).toBe(editor);
});
// @covers AC-UI-PLAN-EDITOR-TASK-SWITCH-001.3
it("does not expose a destroyed editor for the selected task", () => {
const editor = { isDestroyed: true } as Editor;
expect(getAvailableTaskEditor({ taskId: "task-b", editor }, "task-b")).toBeNull();
});
it("reuses an accepted Add and Run comment after its snapshot row disappears", async () => {
const saved = { ...primaryComment };
const handleAddComment = vi.fn().mockResolvedValue(saved);
const runComment = vi
.fn()
.mockRejectedValueOnce(new Error("response lost"))
.mockResolvedValueOnce({ queued: false });
const makeCommentState = (comments: PlanComment[]) =>
({
snapshot: { task_id: TASK_ID, plan_id: "plan-1" },
comments,
handleAddComment,
}) as never;
const view = renderHook(
({ comments }) =>
usePlanSelectionCommentActions({
textSelection: { text: "Primary", from: 1, to: 8, position: { x: 0, y: 0 } },
commentState: makeCommentState(comments),
editor: null,
runComment,
}),
{ initialProps: { comments: [saved] } },
);
await expect(view.result.current.handleAddAndRun(saved.text, saved.selectedText)).resolves.toBe(
false,
);
view.rerender({ comments: [] });
await expect(view.result.current.handleAddAndRun(saved.text, saved.selectedText)).resolves.toBe(
true,
);
expect(handleAddComment).toHaveBeenCalledTimes(1);
expect(runComment).toHaveBeenNthCalledWith(2, saved);
});
});