From 97bf126e43108babb0c5e2be70e769cf7e360ec6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 19 Feb 2024 22:46:52 +0000 Subject: [PATCH 1/4] refactor: clean up tests for debounce --- site/src/hooks/debounce.test.ts | 117 +++++++++++++------------------- 1 file changed, 46 insertions(+), 71 deletions(-) diff --git a/site/src/hooks/debounce.test.ts b/site/src/hooks/debounce.test.ts index b1447afe4a891..e7c7e485e2d54 100644 --- a/site/src/hooks/debounce.test.ts +++ b/site/src/hooks/debounce.test.ts @@ -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(value: T, time: number) { - return renderHook( - ({ value, time }: { value: T; time: number }) => { - return useDebouncedValue(value, time); - }, - { - initialProps: { value, time }, - }, - ); -} - -function renderDebouncedFunction( - callbackArg: (...args: Args) => void | Promise, - time: number, -) { - return renderHook( - ({ callback, time }: { callback: typeof callbackArg; time: number }) => { - return useDebouncedFunction(callback, time); - }, - { - initialProps: { callback: callbackArg, time }, - }, - ); -} - describe(`${useDebouncedValue.name}`, () => { + function renderDebouncedValue(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( + callbackArg: (...args: Args) => void | Promise, + time: number, + ) { + return renderHook( + ({ callback, time }: { callback: typeof callbackArg; time: number }) => { + return useDebouncedFunction(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(); }); }); }); From 3da013a8c36bd5b6a59ac6bc7df10d28be5242d3 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 19 Feb 2024 22:47:38 +0000 Subject: [PATCH 2/4] refactor: clean up tests for useCustomEvent --- site/src/hooks/events.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/site/src/hooks/events.test.ts b/site/src/hooks/events.test.ts index 176dc448316ad..25324bfd8f5ab 100644 --- a/site/src/hooks/events.test.ts +++ b/site/src/hooks/events.test.ts @@ -3,14 +3,15 @@ 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); + 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); }); }); From 13c530961094d7680dbc1ab3018921ada2922fae Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 19 Feb 2024 22:48:33 +0000 Subject: [PATCH 3/4] refactor: clean up events file --- site/src/hooks/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/hooks/events.ts b/site/src/hooks/events.ts index 82004ea1d7ed5..68e6f0c59f153 100644 --- a/site/src/hooks/events.ts +++ b/site/src/hooks/events.ts @@ -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 = ( return () => { window.removeEventListener(eventType, stableListener as EventListener); }; - }, [eventType, stableListener]); + }, [stableListener, eventType]); }; From fad1bca264abb851c299f34c22e33d62e360ac30 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Mon, 19 Feb 2024 22:51:48 +0000 Subject: [PATCH 4/4] refactor: clean up tests for hookPolyfills --- site/src/hooks/events.test.ts | 2 +- site/src/hooks/hookPolyfills.test.ts | 45 ++++++++++++---------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/site/src/hooks/events.test.ts b/site/src/hooks/events.test.ts index 25324bfd8f5ab..56a693a6351c3 100644 --- a/site/src/hooks/events.test.ts +++ b/site/src/hooks/events.test.ts @@ -2,7 +2,7 @@ import { renderHook, waitFor } from "@testing-library/react"; import { dispatchCustomEvent } from "utils/events"; import { useCustomEvent } from "./events"; -describe("useCustomEvent", () => { +describe(useCustomEvent.name, () => { it("Should receive custom events dispatched by the dispatchCustomEvent function", async () => { const mockCallback = jest.fn(); const eventType = "testEvent"; diff --git a/site/src/hooks/hookPolyfills.test.ts b/site/src/hooks/hookPolyfills.test.ts index 1831bf6f6d2a1..274b2f68aa667 100644 --- a/site/src/hooks/hookPolyfills.test.ts +++ b/site/src/hooks/hookPolyfills.test.ts @@ -1,20 +1,19 @@ import { renderHook } from "@testing-library/react"; import { useEffectEvent } from "./hookPolyfills"; -function renderEffectEvent( - callbackArg: (...args: TArgs) => TReturn, -) { - return renderHook( - ({ callback }: { callback: typeof callbackArg }) => { - return useEffectEvent(callback); - }, - { - initialProps: { callback: callbackArg }, - }, - ); -} - -describe(`${useEffectEvent.name}`, () => { +describe(useEffectEvent.name, () => { + function renderEffectEvent( + callbackArg: (...args: TArgs) => TReturn, + ) { + type Callback = typeof callbackArg; + type Props = Readonly<{ callback: Callback }>; + + return renderHook( + ({ callback }) => useEffectEvent(callback), + { initialProps: { callback: callbackArg } }, + ); + } + 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 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); }); });