Skip to content

refactor(site): clean up clipboard functionality and define tests #12296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Feb 28, 2024
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
wip: commit more test progress
  • Loading branch information
Parkreiner committed Feb 26, 2024
commit 97e0c8def1c8743c5277a9a0c142ba39ffabcd79
146 changes: 75 additions & 71 deletions site/src/hooks/useClipboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { type UseClipboardResult, useClipboard } from "./useClipboard";
import { act, renderHook } from "@testing-library/react";

/*
Normally, you could call userEvent.setup to enable clipboard mocking, but
userEvent doesn't expose a teardown function. It also modifies the global
Expand All @@ -14,18 +11,28 @@ import { act, renderHook } from "@testing-library/react";
tests pass, even if they shouldn't. Have to avoid that by creating a custom
clipboard mock.
*/
type MockClipboard = Readonly<
Clipboard & {
getMockText: () => string;
setMockText: (newText: string) => void;
resetMockText: () => void;
setIsSecureContext: (newContext: boolean) => void;
}
>;
import { type UseClipboardResult, useClipboard } from "./useClipboard";
import { act, renderHook } from "@testing-library/react";

function makeMockClipboard(): MockClipboard {
const initialExecCommand = global.document.execCommand;
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.restoreAllMocks();
jest.useRealTimers();
global.document.execCommand = initialExecCommand;
});

type MockClipboardEscapeHatches = Readonly<{
getMockText: () => string;
setMockText: (newText: string) => void;
}>;

type MockClipboard = Readonly<Clipboard & MockClipboardEscapeHatches>;
function makeMockClipboard(isSecureContext: boolean): MockClipboard {
let mockClipboardValue = "";
let isSecureContext = true;

return {
readText: async () => {
Expand All @@ -49,12 +56,6 @@ function makeMockClipboard(): MockClipboard {
setMockText: (newText) => {
mockClipboardValue = newText;
},
resetMockText: () => {
mockClipboardValue = "";
},
setIsSecureContext: (newContext) => {
isSecureContext = newContext;
},

addEventListener: jest.fn(),
removeEventListener: jest.fn(),
Expand All @@ -64,44 +65,6 @@ function makeMockClipboard(): MockClipboard {
};
}

const mockClipboard = makeMockClipboard();

beforeAll(() => {
jest.useFakeTimers();

const originalNavigator = window.navigator;
jest.spyOn(window, "navigator", "get").mockImplementation(() => ({
...originalNavigator,
clipboard: mockClipboard,
}));

// Not the biggest fan of exposing implementation details like this, but
// making any kind of mock for execCommand is really gnarly in general
global.document.execCommand = jest.fn(() => {
const dummyInput = document.querySelector("input[data-testid=dummy]");
const inputIsFocused =
dummyInput instanceof HTMLInputElement &&
document.activeElement === dummyInput;

let copySuccessful = false;
if (inputIsFocused) {
mockClipboard.setMockText(dummyInput.value);
copySuccessful = true;
}

return copySuccessful;
});
});

afterEach(() => {
mockClipboard.resetMockText();
});

afterAll(() => {
jest.restoreAllMocks();
jest.useRealTimers();
});

function renderUseClipboard(textToCopy: string) {
type Props = Readonly<{ textToCopy: string }>;
return renderHook<UseClipboardResult, Props>(
Expand All @@ -110,19 +73,60 @@ function renderUseClipboard(textToCopy: string) {
);
}

async function assertClipboardTextUpdate(
result: ReturnType<typeof renderUseClipboard>["result"],
textToCheck: string,
): Promise<void> {
await act(() => result.current.copyToClipboard());
expect(result.current.showCopiedSuccess).toBe(true);
/**
* Unconventional test setup, but we need two separate instances of the
* MockClipboard (one for HTTP and one for HTTPS).
*
* All beforeAll and afterEach hooks must be tied to the specific instance, or
* else you get shared mutable state, and test cases interfering with each
* other. Test isolation is especially important for this test file
*/
function scheduleTests(isHttps: boolean) {
const mockClipboardInstance = makeMockClipboard(isHttps);

beforeAll(() => {
const originalNavigator = window.navigator;
jest.spyOn(window, "navigator", "get").mockImplementation(() => ({
...originalNavigator,
clipboard: mockClipboardInstance,
}));
});

const clipboardText = mockClipboard.getMockText();
expect(clipboardText).toEqual(textToCheck);
}
if (!isHttps) {
beforeAll(() => {
// Not the biggest fan of exposing implementation details like this, but
// making any kind of mock for execCommand is really gnarly in general
global.document.execCommand = jest.fn(() => {
const dummyInput = document.querySelector("input[data-testid=dummy]");
const inputIsFocused =
dummyInput instanceof HTMLInputElement &&
document.activeElement === dummyInput;

let copySuccessful = false;
if (inputIsFocused) {
mockClipboardInstance.setMockText(dummyInput.value);
copySuccessful = true;
}

return copySuccessful;
});
});
}

function scheduleTests(isHttps: boolean) {
mockClipboard.setIsSecureContext(isHttps);
afterEach(() => {
mockClipboardInstance.setMockText("");
});

const assertClipboardTextUpdate = async (
result: ReturnType<typeof renderUseClipboard>["result"],
textToCheck: string,
): Promise<void> => {
await act(() => result.current.copyToClipboard());
expect(result.current.showCopiedSuccess).toBe(true);

const clipboardText = mockClipboardInstance.getMockText();
expect(clipboardText).toEqual(textToCheck);
};

it("Copies the current text to the user's clipboard", async () => {
const hookText = "dogs";
Expand All @@ -148,7 +152,7 @@ describe(useClipboard.name, () => {
scheduleTests(false);
});

// describe("HTTPS connections", () => {
// scheduleTests(true);
// });
describe("HTTPS connections", () => {
scheduleTests(true);
});
});