Replaces the last direct navigator.clipboard.writeText() call sites across dashboard components and the reports plugin with the shared copyTextToClipboard helper, fixing copy actions that crashed or silently failed on non-secure origins (HTTP/mobile). - Migrated AgentDetailView, AgentErrorDetailsModal, CliBinaryPanel, GitManagerModal, LoginInstructions, PrPanel, SecretsView, and StashConflictModal to use copyTextToClipboard (secure-context guard + execCommand fallback, boolean result handling) instead of calling navigator.clipboard directly. - Migrated the fusion-plugin-reports ShareBlocksPanel to the same helper and added a vitest alias so the plugin's subpath import resolves to the dashboard's copyToClipboard util instead of collapsing to its package root. - Added ./app/utils/copyToClipboard subpath export to @fusion/dashboard's package.json. - Added/extended tests covering copy success, fallback, and failure paths for AgentDetailView, CliBinaryPanel, AgentErrorDetailsModal, GitManagerModal, SecretsView, StashConflictModal, and ShareBlocksPanel. - Added a patch changeset documenting the fix for @runfusion/fusion. Files changed: .changeset/fn-7885-clipboard-migration.md | 7 ++ packages/dashboard/app/components/AgentDetailView.tsx | 16 ++++- packages/dashboard/app/components/AgentErrorDetailsModal.tsx | 5 +- packages/dashboard/app/components/CliBinaryPanel.tsx | 16 +++-- packages/dashboard/app/components/GitManagerModal.tsx | 16 ++++- packages/dashboard/app/components/LoginInstructions.tsx | 21 +++--- packages/dashboard/app/components/PrPanel.tsx | 9 ++- packages/dashboard/app/components/SecretsView.tsx | 11 ++- packages/dashboard/app/components/StashConflictModal.tsx | 13 ++-- packages/dashboard/app/components/__tests__/AgentDetailView.copy.test.tsx | 56 +++++++++++++++ packages/dashboard/app/components/__tests__/AgentErrorDetailsModal.test.tsx | 18 +++++ packages/dashboard/app/components/__tests__/CliBinaryPanel.copy.test.tsx | 68 ++++++++++++++++++ packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx | 23 ++++++ packages/dashboard/app/components/__tests__/SecretsView.test.tsx | 82 ++++++++++++++++++++++ packages/dashboard/app/components/__tests__/StashConflictModal.test.tsx | 25 ++++++- packages/dashboard/package.json | 4 ++ plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx | 5 +- plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx | 43 +++++++++++- plugins/fusion-plugin-reports/vitest.config.ts | 2 + 19 files changed, 402 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-7885 Fusion-Task-Lineage: 122a54ea-962b-4ae1-98ac-c28e03f4f8ca Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { AgentErrorDetailsModal, AgentErrorIndicator } from "../AgentErrorDetailsModal";
|
|
import { loadAllAppCss } from "../../test/cssFixture";
|
|
import { ModalDismissPreferenceProvider } from "../../hooks/useOverlayDismiss";
|
|
|
|
const issueContext = {
|
|
surface: "AgentsView",
|
|
agentId: "agent-1",
|
|
agentName: "Test Agent",
|
|
agentState: "error",
|
|
runId: "run-1",
|
|
taskId: "FN-1",
|
|
timestamp: "2026-01-01T00:00:00.000Z",
|
|
};
|
|
|
|
describe("AgentErrorDetailsModal", () => {
|
|
const originalClipboard = navigator.clipboard;
|
|
const originalExecCommand = document.execCommand;
|
|
const originalSecureContext = window.isSecureContext;
|
|
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
|
beforeEach(() => {
|
|
Object.defineProperty(navigator, "clipboard", {
|
|
value: { writeText: vi.fn().mockResolvedValue(undefined) },
|
|
configurable: true,
|
|
});
|
|
openSpy.mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(navigator, "clipboard", { value: originalClipboard, configurable: true });
|
|
Object.defineProperty(document, "execCommand", { value: originalExecCommand, configurable: true });
|
|
Object.defineProperty(window, "isSecureContext", { value: originalSecureContext, configurable: true });
|
|
});
|
|
|
|
it("does not render when closed", () => {
|
|
const { container } = render(<AgentErrorDetailsModal open={false} onClose={vi.fn()} errorText="boom" issueContext={issueContext} />);
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it("renders long error text in scrollable error region", () => {
|
|
render(<AgentErrorDetailsModal open={true} onClose={vi.fn()} errorText={"stderr\n".repeat(200)} issueContext={issueContext} />);
|
|
const errorRegion = document.querySelector(".agent-error-modal__error");
|
|
expect(errorRegion).toBeInTheDocument();
|
|
expect(errorRegion).toHaveTextContent("stderr");
|
|
});
|
|
|
|
it("copies error text", async () => {
|
|
const user = userEvent.setup();
|
|
render(<AgentErrorDetailsModal open={true} onClose={vi.fn()} errorText="copy me" issueContext={issueContext} />);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Copy error to clipboard" }));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("button", { name: "Copied error to clipboard" })).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("copies error text through execCommand when Clipboard API is unavailable", async () => {
|
|
const execCommand = vi.fn().mockReturnValue(true);
|
|
Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
|
|
Object.defineProperty(window, "isSecureContext", { value: false, configurable: true });
|
|
Object.defineProperty(document, "execCommand", { value: execCommand, configurable: true });
|
|
const user = userEvent.setup();
|
|
render(<AgentErrorDetailsModal open={true} onClose={vi.fn()} errorText="copy me" issueContext={issueContext} />);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Copy error to clipboard" }));
|
|
|
|
await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
|
|
expect(screen.getByRole("button", { name: "Copied error to clipboard" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("gates backdrop dismissal behind the global modal dismiss preference", () => {
|
|
const disabledClose = vi.fn();
|
|
const { unmount } = render(<AgentErrorDetailsModal open={true} onClose={disabledClose} errorText="boom" issueContext={issueContext} />);
|
|
const disabledOverlay = screen.getByRole("dialog", { name: "Agent error details" });
|
|
|
|
fireEvent.mouseDown(disabledOverlay);
|
|
fireEvent.mouseUp(disabledOverlay);
|
|
|
|
expect(disabledClose).not.toHaveBeenCalled();
|
|
unmount();
|
|
|
|
const enabledClose = vi.fn();
|
|
render(
|
|
<ModalDismissPreferenceProvider enabled>
|
|
<AgentErrorDetailsModal open={true} onClose={enabledClose} errorText="boom" issueContext={issueContext} />
|
|
</ModalDismissPreferenceProvider>,
|
|
);
|
|
const enabledOverlay = screen.getByRole("dialog", { name: "Agent error details" });
|
|
|
|
fireEvent.mouseDown(enabledOverlay);
|
|
fireEvent.mouseUp(enabledOverlay);
|
|
|
|
expect(enabledClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("opens github report link", async () => {
|
|
const user = userEvent.setup();
|
|
render(<AgentErrorDetailsModal open={true} onClose={vi.fn()} errorText="report me" issueContext={issueContext} />);
|
|
|
|
await user.click(screen.getByRole("link", { name: /report on github/i }));
|
|
expect(openSpy).toHaveBeenCalledTimes(1);
|
|
expect(openSpy.mock.calls[0]?.[0]).toContain("https://github.com/Runfusion/Fusion/issues/new?");
|
|
});
|
|
|
|
it("AgentErrorIndicator opens shared modal", async () => {
|
|
const user = userEvent.setup();
|
|
render(<AgentErrorIndicator errorText="indicator error" issueContext={issueContext} />);
|
|
await user.click(screen.getByRole("button", { name: "Open error details" }));
|
|
expect(screen.getByRole("dialog", { name: "Agent error details" })).toBeInTheDocument();
|
|
});
|
|
|
|
describe("mobile layout", () => {
|
|
it("keeps error content scrollable and protects modal footer from mobile clipping regression", async () => {
|
|
Object.defineProperty(window, "innerWidth", { configurable: true, value: 375 });
|
|
window.dispatchEvent(new Event("resize"));
|
|
|
|
render(
|
|
<AgentErrorDetailsModal
|
|
open={true}
|
|
onClose={vi.fn()}
|
|
errorText={"stderr line\n".repeat(400)}
|
|
issueContext={issueContext}
|
|
/>,
|
|
);
|
|
|
|
const errorRegion = document.querySelector(".agent-error-modal__error");
|
|
expect(errorRegion).toBeInTheDocument();
|
|
expect(errorRegion).toHaveStyle({ overflow: "auto" });
|
|
|
|
const actions = document.querySelector(".modal-actions");
|
|
expect(actions).toBeInTheDocument();
|
|
expect(errorRegion?.compareDocumentPosition(actions as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
|
|
|
const allCss = await loadAllAppCss();
|
|
expect(allCss).toContain(".agent-error-modal");
|
|
expect(allCss).toContain("--mobile-nav-height");
|
|
expect(allCss).toContain("--standalone-bottom-gap");
|
|
expect(allCss).toContain("env(safe-area-inset-bottom");
|
|
});
|
|
});
|
|
});
|