mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user