Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion src/components/AddressDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("AddressDisplay", () => {
expect(copyBtn).not.toHaveAttribute("tabindex", "-1");
await act(async () => { fireEvent.click(copyBtn); });
expect(mockWriteText).toHaveBeenCalledWith(address);
expect(screen.getByRole("button", { name: "Address copied" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Copied!" })).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByRole("button", { name: "Copy address" })).toBeInTheDocument();
}, { timeout: 2500 });
Expand All @@ -42,6 +42,65 @@ describe("AddressDisplay", () => {
});
});

describe("copy operation & clipboard fallback (#604)", () => {
it("resets aria-label back to 'Copy address' after 2 seconds using fake timers", async () => {
vi.useFakeTimers();
render(<AddressDisplay address={address} />);
const copyBtn = screen.getByRole("button", { name: "Copy address" });

await act(async () => {
fireEvent.click(copyBtn);
});

expect(screen.getByRole("button", { name: "Copied!" })).toBeInTheDocument();

act(() => {
vi.advanceTimersByTime(2000);
});

expect(screen.getByRole("button", { name: "Copy address" })).toBeInTheDocument();
vi.useRealTimers();
});

it("triggers document.execCommand fallback when navigator.clipboard.writeText rejects", async () => {
mockWriteText.mockRejectedValueOnce(new Error("Clipboard access denied"));
const execCommandSpy = vi.fn();
const originalExecCommand = document.execCommand;
document.execCommand = execCommandSpy;

render(<AddressDisplay address={address} />);
const copyBtn = screen.getByRole("button", { name: "Copy address" });

await act(async () => {
fireEvent.click(copyBtn);
});

expect(execCommandSpy).toHaveBeenCalledWith("copy");
expect(screen.getByRole("button", { name: "Copied!" })).toBeInTheDocument();

document.execCommand = originalExecCommand;
});

it("does not throw an error when clipboard is unavailable and fallback fails", async () => {
mockWriteText.mockRejectedValueOnce(new Error("Clipboard access denied"));
const originalExecCommand = document.execCommand;
document.execCommand = vi.fn().mockImplementation(() => {
throw new Error("execCommand unsupported");
});

render(<AddressDisplay address={address} />);
const copyBtn = screen.getByRole("button", { name: "Copy address" });

await expect(
act(async () => {
fireEvent.click(copyBtn);
})
).resolves.not.toThrow();

document.execCommand = originalExecCommand;
});
});

describe("showFull prop", () => {
it("renders the full address and applies select-all class", () => {
render(<AddressDisplay address={address} showFull />);
Expand Down
14 changes: 12 additions & 2 deletions src/components/AddressDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ export function AddressDisplay({
onCopy?.();
setTimeout(() => setCopied(false), 2000);
} catch {
/* fallback */
/* fallback for non-HTTPS or rejected writeText */
try {
if (typeof document !== "undefined" && typeof document.execCommand === "function") {
document.execCommand("copy");
}
setCopied(true);
onCopy?.();
setTimeout(() => setCopied(false), 2000);
} catch {
/* ignore */
}
}
}

Expand All @@ -72,7 +82,7 @@ export function AddressDisplay({
</span>
<button
onClick={copy}
aria-label={copied ? "Address copied" : "Copy address"}
aria-label={copied ? "Copied!" : "Copy address"}
className={cn(
"shrink-0 p-1 rounded-md transition-all",
copied
Expand Down