Files
fusion/packages/dashboard/app/components/__tests__/PendingChatMessageQueue.test.tsx
Fusion Agent 21b3baa62d FN-029: unify direct and planner chat queues
Standardize queued-message controls across Direct Chat and planner conversations while preserving force-send ordering during cancellation reconciliation.

- Add shared pending-message queue UI with edit, reorder, delete, and force-send actions.
- Coordinate durable cancellation reconciliation before dispatching selected queued messages.
- Disable the Direct composer and related controls until force-send reconciliation completes.
- Add localized queue copy, documentation, styling, and regression coverage.

Files changed:
 docs/dashboard-guide.md                            |   8 +
 packages/dashboard/app/components/ChatView.css     |  63 ------
 packages/dashboard/app/components/ChatView.tsx     |  55 +++--
 packages/dashboard/app/components/PendingChatMessageQueue.css     |  94 +++++++++
 packages/dashboard/app/components/PendingChatMessageQueue.tsx     | 200 ++++++++++++++++++
 packages/dashboard/app/components/TaskPlannerChatTab.css          |  84 --------
 packages/dashboard/app/components/TaskPlannerChatTab.tsx          | 113 ++---------
 packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx |  24 ++-
 packages/dashboard/app/components/__tests__/PendingChatMessageQueue.test.tsx   |  76 +++++++
 packages/dashboard/app/hooks/__tests__/useChat.test.ts            |  39 +++
 packages/dashboard/app/hooks/useChat.ts                           | 224 ++++++++++++++++-----
 packages/i18n/locales/en/app.json                                 |  25 +--
 packages/i18n/locales/es/app.json                                 |  25 +--
 packages/i18n/locales/fr/app.json                                 |  25 +--
 packages/i18n/locales/ko/app.json                                 |  25 +--
 packages/i18n/locales/pt-BR/app.json                              |  25 +--
 packages/i18n/locales/zh-CN/app.json                              |  25 +--
 packages/i18n/locales/zh-TW/app.json                              |  25 +--
 packages/i18n/src/resources.d.ts                                  |  25 +--
 19 files changed, 746 insertions(+), 434 deletions(-)

Fusion-Task-Id: FN-029

Fusion-Task-Lineage: 3330d0e7-ba77-4392-98f7-69425070cfd1

Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-20 03:55:39 +00:00

77 lines
3.3 KiB
TypeScript

import React from "react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PendingChatMessageQueue } from "../PendingChatMessageQueue";
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, fallback: string, options?: { preview?: string }) =>
fallback.replace("{{preview}}", options?.preview ?? ""),
}),
}));
describe("PendingChatMessageQueue", () => {
const callbacks = () => ({
onEdit: vi.fn(),
onMove: vi.fn(),
onDelete: vi.fn(),
onForceSend: vi.fn(),
});
it("renders no queue shell for empty or undefined messages", () => {
const props = callbacks();
const { rerender } = render(<PendingChatMessageQueue {...props} />);
expect(screen.queryByRole("list")).not.toBeInTheDocument();
expect(document.body.querySelector("button")).not.toBeInTheDocument();
rerender(<PendingChatMessageQueue {...props} messages={[]} />);
expect(screen.queryByRole("list")).not.toBeInTheDocument();
});
it("manages duplicate entries by index and rejects blank edits without destruction", async () => {
const user = userEvent.setup();
const props = callbacks();
render(<PendingChatMessageQueue {...props} messages={["Duplicate", "Duplicate", "Last"]} testIdPrefix="queue" />);
await user.click(screen.getByTestId("queue-edit-1"));
const input = screen.getByRole("textbox", { name: "Edit queued message 2" });
await user.clear(input);
await user.click(screen.getByTestId("queue-save-1"));
expect(screen.getByText("Queued messages cannot be empty")).toBeInTheDocument();
expect(props.onEdit).not.toHaveBeenCalled();
await user.click(screen.getByTestId("queue-cancel-1"));
expect(screen.getByTestId("queue-message-1")).toHaveTextContent("Duplicate");
await user.click(screen.getByTestId("queue-edit-1"));
const reopenedInput = screen.getByRole("textbox", { name: "Edit queued message 2" });
await user.clear(reopenedInput);
await user.type(reopenedInput, "Edited duplicate");
await user.click(screen.getByTestId("queue-save-1"));
expect(props.onEdit).toHaveBeenCalledWith(1, "Edited duplicate");
await user.click(screen.getByTestId("queue-up-2"));
await user.click(screen.getByTestId("queue-down-0"));
expect(props.onMove).toHaveBeenNthCalledWith(1, 2, -1);
expect(props.onMove).toHaveBeenNthCalledWith(2, 0, 1);
expect(screen.getByTestId("queue-up-0")).toBeDisabled();
expect(screen.getByTestId("queue-down-2")).toBeDisabled();
await user.click(screen.getByTestId("queue-delete-1"));
expect(props.onDelete).toHaveBeenCalledWith(1);
await user.click(screen.getByTestId("queue-force-1"));
expect(props.onForceSend).toHaveBeenCalledWith(1);
});
it("disables every mutation while the caller owns a queue action", async () => {
const user = userEvent.setup();
const props = callbacks();
render(<PendingChatMessageQueue {...props} messages={["One", "Two"]} disabled testIdPrefix="queue" />);
for (const id of ["edit-0", "up-1", "down-0", "delete-0", "force-0"]) {
expect(screen.getByTestId(`queue-${id}`)).toBeDisabled();
}
await user.click(screen.getByTestId("queue-force-0"));
expect(props.onForceSend).not.toHaveBeenCalled();
});
});