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>
77 lines
3.3 KiB
TypeScript
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();
|
|
});
|
|
});
|