Skip to content

chore(site): refactor tests for global hooks   #12216

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 4 commits into from
Feb 20, 2024
Merged
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
117 changes: 46 additions & 71 deletions site/src/hooks/debounce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,21 @@ afterAll(() => {
jest.clearAllMocks();
});

// Most UI tests should be structure from the user's experience, but just
// because these are more abstract, general-purpose hooks, it seemed harder to
// do that. Had to bring in some mocks
function renderDebouncedValue<T = unknown>(value: T, time: number) {
return renderHook(
({ value, time }: { value: T; time: number }) => {
return useDebouncedValue(value, time);
},
{
initialProps: { value, time },
},
);
}

function renderDebouncedFunction<Args extends unknown[]>(
callbackArg: (...args: Args) => void | Promise<void>,
time: number,
) {
return renderHook(
({ callback, time }: { callback: typeof callbackArg; time: number }) => {
return useDebouncedFunction<Args>(callback, time);
},
{
initialProps: { callback: callbackArg, time },
},
);
}

describe(`${useDebouncedValue.name}`, () => {
function renderDebouncedValue<T = unknown>(value: T, time: number) {
return renderHook(
({ value, time }: { value: T; time: number }) => {
return useDebouncedValue(value, time);
},
{
initialProps: { value, time },
},
);
}

it("Should immediately return out the exact same value (by reference) on mount", () => {
const value = {};
const { result } = renderDebouncedValue(value, 2000);

expect(result.current).toBe(value);
});

Expand Down Expand Up @@ -79,6 +61,20 @@ describe(`${useDebouncedValue.name}`, () => {
});

describe(`${useDebouncedFunction.name}`, () => {
function renderDebouncedFunction<Args extends unknown[]>(
callbackArg: (...args: Args) => void | Promise<void>,
time: number,
) {
return renderHook(
({ callback, time }: { callback: typeof callbackArg; time: number }) => {
return useDebouncedFunction<Args>(callback, time);
},
{
initialProps: { callback: callbackArg, time },
},
);
}

describe("hook", () => {
it("Should provide stable function references across re-renders", () => {
const time = 5000;
Expand All @@ -97,62 +93,44 @@ describe(`${useDebouncedFunction.name}`, () => {

it("Resets any pending debounces if the timer argument changes", async () => {
const time = 5000;
let count = 0;
const incrementCount = () => {
count++;
};

const { result, rerender } = renderDebouncedFunction(
incrementCount,
time,
);
const mockCallback = jest.fn();
const { result, rerender } = renderDebouncedFunction(mockCallback, time);

result.current.debounced();
rerender({ callback: incrementCount, time: time + 1 });
rerender({ callback: mockCallback, time: time + 1 });

await jest.runAllTimersAsync();
expect(count).toEqual(0);
expect(mockCallback).not.toBeCalled();
});
});

describe("debounced function", () => {
it("Resolve the debounce after specified milliseconds pass with no other calls", async () => {
let value = false;
const { result } = renderDebouncedFunction(() => {
value = !value;
}, 100);

const mockCallback = jest.fn();
const { result } = renderDebouncedFunction(mockCallback, 100);
result.current.debounced();

await jest.runOnlyPendingTimersAsync();
expect(value).toBe(true);
expect(mockCallback).toBeCalledTimes(1);
});

it("Always uses the most recent callback argument passed in (even if it switches while a debounce is queued)", async () => {
let count = 0;
const mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();
const time = 500;

const { result, rerender } = renderDebouncedFunction(() => {
count = 1;
}, time);

const { result, rerender } = renderDebouncedFunction(mockCallback1, time);
result.current.debounced();
rerender({
callback: () => {
count = 9999;
},
time,
});
rerender({ callback: mockCallback2, time });

await jest.runAllTimersAsync();
expect(count).toEqual(9999);
expect(mockCallback1).not.toBeCalled();
expect(mockCallback2).toBeCalledTimes(1);
});

it("Should reset the debounce timer with repeated calls to the method", async () => {
let count = 0;
const { result } = renderDebouncedFunction(() => {
count++;
}, 2000);
const mockCallback = jest.fn();
const { result } = renderDebouncedFunction(mockCallback, 2000);

for (let i = 0; i < 10; i++) {
setTimeout(() => {
Expand All @@ -161,23 +139,20 @@ describe(`${useDebouncedFunction.name}`, () => {
}

await jest.runAllTimersAsync();
expect(count).toBe(1);
expect(mockCallback).toBeCalledTimes(1);
});
});

describe("cancelDebounce function", () => {
it("Should be able to cancel a pending debounce", async () => {
let count = 0;
const { result } = renderDebouncedFunction(() => {
count++;
}, 2000);
const mockCallback = jest.fn();
const { result } = renderDebouncedFunction(mockCallback, 2000);

const { debounced, cancelDebounce } = result.current;
debounced();
cancelDebounce();
result.current.debounced();
result.current.cancelDebounce();

await jest.runAllTimersAsync();
expect(count).toEqual(0);
expect(mockCallback).not.toBeCalled();
});
});
});
21 changes: 11 additions & 10 deletions site/src/hooks/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { renderHook, waitFor } from "@testing-library/react";
import { dispatchCustomEvent } from "utils/events";
import { useCustomEvent } from "./events";

describe("useCustomEvent", () => {
it("should listem a custom event", async () => {
const callback = jest.fn();
const detail = { title: "Test event" };
renderHook(() => useCustomEvent("testEvent", callback));
dispatchCustomEvent("testEvent", detail);
await waitFor(() => {
expect(callback).toBeCalledTimes(1);
});
expect(callback.mock.calls[0][0].detail).toBe(detail);
describe(useCustomEvent.name, () => {
it("Should receive custom events dispatched by the dispatchCustomEvent function", async () => {
const mockCallback = jest.fn();
const eventType = "testEvent";
const detail = { title: "We have a new event!" };

renderHook(() => useCustomEvent(eventType, mockCallback));
dispatchCustomEvent(eventType, detail);

await waitFor(() => expect(mockCallback).toBeCalledTimes(1));
expect(mockCallback.mock.calls[0]?.[0]?.detail).toBe(detail);
});
});
4 changes: 2 additions & 2 deletions site/src/hooks/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from "react";
import { CustomEventListener } from "utils/events";
import { useEffectEvent } from "./hookPolyfills";
import { type CustomEventListener } from "utils/events";

/**
* Handles a custom event with descriptive type information.
Expand All @@ -21,5 +21,5 @@ export const useCustomEvent = <T, E extends string = string>(
return () => {
window.removeEventListener(eventType, stableListener as EventListener);
};
}, [eventType, stableListener]);
}, [stableListener, eventType]);
};
45 changes: 19 additions & 26 deletions site/src/hooks/hookPolyfills.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { renderHook } from "@testing-library/react";
import { useEffectEvent } from "./hookPolyfills";

function renderEffectEvent<TArgs extends unknown[], TReturn = unknown>(
callbackArg: (...args: TArgs) => TReturn,
) {
return renderHook(
({ callback }: { callback: typeof callbackArg }) => {
return useEffectEvent(callback);
},
{
initialProps: { callback: callbackArg },
},
);
}

describe(`${useEffectEvent.name}`, () => {
describe(useEffectEvent.name, () => {
function renderEffectEvent<TArgs extends unknown[], TReturn = unknown>(
callbackArg: (...args: TArgs) => TReturn,
) {
type Callback = typeof callbackArg;
type Props = Readonly<{ callback: Callback }>;

return renderHook<Callback, Props>(
({ callback }) => useEffectEvent(callback),
{ initialProps: { callback: callbackArg } },
);
}

it("Should maintain a stable reference across all renders", () => {
const callback = jest.fn();
const { result, rerender } = renderEffectEvent(callback);
Expand All @@ -29,20 +28,14 @@ describe(`${useEffectEvent.name}`, () => {
});

it("Should always call the most recent callback passed in", () => {
let value: "A" | "B" | "C" = "A";
const flipToB = () => {
value = "B";
};

const flipToC = () => {
value = "C";
};
const mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();

const { result, rerender } = renderEffectEvent(flipToB);
rerender({ callback: flipToC });
const { result, rerender } = renderEffectEvent(mockCallback1);
rerender({ callback: mockCallback2 });

result.current();
expect(value).toEqual("C");
expect.hasAssertions();
expect(mockCallback1).not.toBeCalled();
expect(mockCallback2).toBeCalledTimes(1);
});
});