diff --git a/dashboard/src/__tests__/clipboard-audit.test.ts b/dashboard/src/__tests__/clipboard-audit.test.ts new file mode 100644 index 0000000..8a17c40 --- /dev/null +++ b/dashboard/src/__tests__/clipboard-audit.test.ts @@ -0,0 +1,43 @@ +import { readdirSync, readFileSync, statSync } from "fs"; +import { join, relative } from "path"; +import { describe, expect, it } from "vitest"; + +const SRC_ROOT = join(process.cwd(), "dashboard", "src"); +const ALLOWED_CLIPBOARD_FILE = "lib/clipboard.ts"; + +function listSourceFiles(dir: string): string[] { + const entries = readdirSync(dir); + const files: string[] = []; + + for (const entry of entries) { + const fullPath = join(dir, entry); + const stat = statSync(fullPath); + if (stat.isDirectory()) { + if (entry === "__tests__") continue; + files.push(...listSourceFiles(fullPath)); + continue; + } + if (/\.(ts|tsx)$/.test(entry)) { + files.push(fullPath); + } + } + + return files; +} + +describe("clipboard API usage audit", () => { + it("keeps direct clipboard APIs inside the shared fallback helper", () => { + const directClipboardUsers = listSourceFiles(SRC_ROOT) + .map((file) => ({ + file: relative(SRC_ROOT, file).replace(/\\/g, "/"), + source: readFileSync(file, "utf8"), + })) + .filter(({ file, source }) => + file !== ALLOWED_CLIPBOARD_FILE && + (/navigator\.clipboard/.test(source) || /document\.execCommand/.test(source)), + ) + .map(({ file }) => file); + + expect(directClipboardUsers).toEqual([]); + }); +}); diff --git a/dashboard/src/__tests__/settings-tab.test.tsx b/dashboard/src/__tests__/settings-tab.test.tsx index ca3e5b1..2a21bad 100644 --- a/dashboard/src/__tests__/settings-tab.test.tsx +++ b/dashboard/src/__tests__/settings-tab.test.tsx @@ -8,6 +8,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { SettingsTab } from "../components/tabs/settings-tab"; import type { SettingsTabProps } from "../components/tabs/settings-tab"; +const { copyTextMock } = vi.hoisted(() => ({ + copyTextMock: vi.fn().mockResolvedValue("ok" as const), +})); + +vi.mock("../lib/clipboard", () => ({ + copyText: copyTextMock, +})); + const RECIPIENT = { name: "Rosa Garcia", age: 78, @@ -35,6 +43,11 @@ function buildProps(overrides: Partial = {}): SettingsTabProps }; } +beforeEach(() => { + copyTextMock.mockReset(); + copyTextMock.mockResolvedValue("ok"); +}); + describe("SettingsTab — load from props (Issue #79)", () => { it("renders recipient name from props (not hardcoded)", () => { render(); @@ -168,3 +181,26 @@ describe("SettingsTab — agent status (Issue #79)", () => { expect(onTogglePause).toHaveBeenCalledTimes(1); }); }); + +describe("SettingsTab — clipboard fallback (Issue #218)", () => { + it("shows a fallback toast when wallet copy fails", async () => { + const wallet = "GDSETTINGSWALLET123456789"; + copyTextMock.mockResolvedValueOnce("failed"); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /Copy/i })); + + await waitFor(() => { + expect(screen.getByRole("status").textContent).toContain("Couldn't copy. Press Ctrl+C."); + }); + expect((screen.getByLabelText(/Text that could not be copied/i) as HTMLInputElement).value) + .toBe(wallet); + }); +});