Skip to content

Commit 6414b7a

Browse files
authored
chore(site): refactor tests for global hooks   (coder#12216)
* refactor: clean up tests for debounce * refactor: clean up tests for useCustomEvent * refactor: clean up events file * refactor: clean up tests for hookPolyfills
1 parent d6ae9d8 commit 6414b7a

File tree

4 files changed

+78
-109
lines changed

4 files changed

+78
-109
lines changed

site/src/hooks/debounce.test.ts

Lines changed: 46 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,21 @@ afterAll(() => {
1111
jest.clearAllMocks();
1212
});
1313

14-
// Most UI tests should be structure from the user's experience, but just
15-
// because these are more abstract, general-purpose hooks, it seemed harder to
16-
// do that. Had to bring in some mocks
17-
function renderDebouncedValue<T = unknown>(value: T, time: number) {
18-
return renderHook(
19-
({ value, time }: { value: T; time: number }) => {
20-
return useDebouncedValue(value, time);
21-
},
22-
{
23-
initialProps: { value, time },
24-
},
25-
);
26-
}
27-
28-
function renderDebouncedFunction<Args extends unknown[]>(
29-
callbackArg: (...args: Args) => void | Promise<void>,
30-
time: number,
31-
) {
32-
return renderHook(
33-
({ callback, time }: { callback: typeof callbackArg; time: number }) => {
34-
return useDebouncedFunction<Args>(callback, time);
35-
},
36-
{
37-
initialProps: { callback: callbackArg, time },
38-
},
39-
);
40-
}
41-
4214
describe(`${useDebouncedValue.name}`, () => {
15+
function renderDebouncedValue<T = unknown>(value: T, time: number) {
16+
return renderHook(
17+
({ value, time }: { value: T; time: number }) => {
18+
return useDebouncedValue(value, time);
19+
},
20+
{
21+
initialProps: { value, time },
22+
},
23+
);
24+
}
25+
4326
it("Should immediately return out the exact same value (by reference) on mount", () => {
4427
const value = {};
4528
const { result } = renderDebouncedValue(value, 2000);
46-
4729
expect(result.current).toBe(value);
4830
});
4931

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

8163
describe(`${useDebouncedFunction.name}`, () => {
64+
function renderDebouncedFunction<Args extends unknown[]>(
65+
callbackArg: (...args: Args) => void | Promise<void>,
66+
time: number,
67+
) {
68+
return renderHook(
69+
({ callback, time }: { callback: typeof callbackArg; time: number }) => {
70+
return useDebouncedFunction<Args>(callback, time);
71+
},
72+
{
73+
initialProps: { callback: callbackArg, time },
74+
},
75+
);
76+
}
77+
8278
describe("hook", () => {
8379
it("Should provide stable function references across re-renders", () => {
8480
const time = 5000;
@@ -97,62 +93,44 @@ describe(`${useDebouncedFunction.name}`, () => {
9793

9894
it("Resets any pending debounces if the timer argument changes", async () => {
9995
const time = 5000;
100-
let count = 0;
101-
const incrementCount = () => {
102-
count++;
103-
};
104-
105-
const { result, rerender } = renderDebouncedFunction(
106-
incrementCount,
107-
time,
108-
);
96+
const mockCallback = jest.fn();
97+
const { result, rerender } = renderDebouncedFunction(mockCallback, time);
10998

11099
result.current.debounced();
111-
rerender({ callback: incrementCount, time: time + 1 });
100+
rerender({ callback: mockCallback, time: time + 1 });
112101

113102
await jest.runAllTimersAsync();
114-
expect(count).toEqual(0);
103+
expect(mockCallback).not.toBeCalled();
115104
});
116105
});
117106

118107
describe("debounced function", () => {
119108
it("Resolve the debounce after specified milliseconds pass with no other calls", async () => {
120-
let value = false;
121-
const { result } = renderDebouncedFunction(() => {
122-
value = !value;
123-
}, 100);
124-
109+
const mockCallback = jest.fn();
110+
const { result } = renderDebouncedFunction(mockCallback, 100);
125111
result.current.debounced();
126112

127113
await jest.runOnlyPendingTimersAsync();
128-
expect(value).toBe(true);
114+
expect(mockCallback).toBeCalledTimes(1);
129115
});
130116

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

135-
const { result, rerender } = renderDebouncedFunction(() => {
136-
count = 1;
137-
}, time);
138-
122+
const { result, rerender } = renderDebouncedFunction(mockCallback1, time);
139123
result.current.debounced();
140-
rerender({
141-
callback: () => {
142-
count = 9999;
143-
},
144-
time,
145-
});
124+
rerender({ callback: mockCallback2, time });
146125

147126
await jest.runAllTimersAsync();
148-
expect(count).toEqual(9999);
127+
expect(mockCallback1).not.toBeCalled();
128+
expect(mockCallback2).toBeCalledTimes(1);
149129
});
150130

151131
it("Should reset the debounce timer with repeated calls to the method", async () => {
152-
let count = 0;
153-
const { result } = renderDebouncedFunction(() => {
154-
count++;
155-
}, 2000);
132+
const mockCallback = jest.fn();
133+
const { result } = renderDebouncedFunction(mockCallback, 2000);
156134

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

163141
await jest.runAllTimersAsync();
164-
expect(count).toBe(1);
142+
expect(mockCallback).toBeCalledTimes(1);
165143
});
166144
});
167145

168146
describe("cancelDebounce function", () => {
169147
it("Should be able to cancel a pending debounce", async () => {
170-
let count = 0;
171-
const { result } = renderDebouncedFunction(() => {
172-
count++;
173-
}, 2000);
148+
const mockCallback = jest.fn();
149+
const { result } = renderDebouncedFunction(mockCallback, 2000);
174150

175-
const { debounced, cancelDebounce } = result.current;
176-
debounced();
177-
cancelDebounce();
151+
result.current.debounced();
152+
result.current.cancelDebounce();
178153

179154
await jest.runAllTimersAsync();
180-
expect(count).toEqual(0);
155+
expect(mockCallback).not.toBeCalled();
181156
});
182157
});
183158
});

site/src/hooks/events.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { renderHook, waitFor } from "@testing-library/react";
22
import { dispatchCustomEvent } from "utils/events";
33
import { useCustomEvent } from "./events";
44

5-
describe("useCustomEvent", () => {
6-
it("should listem a custom event", async () => {
7-
const callback = jest.fn();
8-
const detail = { title: "Test event" };
9-
renderHook(() => useCustomEvent("testEvent", callback));
10-
dispatchCustomEvent("testEvent", detail);
11-
await waitFor(() => {
12-
expect(callback).toBeCalledTimes(1);
13-
});
14-
expect(callback.mock.calls[0][0].detail).toBe(detail);
5+
describe(useCustomEvent.name, () => {
6+
it("Should receive custom events dispatched by the dispatchCustomEvent function", async () => {
7+
const mockCallback = jest.fn();
8+
const eventType = "testEvent";
9+
const detail = { title: "We have a new event!" };
10+
11+
renderHook(() => useCustomEvent(eventType, mockCallback));
12+
dispatchCustomEvent(eventType, detail);
13+
14+
await waitFor(() => expect(mockCallback).toBeCalledTimes(1));
15+
expect(mockCallback.mock.calls[0]?.[0]?.detail).toBe(detail);
1516
});
1617
});

site/src/hooks/events.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect } from "react";
2-
import { CustomEventListener } from "utils/events";
32
import { useEffectEvent } from "./hookPolyfills";
3+
import { type CustomEventListener } from "utils/events";
44

55
/**
66
* Handles a custom event with descriptive type information.
@@ -21,5 +21,5 @@ export const useCustomEvent = <T, E extends string = string>(
2121
return () => {
2222
window.removeEventListener(eventType, stableListener as EventListener);
2323
};
24-
}, [eventType, stableListener]);
24+
}, [stableListener, eventType]);
2525
};

site/src/hooks/hookPolyfills.test.ts

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { renderHook } from "@testing-library/react";
22
import { useEffectEvent } from "./hookPolyfills";
33

4-
function renderEffectEvent<TArgs extends unknown[], TReturn = unknown>(
5-
callbackArg: (...args: TArgs) => TReturn,
6-
) {
7-
return renderHook(
8-
({ callback }: { callback: typeof callbackArg }) => {
9-
return useEffectEvent(callback);
10-
},
11-
{
12-
initialProps: { callback: callbackArg },
13-
},
14-
);
15-
}
16-
17-
describe(`${useEffectEvent.name}`, () => {
4+
describe(useEffectEvent.name, () => {
5+
function renderEffectEvent<TArgs extends unknown[], TReturn = unknown>(
6+
callbackArg: (...args: TArgs) => TReturn,
7+
) {
8+
type Callback = typeof callbackArg;
9+
type Props = Readonly<{ callback: Callback }>;
10+
11+
return renderHook<Callback, Props>(
12+
({ callback }) => useEffectEvent(callback),
13+
{ initialProps: { callback: callbackArg } },
14+
);
15+
}
16+
1817
it("Should maintain a stable reference across all renders", () => {
1918
const callback = jest.fn();
2019
const { result, rerender } = renderEffectEvent(callback);
@@ -29,20 +28,14 @@ describe(`${useEffectEvent.name}`, () => {
2928
});
3029

3130
it("Should always call the most recent callback passed in", () => {
32-
let value: "A" | "B" | "C" = "A";
33-
const flipToB = () => {
34-
value = "B";
35-
};
36-
37-
const flipToC = () => {
38-
value = "C";
39-
};
31+
const mockCallback1 = jest.fn();
32+
const mockCallback2 = jest.fn();
4033

41-
const { result, rerender } = renderEffectEvent(flipToB);
42-
rerender({ callback: flipToC });
34+
const { result, rerender } = renderEffectEvent(mockCallback1);
35+
rerender({ callback: mockCallback2 });
4336

4437
result.current();
45-
expect(value).toEqual("C");
46-
expect.hasAssertions();
38+
expect(mockCallback1).not.toBeCalled();
39+
expect(mockCallback2).toBeCalledTimes(1);
4740
});
4841
});

0 commit comments

Comments
 (0)