Skip to content

Commit e59db08

Browse files
CUI-6: Add CopyButton tests (clipboard, copyAsHtml, tooltip, success reset)
1 parent c816876 commit e59db08

File tree

1 file changed

+252
-0
lines changed

1 file changed

+252
-0
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import "@testing-library/jest-dom";
2+
import { act, render, screen, waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import React from "react";
5+
import { getWrapper } from "../../testUtils";
6+
import { CopyButton } from "./CopyButton.component";
7+
8+
describe("CopyButton", () => {
9+
const { Wrapper } = getWrapper();
10+
const originalClipboard = navigator.clipboard;
11+
12+
beforeEach(() => {
13+
Object.assign(navigator, {
14+
clipboard: {
15+
writeText: jest.fn(),
16+
write: jest.fn(),
17+
},
18+
});
19+
// JSDOM doesn't define ClipboardItem; needed for copyAsHtml path. Stub only — full interface not needed for tests.
20+
if (typeof globalThis.ClipboardItem === "undefined") {
21+
// @ts-expect-error — intentional stub for JSDOM; global ClipboardItem type expects full interface (getType, types, etc.)
22+
globalThis.ClipboardItem = class {};
23+
}
24+
});
25+
26+
afterEach(() => {
27+
Object.assign(navigator, { clipboard: originalClipboard });
28+
jest.restoreAllMocks();
29+
});
30+
31+
it("when writeText returns a rejected Promise, should enter the unsupported state", async () => {
32+
const writeTextMock = navigator.clipboard.writeText as jest.Mock;
33+
writeTextMock.mockRejectedValueOnce(new Error("Clipboard denied"));
34+
35+
render(<CopyButton textToCopy="test" />, { wrapper: Wrapper });
36+
37+
const button = screen.getByRole("button", { name: "Copy" });
38+
await act(() => userEvent.click(button));
39+
40+
await waitFor(() => {
41+
expect(writeTextMock).toHaveBeenCalledWith("test");
42+
});
43+
44+
// Unsupported state: button should not show success, and not be aria-disabled
45+
expect(
46+
screen.queryByRole("button", { name: "Copied !" }),
47+
).not.toBeInTheDocument();
48+
const buttonAfterReject = screen.getByRole("button", { name: "Copy" });
49+
expect(buttonAfterReject).not.toHaveAttribute("aria-disabled", "true");
50+
});
51+
52+
it("during success state click does nothing; after 2s resets to idle and click works again", async () => {
53+
jest.useFakeTimers();
54+
try {
55+
const writeTextMock = navigator.clipboard.writeText as jest.Mock;
56+
writeTextMock.mockResolvedValue(undefined);
57+
58+
render(<CopyButton textToCopy="test" />, { wrapper: Wrapper });
59+
60+
await act(() =>
61+
userEvent.click(screen.getByRole("button", { name: "Copy" })),
62+
);
63+
64+
await waitFor(() => {
65+
expect(
66+
screen.getByRole("button", { name: "Copied !" }),
67+
).toBeInTheDocument();
68+
});
69+
70+
expect(writeTextMock).toHaveBeenCalledTimes(1);
71+
const inSuccess = screen.getByRole("button", { name: "Copied !" });
72+
expect(inSuccess).toHaveAttribute("aria-disabled", "true");
73+
74+
await act(() => userEvent.click(inSuccess));
75+
expect(writeTextMock).toHaveBeenCalledTimes(1);
76+
77+
act(() => {
78+
jest.advanceTimersByTime(2000);
79+
});
80+
81+
await waitFor(() => {
82+
expect(
83+
screen.getByRole("button", { name: "Copy" }),
84+
).toBeInTheDocument();
85+
expect(
86+
screen.getByRole("button", { name: "Copy" }),
87+
).not.toHaveAttribute("aria-disabled", "true");
88+
});
89+
90+
await act(() =>
91+
userEvent.click(screen.getByRole("button", { name: "Copy" })),
92+
);
93+
94+
await waitFor(() => {
95+
expect(writeTextMock).toHaveBeenCalledTimes(2);
96+
});
97+
} finally {
98+
jest.useRealTimers();
99+
}
100+
});
101+
102+
it("standard copy (writeText): on success shows Copied ! tooltip and aria-disabled", async () => {
103+
const writeTextMock = navigator.clipboard.writeText as jest.Mock;
104+
writeTextMock.mockResolvedValueOnce(undefined);
105+
106+
render(<CopyButton textToCopy="hello" />, { wrapper: Wrapper });
107+
108+
await act(() =>
109+
userEvent.click(screen.getByRole("button", { name: "Copy" })),
110+
);
111+
112+
await waitFor(() => {
113+
expect(writeTextMock).toHaveBeenCalledWith("hello");
114+
const button = screen.getByRole("button", { name: "Copied !" });
115+
expect(button).toHaveAttribute("aria-disabled", "true");
116+
});
117+
});
118+
119+
it("copyAsHtml: calls clipboard.write and on success shows Copied ! and aria-disabled", async () => {
120+
const writeMock = navigator.clipboard.write as jest.Mock;
121+
writeMock.mockResolvedValueOnce(undefined);
122+
123+
render(<CopyButton textToCopy="<b>hi</b>" copyAsHtml />, {
124+
wrapper: Wrapper,
125+
});
126+
127+
await act(() =>
128+
userEvent.click(screen.getByRole("button", { name: "Copy" })),
129+
);
130+
131+
await waitFor(() => {
132+
expect(writeMock).toHaveBeenCalledTimes(1);
133+
expect(writeMock.mock.calls[0][0]).toHaveLength(1);
134+
expect(
135+
screen.getByRole("button", { name: "Copied !" }),
136+
).toBeInTheDocument();
137+
expect(screen.getByRole("button", { name: "Copied !" })).toHaveAttribute(
138+
"aria-disabled",
139+
"true",
140+
);
141+
});
142+
});
143+
144+
it("copyAsHtml: when write rejects, enters unsupported state", async () => {
145+
const writeMock = navigator.clipboard.write as jest.Mock;
146+
writeMock.mockRejectedValueOnce(new Error("Denied"));
147+
148+
render(<CopyButton textToCopy="<b>hi</b>" copyAsHtml />, {
149+
wrapper: Wrapper,
150+
});
151+
152+
await act(() =>
153+
userEvent.click(screen.getByRole("button", { name: "Copy" })),
154+
);
155+
156+
await waitFor(() => {
157+
expect(writeMock).toHaveBeenCalled();
158+
});
159+
160+
expect(
161+
screen.queryByRole("button", { name: "Copied !" }),
162+
).not.toBeInTheDocument();
163+
expect(screen.getByRole("button", { name: "Copy" })).not.toHaveAttribute(
164+
"aria-disabled",
165+
"true",
166+
);
167+
});
168+
169+
it("ghost variant without label: tooltip is Copy then Copied ! on success", async () => {
170+
const writeTextMock = navigator.clipboard.writeText as jest.Mock;
171+
writeTextMock.mockResolvedValueOnce(undefined);
172+
173+
render(<CopyButton textToCopy="x" />, { wrapper: Wrapper });
174+
175+
expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument();
176+
177+
await act(() =>
178+
userEvent.click(screen.getByRole("button", { name: "Copy" })),
179+
);
180+
181+
await waitFor(() => {
182+
expect(
183+
screen.getByRole("button", { name: "Copied !" }),
184+
).toBeInTheDocument();
185+
});
186+
});
187+
188+
it("ghost variant with label: tooltip is Copy {label} then Copied ! on success", async () => {
189+
const writeTextMock = navigator.clipboard.writeText as jest.Mock;
190+
writeTextMock.mockResolvedValueOnce(undefined);
191+
192+
render(<CopyButton textToCopy="x" label="key" />, { wrapper: Wrapper });
193+
194+
expect(
195+
screen.getByRole("button", { name: "Copy key" }),
196+
).toBeInTheDocument();
197+
198+
await act(() =>
199+
userEvent.click(screen.getByRole("button", { name: "Copy key" })),
200+
);
201+
202+
await waitFor(() => {
203+
expect(
204+
screen.getByRole("button", { name: "Copied !" }),
205+
).toBeInTheDocument();
206+
});
207+
});
208+
209+
it("outline variant without label: tooltip is Copy then Copied! on success", async () => {
210+
const writeTextMock = navigator.clipboard.writeText as jest.Mock;
211+
writeTextMock.mockResolvedValueOnce(undefined);
212+
213+
render(<CopyButton textToCopy="x" variant="outline" />, {
214+
wrapper: Wrapper,
215+
});
216+
217+
expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument();
218+
219+
await act(() =>
220+
userEvent.click(screen.getByRole("button", { name: "Copy" })),
221+
);
222+
223+
await waitFor(() => {
224+
expect(
225+
screen.getByRole("button", { name: "Copied!" }),
226+
).toBeInTheDocument();
227+
});
228+
});
229+
230+
it("outline variant with label: tooltip is Copy {label} then Copied {label}! on success", async () => {
231+
const writeTextMock = navigator.clipboard.writeText as jest.Mock;
232+
writeTextMock.mockResolvedValueOnce(undefined);
233+
234+
render(<CopyButton variant="outline" textToCopy="x" label="key" />, {
235+
wrapper: Wrapper,
236+
});
237+
238+
expect(
239+
screen.getByRole("button", { name: "Copy key" }),
240+
).toBeInTheDocument();
241+
242+
await act(() =>
243+
userEvent.click(screen.getByRole("button", { name: "Copy key" })),
244+
);
245+
246+
await waitFor(() => {
247+
expect(
248+
screen.getByRole("button", { name: "Copied key!" }),
249+
).toBeInTheDocument();
250+
});
251+
});
252+
});

0 commit comments

Comments
 (0)