FN-6003: wrap dashboard chat and review tests in async act

Reduce persistent React act() warnings across dashboard chat and review tests.

- add shared async act()-wrapped render helpers in ChatView and TaskReviewTab test files
- convert affected test cases and render helpers to await initial renders before assertions and interactions
- keep existing test coverage while updating room, draft, CLI mount, scroll, and review scenarios for React's async rendering expectations

Files changed:
 .../__tests__/ChatView.cli-mount.test.tsx          |  26 +-
 .../components/__tests__/ChatView.draft.test.tsx   |  30 +-
 .../components/__tests__/ChatView.rooms.test.tsx   |  92 ++--
 .../__tests__/ChatView.scroll-to-top.test.tsx      |  34 +-
 .../app/components/__tests__/ChatView.test.tsx     | 603 +++++++++++----------
 .../components/__tests__/TaskReviewTab.test.tsx    |  79 +--
 6 files changed, 469 insertions(+), 395 deletions(-)

Fusion-Task-Id: FN-6003

Fusion-Task-Lineage: 29866d71-b59f-4441-89df-e21408018385
This commit is contained in:
gsxdsm
2026-06-08 12:19:46 -07:00
parent 8d2f43892c
commit a67ad70bf7
6 changed files with 469 additions and 395 deletions

View File

