Skip to content

Commit 8613133

Browse files
committed
wip: commit progress on test revamps
1 parent f13b1c9 commit 8613133

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { act, renderHook } from "@testing-library/react";
2+
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
3+
import { ThemeProvider } from "contexts/ThemeProvider";
4+
import {
5+
type UseClipboardInput,
6+
type UseClipboardResult,
7+
useClipboard,
8+
} from "./useClipboard";
9+
10+
type SetupMockClipboardResult = Readonly<{
11+
mockClipboard: Clipboard;
12+
getClipboardText: () => string;
13+
setClipboardText: (newText: string) => void;
14+
setSimulateFailure: (shouldFail: boolean) => void;
15+
}>;
16+
17+
function setupMockClipboard(isSecure: boolean): SetupMockClipboardResult {
18+
let mockClipboardText = "";
19+
let shouldSimulateFailure = false;
20+
21+
const mockClipboard: Clipboard = {
22+
readText: async () => {
23+
if (!isSecure) {
24+
throw new Error(
25+
"Not allowed to access clipboard outside of secure contexts",
26+
);
27+
}
28+
29+
if (shouldSimulateFailure) {
30+
throw new Error("Failed to read from clipboard");
31+
}
32+
33+
return mockClipboardText;
34+
},
35+
36+
writeText: async (newText) => {
37+
if (!isSecure) {
38+
throw new Error(
39+
"Not allowed to access clipboard outside of secure contexts",
40+
);
41+
}
42+
43+
if (shouldSimulateFailure) {
44+
throw new Error("Failed to write to clipboard");
45+
}
46+
47+
mockClipboardText = newText;
48+
},
49+
50+
// Don't need these other methods for any of the tests; read and write are
51+
// both synchronous and slower than the promise-based methods, so ideally
52+
// we won't ever need to call them in the hook logic
53+
addEventListener: jest.fn(),
54+
removeEventListener: jest.fn(),
55+
dispatchEvent: jest.fn(),
56+
read: jest.fn(),
57+
write: jest.fn(),
58+
};
59+
60+
return {
61+
mockClipboard,
62+
getClipboardText: () => mockClipboardText,
63+
setClipboardText: (newText) => {
64+
mockClipboardText = newText;
65+
},
66+
setSimulateFailure: (newShouldFailValue) => {
67+
shouldSimulateFailure = newShouldFailValue;
68+
},
69+
};
70+
}
71+
72+
function renderUseClipboard<TInput extends UseClipboardInput>(inputs: TInput) {
73+
return renderHook<UseClipboardResult, TInput>(
74+
(props) => useClipboard(props),
75+
{
76+
initialProps: inputs,
77+
wrapper: ({ children }) => (
78+
// Need ThemeProvider because GlobalSnackbar uses theme
79+
<ThemeProvider>
80+
{children}
81+
<GlobalSnackbar />
82+
</ThemeProvider>
83+
),
84+
},
85+
);
86+
}
87+
88+
const secureContextValues: readonly boolean[] = [true, false];
89+
const originalNavigator = window.navigator;
90+
const originalExecCommand = global.document.execCommand;
91+
92+
// Not a big fan of describe.each most of the time, but since we need to test
93+
// the exact same test cases against different inputs, and we want them to run
94+
// as sequentially as possible to minimize flakes, they make sense here
95+
describe.each(secureContextValues)("useClipboard - secure: %j", (context) => {
96+
const {
97+
mockClipboard,
98+
getClipboardText,
99+
setClipboardText,
100+
setSimulateFailure,
101+
} = setupMockClipboard(context);
102+
103+
beforeEach(() => {
104+
jest.useFakeTimers();
105+
jest.spyOn(window, "navigator", "get").mockImplementation(() => ({
106+
...originalNavigator,
107+
clipboard: mockClipboard,
108+
}));
109+
110+
global.document.execCommand = jest.fn(() => {
111+
const dummyInput = document.querySelector("input[data-testid=dummy]");
112+
const inputIsFocused =
113+
dummyInput instanceof HTMLInputElement &&
114+
document.activeElement === dummyInput;
115+
116+
let copySuccessful = false;
117+
if (inputIsFocused) {
118+
setClipboardText(dummyInput.value);
119+
copySuccessful = true;
120+
}
121+
122+
return copySuccessful;
123+
});
124+
});
125+
126+
afterEach(() => {
127+
jest.runAllTimers();
128+
jest.useRealTimers();
129+
jest.resetAllMocks();
130+
global.document.execCommand = originalExecCommand;
131+
});
132+
133+
const assertClipboardTextUpdate = async (
134+
result: ReturnType<typeof renderUseClipboard>["result"],
135+
textToCheck: string,
136+
): Promise<void> => {
137+
await act(() => result.current.copyToClipboard());
138+
expect(result.current.showCopiedSuccess).toBe(true);
139+
140+
const clipboardText = getClipboardText();
141+
expect(clipboardText).toEqual(textToCheck);
142+
};
143+
144+
it("Copies the current text to the user's clipboard", async () => {
145+
const textToCopy = "dogs";
146+
const { result } = renderUseClipboard({ textToCopy });
147+
await assertClipboardTextUpdate(result, textToCopy);
148+
});
149+
150+
it("Should indicate to components not to show successful copy after a set period of time", async () => {
151+
const textToCopy = "cats";
152+
const { result } = renderUseClipboard({ textToCopy });
153+
await assertClipboardTextUpdate(result, textToCopy);
154+
155+
await jest.runAllTimersAsync();
156+
expect(result.current.showCopiedSuccess).toBe(false);
157+
});
158+
159+
it("Should notify the user of an error using the provided callback", async () => {
160+
const textToCopy = "birds";
161+
const onError = jest.fn();
162+
const { result } = renderUseClipboard({ textToCopy, onError });
163+
164+
setSimulateFailure(true);
165+
await act(() => result.current.copyToClipboard());
166+
expect(onError).toBeCalled();
167+
});
168+
169+
it.skip("Should dispatch a new toast message to the global snackbar if no callback is provided", async () => {
170+
expect.hasAssertions();
171+
});
172+
});

0 commit comments

Comments
 (0)