Files
fusion/packages/dashboard/app/components/__tests__/AgentErrorDetailsModal.test.tsx
gsxdsm 6ea53966f6 FN-7885: migrate remaining direct clipboard callers to shared copyTextToClipboard helper
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>
2026-07-12 17:54:21 -07:00

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");
});
});
});