@@ -7,7 +7,7 @@
// SessionTerminal is mocked (no xterm / no WS / no PTY / no port 4040) because
// CliChatSurface renders it under the hood.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render as rtlRender, screen } from "@testing-library/react";
import { ChatView } from "../ChatView";
import * as useChatModule from "../../hooks/useChat";
import * as useChatRoomsModule from "../../hooks/useChatRooms";
@@ -45,6 +45,14 @@ vi.mock("../../api", async (importOriginal) => {
};
});
async function renderWithAct(ui: Parameters<typeof rtlRender>[0]) {
let result: ReturnType<typeof rtlRender> | undefined;
await act(async () => {
result = rtlRender(ui);
});
return result!;
}
const mockUseChat = vi.mocked(useChatModule.useChat);
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
@@ -110,11 +118,11 @@ describe("ChatView CLI-backed session mount", () => {
mockUseChatRooms.mockReturnValue(defaultRoomsState);
});
it("renders CliChatSurface (transcript/terminal toggle) for a cli-backed session", () => {
it("renders CliChatSurface (transcript/terminal toggle) for a cli-backed session", async () => {
mockUseChat.mockReturnValue(
chatState(makeSession({ cliExecutorAdapterId: "claude-code", cliSessionFile: "cli-native-1" })),
);
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
// CliChatSurface renders the transcript/terminal toggle tablist.
expect(screen.getByRole("tab", { name: /transcript/i })).toBeInTheDocument();
@@ -124,26 +132,26 @@ describe("ChatView CLI-backed session mount", () => {
// existing composer, but the distinguishing CLI toggle is present.
});
it("attaches the terminal to the native cli session id linkage", () => {
it("attaches the terminal to the native cli session id linkage", async () => {
mockUseChat.mockReturnValue(
chatState(makeSession({ cliExecutorAdapterId: "claude-code", cliSessionFile: "cli-native-1" })),
);
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
// Switch to the terminal tab to mount SessionTerminal.
fireEvent.click(screen.getByRole("tab", { name: /terminal/i }));
expect(screen.getByTestId("session-terminal").getAttribute("data-session-id")).toBe("cli-native-1");
});
it("generic-tier cli session renders terminal-only (no toggle)", () => {
it("generic-tier cli session renders terminal-only (no toggle)", async () => {
mockUseChat.mockReturnValue(chatState(makeSession({ cliExecutorAdapterId: "generic" })));
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(screen.getByTestId("session-terminal")).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: /transcript/i })).toBeNull();
});
it("renders the normal provider composer for a regular (non-cli) session", () => {
it("renders the normal provider composer for a regular (non-cli) session", async () => {
mockUseChat.mockReturnValue(chatState(makeSession({ cliExecutorAdapterId: null })));
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
// Normal composer present, CLI toggle absent.
expect(screen.getByPlaceholderText("Type a message...")).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: /transcript/i })).toBeNull();

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { act, render as rtlRender, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { ChatView } from "../ChatView";
import * as useChatModule from "../../hooks/useChat";
@@ -30,6 +30,14 @@ vi.mock("../../api", async (importOriginal) => {
};
});
async function renderWithAct(ui: Parameters<typeof rtlRender>[0]) {
let result: ReturnType<typeof rtlRender> | undefined;
await act(async () => {
result = rtlRender(ui);
});
return result!;
}
const mockUseChat = vi.mocked(useChatModule.useChat);
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
@@ -124,8 +132,8 @@ function mockDesktopViewport() {
}));
}
function renderChatView() {
return render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
async function renderChatView() {
return await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
}
describe("ChatView draft persistence", () => {
@@ -138,7 +146,7 @@ describe("ChatView draft persistence", () => {
});
it("writes direct-session drafts to localStorage while typing", async () => {
renderChatView();
await renderChatView();
await userEvent.type(screen.getByPlaceholderText("Type a message..."), "hello draft");
@@ -147,14 +155,14 @@ describe("ChatView draft persistence", () => {
});
});
it("restores the persisted direct-session draft when remounted", () => {
it("restores the persisted direct-session draft when remounted", async () => {
localStorage.setItem("fusion:chat-draft:direct:session-001", "saved draft");
const { unmount } = renderChatView();
const { unmount } = await renderChatView();
expect(screen.getByPlaceholderText("Type a message...")).toHaveValue("saved draft");
unmount();
renderChatView();
await renderChatView();
expect(screen.getByPlaceholderText("Type a message...")).toHaveValue("saved draft");
});
@@ -162,7 +170,7 @@ describe("ChatView draft persistence", () => {
it("swaps the visible draft when the active direct session changes", async () => {
localStorage.setItem("fusion:chat-draft:direct:session-002", "session two draft");
const { rerender } = renderChatView();
const { rerender } = await renderChatView();
expect(screen.getByPlaceholderText("Type a message...")).toHaveValue("");
setup({
@@ -181,7 +189,7 @@ describe("ChatView draft persistence", () => {
const sendMessage = vi.fn();
setup({ sendMessage });
renderChatView();
await renderChatView();
await userEvent.type(screen.getByPlaceholderText("Type a message..."), "send me");
await userEvent.click(screen.getAllByTestId("chat-send-btn")[0]);
@@ -194,7 +202,7 @@ describe("ChatView draft persistence", () => {
});
it("removes the storage key when the draft becomes empty", async () => {
renderChatView();
await renderChatView();
const textarea = screen.getByPlaceholderText("Type a message...");
await userEvent.type(textarea, "temporary");
@@ -214,7 +222,7 @@ describe("ChatView draft persistence", () => {
localStorage.setItem("fusion:chat-draft:direct:session-001", "direct draft");
localStorage.setItem("fusion:chat-draft:rooms:room-001", "room draft");
renderChatView();
await renderChatView();
const textarea = screen.getByPlaceholderText("Type a message...");
expect(textarea).toHaveValue("room draft");

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { act, fireEvent, render as rtlRender, screen, waitFor, within } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { ChatView } from "../ChatView";
import * as useChatModule from "../../hooks/useChat";
@@ -38,6 +38,14 @@ vi.mock("../../api", async (importOriginal) => {
};
});
async function renderWithAct(ui: Parameters<typeof rtlRender>[0]) {
let result: ReturnType<typeof rtlRender> | undefined;
await act(async () => {
result = rtlRender(ui);
});
return result!;
}
const mockUseChat = vi.mocked(useChatModule.useChat);
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
@@ -249,7 +257,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const roomB = { ...roomA, id: "room-b", name: "Room B", slug: "room-b" };
setup({}, { rooms: [roomA, roomB], selectRoom });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
expect(screen.getByTestId("chat-sidebar-scope-direct")).toBeInTheDocument();
expect(screen.getByTestId("chat-sidebar-scope-rooms")).toBeInTheDocument();
@@ -267,7 +275,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await waitFor(() => {
expect(screen.queryByTestId("chat-message-rmsg-skip")).not.toBeInTheDocument();
@@ -275,10 +283,10 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
});
});
it("shows Create room in mobile footer for Rooms scope and hides New Chat + rooms header", () => {
it("shows Create room in mobile footer for Rooms scope and hides New Chat + rooms header", async () => {
const viewportSpy = mockMobileViewport();
const { container } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const { container } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const createRoomButton = screen.getByTestId("chat-create-room-btn");
expect(createRoomButton.closest(".chat-sidebar-footer")).toBeInTheDocument();
@@ -288,10 +296,10 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
viewportSpy.mockRestore();
});
it("keeps Create room in rooms header on desktop and omits rooms footer", () => {
it("keeps Create room in rooms header on desktop and omits rooms footer", async () => {
const viewportSpy = mockDesktopViewport();
const { container } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const { container } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const createRoomButton = screen.getByTestId("chat-create-room-btn");
expect(createRoomButton.closest(".chat-sidebar-rooms-header")).toBeInTheDocument();
@@ -303,7 +311,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
it.each([
{ memberCount: 1, expectedText: "1 member" },
{ memberCount: 2, expectedText: "2 members" },
])("shows active room member count ($expectedText) and hides inactive meta", ({ memberCount, expectedText }) => {
])("shows active room member count ($expectedText) and hides inactive meta", async ({ memberCount, expectedText }) => {
const roomB = { ...roomA, id: "room-b", name: "Room B", slug: "room-b" };
const activeMembers = Array.from({ length: memberCount }, (_, index) => ({
roomId: roomA.id,
@@ -314,7 +322,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
setup({}, { rooms: [roomA, roomB], activeRoom: roomA, activeRoomMembers: activeMembers });
const { container } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const { container } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const activeRow = screen.getByTestId("chat-room-item-room-a");
const inactiveRow = screen.getByTestId("chat-room-item-room-b");
@@ -331,7 +339,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { createRoom, sendRoomMessage });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await userEvent.click(screen.getByTestId("chat-create-room-btn"));
await userEvent.type(screen.getByLabelText("Room name"), "room-new");
@@ -359,7 +367,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "Room upload{enter}");
@@ -378,7 +386,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockReturnValue(sendPromise);
setup({}, { sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "single send");
@@ -404,7 +412,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
.mockRejectedValueOnce(new RoomMessageDeliveredButReplyFailedError("No active room responders available", "room-a"));
setup({}, { sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "Will retry{enter}");
@@ -423,7 +431,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockRejectedValueOnce(new Error("POST failed"));
setup({}, { sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "Will retry{enter}");
@@ -441,7 +449,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "Delivered{enter}");
@@ -462,7 +470,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockReturnValue(sendPromise);
setup({}, { sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "Optimistic clear{enter}");
@@ -484,7 +492,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { clearRoom, sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, " /clear {enter}");
@@ -500,7 +508,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { clearRoom, sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, " /new {enter}");
@@ -516,7 +524,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { clearRoom, sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "/clear now{enter}");
@@ -532,7 +540,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const clearRoom = vi.fn().mockRejectedValue(new Error("clear failed"));
setup({}, { clearRoom, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "/clear{enter}");
@@ -555,7 +563,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
.mockReturnValueOnce({ ...defaultRoomsState, deleteRoom })
.mockReturnValue(rerenderedRooms);
const { rerender } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const { rerender } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await userEvent.click(screen.getByTestId("chat-room-delete-room-a"));
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
@@ -571,11 +579,11 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
expect(screen.getByText("Updated room reply")).toBeInTheDocument();
});
it("shows mobile back button in room thread view", () => {
it("shows mobile back button in room thread view", async () => {
const mediaSpy = mockMobileViewport();
setup();
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
expect(screen.getByTestId("chat-back-btn")).toBeInTheDocument();
mediaSpy.mockRestore();
@@ -594,7 +602,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
},
);
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const roomInput = screen.getByTestId("chat-input") as HTMLTextAreaElement;
const roomFocusSpy = vi.spyOn(roomInput, "focus");
@@ -633,7 +641,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
},
);
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const input = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await act(async () => {
@@ -689,7 +697,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
],
});
const { unmount } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const { unmount } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await waitFor(() => {
expect(metrics.getScrollTop()).toBe(960);
@@ -698,7 +706,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
metrics.setScrollTop(0);
unmount();
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await waitFor(() => {
expect(metrics.getScrollTop()).toBe(960);
@@ -718,7 +726,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
activeRoom: roomA,
messages: [{ id: "rmsg-1", roomId: roomA.id, role: "assistant", content: "One", createdAt: "2026-04-08T00:00:00.000Z", senderAgentId: "agent-1", mentions: [] }],
});
const { rerender } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const { rerender } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const messagesContainer = document.querySelector(".chat-messages") as HTMLDivElement;
metrics.setScrollTop(980);
@@ -750,7 +758,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
activeRoom: roomA,
messages: [{ id: "rmsg-1", roomId: roomA.id, role: "assistant", content: "One", createdAt: "2026-04-08T00:00:00.000Z", senderAgentId: "agent-1", mentions: [] }],
});
const { rerender } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const { rerender } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const messagesContainer = document.querySelector(".chat-messages") as HTMLDivElement;
metrics.setScrollTop(720);
@@ -784,7 +792,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
messages: [{ id: "rmsg-1", roomId: roomA.id, role: "assistant", content: "One", createdAt: "2026-04-08T00:00:00.000Z", senderAgentId: "agent-1", mentions: [] }],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
Object.defineProperty(document, "visibilityState", { configurable: true, value: "hidden" });
fireEvent(document, new Event("visibilitychange"));
@@ -813,7 +821,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
messages: [{ id: "rmsg-1", roomId: roomA.id, role: "assistant", content: "One", createdAt: "2026-04-08T00:00:00.000Z", senderAgentId: "agent-1", mentions: [] }],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
metrics.setScrollTop(300);
fireEvent(window, new Event("pageshow"));
@@ -837,7 +845,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
messages: [{ id: "rmsg-1", roomId: roomA.id, role: "assistant", content: "One", createdAt: "2026-04-08T00:00:00.000Z", senderAgentId: "agent-1", mentions: [] }],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
metrics.setScrollTop(300);
Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" });
@@ -854,10 +862,10 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
});
describe("room switcher dropdown", () => {
it("renders trigger with active room and menu semantics", () => {
it("renders trigger with active room and menu semantics", async () => {
setup({}, { activeRoom: roomA, rooms: [roomA] });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const trigger = screen.getByTestId("chat-room-switcher-trigger");
expect(trigger).toHaveTextContent("#Room A");
@@ -868,7 +876,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const roomB = { ...roomA, id: "room-b", name: "Room B", slug: "room-b" };
setup({}, { activeRoom: roomA, rooms: [roomA, roomB] });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await userEvent.click(screen.getByTestId("chat-room-switcher-trigger"));
@@ -883,7 +891,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const selectRoom = vi.fn();
setup({}, { activeRoom: roomA, rooms: [roomA, roomB], selectRoom });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await userEvent.click(screen.getByTestId("chat-room-switcher-trigger"));
await userEvent.click(screen.getByTestId("chat-room-switcher-option-room-b"));
@@ -896,7 +904,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const roomB = { ...roomA, id: "room-b", name: "Room B", slug: "room-b" };
setup({}, { activeRoom: roomA, rooms: [roomA, roomB] });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await userEvent.click(screen.getByTestId("chat-room-switcher-trigger"));
fireEvent.keyDown(document, { key: "Escape" });
@@ -908,7 +916,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const roomB = { ...roomA, id: "room-b", name: "Room B", slug: "room-b" };
setup({}, { activeRoom: roomA, rooms: [roomA, roomB] });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await userEvent.click(screen.getByTestId("chat-room-switcher-trigger"));
fireEvent.mouseDown(screen.getByText("Room hello"));
@@ -944,7 +952,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
selectSession,
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
expect(screen.queryByTestId("chat-unread-dot-session-a")).toBeNull();
expect(screen.getByTestId("chat-unread-dot-session-b")).toBeInTheDocument();
@@ -954,7 +962,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
expect(localStorage.getItem("kb:proj-123:fusion:chat-unread:direct")).toContain("session-b");
});
it("renders unread dots for unread rooms", () => {
it("renders unread dots for unread rooms", async () => {
localStorage.setItem("fusion:chat-scope", "rooms");
localStorage.setItem("kb:proj-123:fusion:chat-unread:rooms", JSON.stringify({ "room-a": "2026-04-08T00:00:00.000Z" }));
@@ -968,7 +976,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
setup({}, { rooms: [roomA, roomB], activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
expect(screen.queryByTestId("chat-unread-dot-room-a")).toBeNull();
expect(screen.getByTestId("chat-unread-dot-room-b")).toBeInTheDocument();
@@ -981,7 +989,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
const sendRoomMessage = vi.fn().mockRejectedValue(new Error("Room backend failed"));
setup({ sendMessage }, { sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
await renderWithAct(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input");
await userEvent.type(textarea, "Direct hello{enter}");

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render as rtlRender, screen } from "@testing-library/react";
import { ChatView } from "../ChatView";
import * as useChatModule from "../../hooks/useChat";
import * as useChatRoomsModule from "../../hooks/useChatRooms";
@@ -36,6 +36,14 @@ vi.mock("lucide-react", async (importOriginal) => {
};
});
async function renderWithAct(ui: Parameters<typeof rtlRender>[0]) {
let result: ReturnType<typeof rtlRender> | undefined;
await act(async () => {
result = rtlRender(ui);
});
return result!;
}
const mockUseChat = vi.mocked(useChatModule.useChat);
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
@@ -103,10 +111,10 @@ const defaultRoomsState: UseChatRoomsResult = {
refreshRooms: vi.fn(),
};
function setup(chatOverrides: Partial<UseChatReturn> = {}, roomsOverrides: Partial<UseChatRoomsResult> = {}, experimentalFeatures?: Record<string, boolean>) {
async function setup(chatOverrides: Partial<UseChatReturn> = {}, roomsOverrides: Partial<UseChatRoomsResult> = {}, experimentalFeatures?: Record<string, boolean>) {
mockUseChat.mockReturnValue({ ...defaultChatState, ...chatOverrides });
mockUseChatRooms.mockReturnValue({ ...defaultRoomsState, ...roomsOverrides });
return render(<ChatView addToast={vi.fn()} experimentalFeatures={experimentalFeatures} />);
return await renderWithAct(<ChatView addToast={vi.fn()} experimentalFeatures={experimentalFeatures} />);
}
describe("ChatView scroll-to-top message affordance", () => {
@@ -132,8 +140,8 @@ describe("ChatView scroll-to-top message affordance", () => {
});
});
it("renders on assistant messages and not on user or failed assistant messages", () => {
setup({
it("renders on assistant messages and not on user or failed assistant messages", async () => {
await setup({
messages: [
{ id: "assistant-ok", sessionId: activeSession.id, role: "assistant", content: "hello", createdAt: "2026-04-08T00:00:00.000Z" },
{ id: "assistant-failed", sessionId: activeSession.id, role: "assistant", content: "failed", createdAt: "2026-04-08T00:00:01.000Z", failureInfo: { summary: "oops" } },
@@ -146,8 +154,8 @@ describe("ChatView scroll-to-top message affordance", () => {
expect(screen.queryByTestId("chat-message-scroll-to-top-user-1")).toBeNull();
});
it("scrolls container to message top with smooth behavior", () => {
setup({
it("scrolls container to message top with smooth behavior", async () => {
await setup({
messages: [
{ id: "assistant-ok", sessionId: activeSession.id, role: "assistant", content: "hello", createdAt: "2026-04-08T00:00:00.000Z" },
],
@@ -164,7 +172,7 @@ describe("ChatView scroll-to-top message affordance", () => {
expect(container.scrollTo).toHaveBeenCalledWith({ top: 180, behavior: "smooth" });
});
it("uses auto behavior when reduced motion is preferred", () => {
it("uses auto behavior when reduced motion is preferred", async () => {
vi.mocked(window.matchMedia).mockImplementation((query: string) => ({
matches: query === "(prefers-reduced-motion: reduce)",
media: query,
@@ -176,7 +184,7 @@ describe("ChatView scroll-to-top message affordance", () => {
dispatchEvent: vi.fn(),
}));
setup({
await setup({
messages: [
{ id: "assistant-ok", sessionId: activeSession.id, role: "assistant", content: "hello", createdAt: "2026-04-08T00:00:00.000Z" },
],
@@ -195,8 +203,8 @@ describe("ChatView scroll-to-top message affordance", () => {
expect(container.scrollTo).toHaveBeenCalledWith({ top: 120, behavior: "auto" });
});
it("renders the affordance for room assistant messages", () => {
setup(
it("renders the affordance for room assistant messages", async () => {
await setup(
{
sessions: [activeSession],
activeSession,
@@ -214,7 +222,7 @@ describe("ChatView scroll-to-top message affordance", () => {
expect(screen.getByTestId("chat-message-scroll-to-top-room-assistant-1")).toBeInTheDocument();
});
it("does not reset to top when a stale zero snapshot is captured while user is reading older messages", () => {
it("does not reset to top when a stale zero snapshot is captured while user is reading older messages", async () => {
const state: UseChatReturn = {
...defaultChatState,
messages: [
@@ -226,7 +234,7 @@ describe("ChatView scroll-to-top message affordance", () => {
mockUseChat.mockImplementation(() => state);
mockUseChatRooms.mockReturnValue(defaultRoomsState);
const { rerender } = render(<ChatView addToast={vi.fn()} />);
const { rerender } = await renderWithAct(<ChatView addToast={vi.fn()} />);
const container = document.querySelector(".chat-messages") as HTMLDivElement;
let scrollTopValue = 600;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { act, render as rtlRender, screen, fireEvent, waitFor } from "@testing-library/react";
import { TaskReviewTab } from "../TaskReviewTab";
import { makeTask } from "./TaskDetailModal.test-helpers";
import { loadAllAppCss } from "../../test/cssFixture";
@@ -20,6 +20,14 @@ vi.mock("../../api", () => ({
updateTask: apiMocks.updateTask,
}));
async function renderWithAct(ui: Parameters<typeof rtlRender>[0]) {
let result: ReturnType<typeof rtlRender> | undefined;
await act(async () => {
result = rtlRender(ui);
});
return result!;
}
describe("TaskReviewTab", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -33,7 +41,7 @@ describe("TaskReviewTab", () => {
emptyMessage: "No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode.",
});
render(<TaskReviewTab task={makeTask({ reviewState: undefined })} addToast={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={makeTask({ reviewState: undefined })} addToast={vi.fn()} />);
expect(await screen.findByText("No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Request revision" })).toBeDisabled();
});
@@ -52,7 +60,7 @@ describe("TaskReviewTab", () => {
},
automationStatus: null,
});
render(<TaskReviewTab task={task} addToast={addToast} />);
await renderWithAct(<TaskReviewTab task={task} addToast={addToast} />);
fireEvent.click(await screen.findByRole("button", { name: "Refresh" }));
expect(apiMocks.refreshTaskReview).toHaveBeenCalledWith(task.id, undefined);
expect(await screen.findByText("APPROVED")).toBeInTheDocument();
@@ -70,7 +78,7 @@ describe("TaskReviewTab", () => {
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
apiMocks.refreshTaskReview.mockReturnValue(refreshPromise as Promise<never>);
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
fireEvent.click(await screen.findByRole("button", { name: "Refresh" }));
expect(screen.getByRole("button", { name: "Refreshing…" })).toBeDisabled();
@@ -93,7 +101,7 @@ describe("TaskReviewTab", () => {
prInfo: task.prInfo,
});
render(<TaskReviewTab task={task} addToast={addToast} />);
await renderWithAct(<TaskReviewTab task={task} addToast={addToast} />);
fireEvent.click(await screen.findByRole("button", { name: "Refresh" }));
@@ -112,7 +120,7 @@ describe("TaskReviewTab", () => {
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("No review items yet.")).toBeInTheDocument();
});
@@ -120,7 +128,7 @@ describe("TaskReviewTab", () => {
it("shows load error when initial review fetch fails", async () => {
apiMocks.fetchTaskReview.mockRejectedValue(new Error("boom"));
render(<TaskReviewTab task={makeTask()} addToast={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={makeTask()} addToast={vi.fn()} />);
expect(await screen.findByText("Failed to load review data.")).toBeInTheDocument();
});
@@ -145,7 +153,7 @@ describe("TaskReviewTab", () => {
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
await screen.findByText("CHANGES_REQUESTED");
expect(screen.getByText("failed").className).toContain("task-review-tab__status--failed");
@@ -181,7 +189,7 @@ describe("TaskReviewTab", () => {
window.localStorage.setItem(REVIEW_MARKDOWN_TOGGLE_STORAGE_KEY, "false");
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
const checkbox = await screen.findByRole("checkbox");
expect(checkbox).not.toBeChecked();
@@ -215,7 +223,7 @@ describe("TaskReviewTab", () => {
window.localStorage.setItem(REVIEW_MARKDOWN_TOGGLE_STORAGE_KEY, "true");
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
const checkbox = await screen.findByRole("checkbox");
const link = await screen.findByRole("link", { name: "example" });
@@ -247,9 +255,10 @@ describe("TaskReviewTab", () => {
window.localStorage.setItem(REVIEW_MARKDOWN_TOGGLE_STORAGE_KEY, "false");
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
await screen.findByText("Plain mode item");
await act(async () => {});
const body = container.querySelector("pre.task-review-tab__body");
expect(body).not.toBeNull();
expect(body?.closest("label")).toBeNull();
@@ -275,7 +284,7 @@ describe("TaskReviewTab", () => {
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container, unmount } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container, unmount } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
await screen.findByText("Markdown body");
expect(container.querySelector("strong")?.textContent).toBe("bold");
@@ -287,7 +296,7 @@ describe("TaskReviewTab", () => {
expect(container.querySelector("strong")).toBeNull();
unmount();
const rerendered = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const rerendered = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
await screen.findByText("Markdown body");
await waitFor(() => expect(rerendered.container.querySelector("pre.task-review-tab__body")?.textContent).toContain("**bold**"));
@@ -320,10 +329,12 @@ describe("TaskReviewTab", () => {
apiMocks.reviseTaskReviewItems.mockResolvedValue({ task, reviewState: task.reviewState });
apiMocks.refreshTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null });
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
fireEvent.click(await screen.findByRole("checkbox"));
fireEvent.click(screen.getByRole("button", { name: "Request revision" }));
await act(async () => {
fireEvent.click(await screen.findByRole("checkbox"));
fireEvent.click(screen.getByRole("button", { name: "Request revision" }));
});
expect(apiMocks.reviseTaskReviewItems).toHaveBeenCalledWith(task.id, [expect.objectContaining({
id: "ri-1",
@@ -372,7 +383,7 @@ describe("TaskReviewTab", () => {
automationStatus: null,
});
render(<TaskReviewTab task={task} addToast={addToast} />);
await renderWithAct(<TaskReviewTab task={task} addToast={addToast} />);
fireEvent.click(await screen.findByRole("button", { name: "Refresh" }));
@@ -405,7 +416,7 @@ describe("TaskReviewTab", () => {
emptyMessage: null,
});
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("code review Step 2: REVISE")).toBeInTheDocument();
expect(screen.getAllByText("REVISE").length).toBeGreaterThan(0);
expect(container.querySelector(".task-review-tab__item-header")).not.toBeNull();
@@ -454,7 +465,7 @@ describe("TaskReviewTab", () => {
emptyMessage: null,
});
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("queued item")).toBeInTheDocument();
expect(screen.getByText("in progress item")).toBeInTheDocument();
@@ -494,7 +505,7 @@ describe("TaskReviewTab", () => {
emptyMessage: null,
});
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("Fix edge case")).toBeInTheDocument();
expect(screen.getByText(/Error: Patch failed/)).toBeInTheDocument();
});
@@ -593,7 +604,7 @@ describe("TaskReviewTab", () => {
.mockResolvedValueOnce(cases[2].response)
.mockResolvedValueOnce(cases[3].response);
const { container, rerender } = render(<TaskReviewTab task={cases[0].task} addToast={vi.fn()} />);
const { container, rerender } = await renderWithAct(<TaskReviewTab task={cases[0].task} addToast={vi.fn()} />);
for (const [index, testCase] of cases.entries()) {
if (index > 0) {
@@ -646,7 +657,7 @@ describe("TaskReviewTab", () => {
const task = makeTask({ column: "in-review", prInfo: undefined, autoMerge: taskAutoMerge });
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
render(
await renderWithAct(
<TaskReviewTab
task={task}
addToast={vi.fn()}
@@ -672,7 +683,7 @@ describe("TaskReviewTab", () => {
const task = makeTask({ column: "todo", prInfo: undefined });
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
render(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable onRequestCreatePr={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable onRequestCreatePr={vi.fn()} />);
await screen.findByRole("button", { name: "Refresh" });
expect(screen.queryByTestId("task-review-create-pr")).toBeNull();
@@ -692,7 +703,7 @@ describe("TaskReviewTab", () => {
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
render(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable onRequestCreatePr={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable onRequestCreatePr={vi.fn()} />);
await screen.findByRole("button", { name: "Refresh" });
expect(screen.queryByTestId("task-review-create-pr")).toBeNull();
@@ -702,7 +713,7 @@ describe("TaskReviewTab", () => {
const task = makeTask({ column: "in-review", prInfo: undefined });
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
render(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable={false} onRequestCreatePr={vi.fn()} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable={false} onRequestCreatePr={vi.fn()} />);
await screen.findByRole("button", { name: "Refresh" });
expect(screen.queryByTestId("task-review-create-pr")).toBeNull();
@@ -712,7 +723,7 @@ describe("TaskReviewTab", () => {
const task = makeTask({ column: "in-review", prInfo: undefined, autoMerge: undefined });
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
render(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable onRequestCreatePr={vi.fn()} autoMergeEnabled />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} prAuthAvailable onRequestCreatePr={vi.fn()} autoMergeEnabled />);
await screen.findByRole("button", { name: "Refresh" });
expect(screen.queryByTestId("task-review-create-pr")).toBeNull();
@@ -732,9 +743,11 @@ describe("TaskReviewTab", () => {
});
apiMocks.reviseTaskReviewItems.mockResolvedValue({ task: makeTask(), reviewState: { source: "reviewer-agent", items: [], addressing: [] } });
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
fireEvent.click(await screen.findByRole("checkbox"));
fireEvent.click(screen.getByRole("button", { name: "Request revision" }));
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
await act(async () => {
fireEvent.click(await screen.findByRole("checkbox"));
fireEvent.click(screen.getByRole("button", { name: "Request revision" }));
});
expect(apiMocks.reviseTaskReviewItems).toHaveBeenCalledWith(task.id, [expect.objectContaining({ id: "reviewer-code-1", source: "reviewer-agent" })], undefined);
});
@@ -748,7 +761,7 @@ describe("TaskReviewTab", () => {
apiMocks.updateTask.mockResolvedValueOnce({ ...task, autoMerge: false });
apiMocks.updateTask.mockResolvedValueOnce({ ...task, autoMerge: undefined });
render(<TaskReviewTab task={task} addToast={vi.fn()} onTaskUpdated={onTaskUpdated} />);
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} onTaskUpdated={onTaskUpdated} />);
const select = await screen.findByTestId("task-review-auto-merge-select");
fireEvent.change(select, { target: { value: "on" } });
@@ -767,7 +780,7 @@ describe("TaskReviewTab", () => {
const inReviewTask = makeTask({ column: "in-review", autoMerge: undefined, reviewState: { source: "pull-request", items: [], addressing: [] } });
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: inReviewTask.reviewState, automationStatus: null, emptyMessage: null });
const { rerender } = render(<TaskReviewTab task={inReviewTask} addToast={vi.fn()} autoMergeEnabled />);
const { rerender } = await renderWithAct(<TaskReviewTab task={inReviewTask} addToast={vi.fn()} autoMergeEnabled />);
expect(await screen.findByTestId("task-review-auto-merge-effective-hint")).toHaveTextContent("Effective: Auto-merge on — frozen on entry to review");
rerender(<TaskReviewTab task={inReviewTask} addToast={vi.fn()} autoMergeEnabled={false} />);
@@ -778,7 +791,7 @@ describe("TaskReviewTab", () => {
const inReviewTask = makeTask({ column: "in-review", autoMerge: undefined, reviewState: { source: "pull-request", items: [], addressing: [] } });
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: inReviewTask.reviewState, automationStatus: null, emptyMessage: null });
const { rerender } = render(
const { rerender } = await renderWithAct(
<TaskReviewTab
task={inReviewTask}
addToast={vi.fn()}
@@ -819,7 +832,7 @@ describe("TaskReviewTab", () => {
const task = makeTask({ autoMerge: true, reviewState: { source: "pull-request", items: [], addressing: [] } });
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { rerender } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
const { rerender } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByTestId("task-review-auto-merge-select")).toHaveValue("on");
rerender(<TaskReviewTab task={makeTask({ autoMerge: false, reviewState: { source: "pull-request", items: [], addressing: [] } })} addToast={vi.fn()} />);