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

* refactor: clean up tests for debounce

* refactor: clean up tests for useCustomEvent

* refactor: clean up events file

* refactor: clean up tests for hookPolyfills
This commit is contained in:
Michael Smith
2024-02-20 09:19:43 -05:00
committed by GitHub
parent d6ae9d8548
commit 6414b7aade
4 changed files with 77 additions and 108 deletions
+46 -71
View File
@@ -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);
});
@@ -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;
@@ -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(() => {
@@ -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();
});
});
});
+11 -10
View File
@@ -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);
});
});
+2 -2
View File
@@ -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.
@@ -21,5 +21,5 @@ export const useCustomEvent = <T, E extends string = string>(
return () => {
window.removeEventListener(eventType, stableListener as EventListener);
};
}, [eventType, stableListener]);
}, [stableListener, eventType]);
};
+18 -25
View File
@@ -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, () => {
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 } },
);
}
describe(`${useEffectEvent.name}`, () => {
it("Should maintain a stable reference across all renders", () => {
const callback = jest.fn();
const { result, rerender } = renderEffectEvent(callback);
@@ -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 mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();
const flipToC = () => {
value = "C";
};
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);
});
});