Skip to content

Commit 18cf563

Browse files
committed
fix: update existing tests to new format
1 parent 8613133 commit 18cf563

File tree

2 files changed

+59
-27
lines changed

2 files changed

+59
-27
lines changed

site/src/hooks/useClipboard.temp.test.tsx

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,24 @@ import { ThemeProvider } from "contexts/ThemeProvider";
44
import {
55
type UseClipboardInput,
66
type UseClipboardResult,
7+
COPY_FAILED_MESSAGE,
78
useClipboard,
89
} from "./useClipboard";
910

11+
// execCommand is the workaround for copying text to the clipboard on HTTP-only
12+
// connections
13+
const originalExecCommand = global.document.execCommand;
14+
const originalNavigator = window.navigator;
15+
16+
// Need to mock console.error because we deliberately need to trigger errors in
17+
// the code to assert that it can recover from them, but we also don't want them
18+
// logged if they're expected
19+
const originalConsoleError = console.error;
20+
1021
type SetupMockClipboardResult = Readonly<{
1122
mockClipboard: Clipboard;
23+
mockExecCommand: typeof originalExecCommand;
1224
getClipboardText: () => string;
13-
setClipboardText: (newText: string) => void;
1425
setSimulateFailure: (shouldFail: boolean) => void;
1526
}>;
1627

@@ -60,12 +71,31 @@ function setupMockClipboard(isSecure: boolean): SetupMockClipboardResult {
6071
return {
6172
mockClipboard,
6273
getClipboardText: () => mockClipboardText,
63-
setClipboardText: (newText) => {
64-
mockClipboardText = newText;
65-
},
6674
setSimulateFailure: (newShouldFailValue) => {
6775
shouldSimulateFailure = newShouldFailValue;
6876
},
77+
mockExecCommand: (commandId) => {
78+
if (commandId !== "copy") {
79+
return false;
80+
}
81+
82+
if (shouldSimulateFailure) {
83+
throw new Error("Failed to execute command 'copy'");
84+
}
85+
86+
const dummyInput = document.querySelector("input[data-testid=dummy]");
87+
const inputIsFocused =
88+
dummyInput instanceof HTMLInputElement &&
89+
document.activeElement === dummyInput;
90+
91+
let copySuccessful = false;
92+
if (inputIsFocused) {
93+
mockClipboardText = dummyInput.value;
94+
copySuccessful = true;
95+
}
96+
97+
return copySuccessful;
98+
},
6999
};
70100
}
71101

@@ -86,73 +116,75 @@ function renderUseClipboard<TInput extends UseClipboardInput>(inputs: TInput) {
86116
}
87117

88118
const secureContextValues: readonly boolean[] = [true, false];
89-
const originalNavigator = window.navigator;
90-
const originalExecCommand = global.document.execCommand;
91119

92120
// Not a big fan of describe.each most of the time, but since we need to test
93121
// the exact same test cases against different inputs, and we want them to run
94122
// as sequentially as possible to minimize flakes, they make sense here
95-
describe.each(secureContextValues)("useClipboard - secure: %j", (context) => {
123+
describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
96124
const {
97125
mockClipboard,
126+
mockExecCommand,
98127
getClipboardText,
99-
setClipboardText,
100128
setSimulateFailure,
101-
} = setupMockClipboard(context);
129+
} = setupMockClipboard(isSecure);
102130

103131
beforeEach(() => {
104132
jest.useFakeTimers();
133+
global.document.execCommand = mockExecCommand;
105134
jest.spyOn(window, "navigator", "get").mockImplementation(() => ({
106135
...originalNavigator,
107136
clipboard: mockClipboard,
108137
}));
109138

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;
139+
console.error = (errorValue, ...rest) => {
140+
const canIgnore =
141+
errorValue instanceof Error &&
142+
errorValue.message === COPY_FAILED_MESSAGE;
115143

116-
let copySuccessful = false;
117-
if (inputIsFocused) {
118-
setClipboardText(dummyInput.value);
119-
copySuccessful = true;
144+
if (!canIgnore) {
145+
originalConsoleError(errorValue, ...rest);
120146
}
121-
122-
return copySuccessful;
123-
});
147+
};
124148
});
125149

126150
afterEach(() => {
127151
jest.runAllTimers();
128152
jest.useRealTimers();
129153
jest.resetAllMocks();
154+
155+
console.error = originalConsoleError;
130156
global.document.execCommand = originalExecCommand;
131157
});
132158

133-
const assertClipboardTextUpdate = async (
159+
const assertClipboardUpdateLifecycle = async (
134160
result: ReturnType<typeof renderUseClipboard>["result"],
135161
textToCheck: string,
136162
): Promise<void> => {
137163
await act(() => result.current.copyToClipboard());
138164
expect(result.current.showCopiedSuccess).toBe(true);
139165

166+
// Because of timing trickery, any timeouts for flipping the copy status
167+
// back to false will trigger before the test can complete. This will never
168+
// be an issue in the real world, but it will kick up 'act' warnings in the
169+
// console, which makes tests more annoying. Just wait for them to finish up
170+
// to avoid anything from being logged, but note that the value of
171+
// showCopiedSuccess will become false after this
172+
await act(() => jest.runAllTimersAsync());
173+
140174
const clipboardText = getClipboardText();
141175
expect(clipboardText).toEqual(textToCheck);
142176
};
143177

144178
it("Copies the current text to the user's clipboard", async () => {
145179
const textToCopy = "dogs";
146180
const { result } = renderUseClipboard({ textToCopy });
147-
await assertClipboardTextUpdate(result, textToCopy);
181+
await assertClipboardUpdateLifecycle(result, textToCopy);
148182
});
149183

150184
it("Should indicate to components not to show successful copy after a set period of time", async () => {
151185
const textToCopy = "cats";
152186
const { result } = renderUseClipboard({ textToCopy });
153-
await assertClipboardTextUpdate(result, textToCopy);
154-
155-
await jest.runAllTimersAsync();
187+
await assertClipboardUpdateLifecycle(result, textToCopy);
156188
expect(result.current.showCopiedSuccess).toBe(false);
157189
});
158190

site/src/hooks/useClipboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
22
import { displayError } from "components/GlobalSnackbar/utils";
33

44
const CLIPBOARD_TIMEOUT_MS = 1_000;
5-
const COPY_FAILED_MESSAGE = "Failed to copy text to clipboard";
5+
export const COPY_FAILED_MESSAGE = "Failed to copy text to clipboard";
66

77
export type UseClipboardInput = Readonly<{
88
textToCopy: string;

0 commit comments

Comments
 (0)