From 73360026ab7bb76bd869a1724cf49aa6ffe60558 Mon Sep 17 00:00:00 2001 From: alunturner <56027671+alunturner@users.noreply.github.com> Date: Thu, 16 Feb 2023 14:13:05 +0000 Subject: [PATCH] Convert remaining hook tests to RTL (#10166) * convert useProfileInfo to RTL * convert useLatestResult to RTL --- test/hooks/useLatestResult-test.tsx | 170 ++++++++++++++++++---------- test/hooks/useProfileInfo-test.tsx | 124 +++++++------------- 2 files changed, 146 insertions(+), 148 deletions(-) diff --git a/test/hooks/useLatestResult-test.tsx b/test/hooks/useLatestResult-test.tsx index 5c0eb2ecf9..a561a82c09 100644 --- a/test/hooks/useLatestResult-test.tsx +++ b/test/hooks/useLatestResult-test.tsx @@ -14,77 +14,123 @@ See the License for the specific language governing permissions and limitations under the License. */ -// eslint-disable-next-line deprecate/import -import { mount } from "enzyme"; -import { sleep } from "matrix-js-sdk/src/utils"; -import React, { useEffect, useState } from "react"; -import { act } from "react-dom/test-utils"; +import { renderHook, RenderHookResult } from "@testing-library/react-hooks/dom"; import { useLatestResult } from "../../src/hooks/useLatestResult"; -function LatestResultsComponent({ query, doRequest }: { query: number; doRequest(query: number): Promise }) { - const [value, setValueInternal] = useState(0); - const [updateQuery, updateResult] = useLatestResult(setValueInternal); - useEffect(() => { - updateQuery(query); - doRequest(query).then((it: number) => { - updateResult(query, it); - }); - }, [doRequest, query, updateQuery, updateResult]); +// All tests use fake timers throughout, comments will show the elapsed time in ms +jest.useFakeTimers(); - return
{value}
; +const mockSetter = jest.fn(); + +beforeEach(() => { + mockSetter.mockClear(); +}); + +function simulateRequest( + hookResult: RenderHookResult>["result"], + { id, delayInMs, result }: { id: string; delayInMs: number; result: string }, +) { + const [setQuery, setResult] = hookResult.current; + setQuery(id); + setTimeout(() => setResult(id, result), delayInMs); } -describe("useLatestResult", () => { - it("should return results", async () => { - const doRequest = async (query: number) => { - await sleep(180); - return query; - }; +describe("renderhook tests", () => { + it("should return a result", () => { + const { result: hookResult } = renderHook(() => useLatestResult(mockSetter)); - const wrapper = mount(); - await act(async () => { - await sleep(100); - }); - expect(wrapper.text()).toEqual("0"); - wrapper.setProps({ doRequest, query: 1 }); - await act(async () => { - await sleep(70); - }); - wrapper.setProps({ doRequest, query: 2 }); - await act(async () => { - await sleep(70); - }); - expect(wrapper.text()).toEqual("0"); - await act(async () => { - await sleep(120); - }); - expect(wrapper.text()).toEqual("2"); + const query = { id: "query1", delayInMs: 100, result: "result1" }; + simulateRequest(hookResult, query); + + // check we have made no calls to the setter + expect(mockSetter).not.toHaveBeenCalled(); + + // advance timer until the timeout elapses, check we have called the setter + jest.advanceTimersToNextTimer(); + expect(mockSetter).toHaveBeenCalledTimes(1); + expect(mockSetter).toHaveBeenLastCalledWith(query.result); }); - it("should prevent out-of-order results", async () => { - const doRequest = async (query: number) => { - await sleep(query); - return query; - }; + it("should not let a slower response to an earlier query overwrite the result of a later query", () => { + const { result: hookResult } = renderHook(() => useLatestResult(mockSetter)); - const wrapper = mount(); - await act(async () => { - await sleep(5); - }); - expect(wrapper.text()).toEqual("0"); - wrapper.setProps({ doRequest, query: 50 }); - await act(async () => { - await sleep(5); - }); - wrapper.setProps({ doRequest, query: 1 }); - await act(async () => { - await sleep(5); - }); - expect(wrapper.text()).toEqual("1"); - await act(async () => { - await sleep(50); - }); - expect(wrapper.text()).toEqual("1"); + const slowQuery = { id: "slowQuery", delayInMs: 500, result: "slowResult" }; + const fastQuery = { id: "fastQuery", delayInMs: 100, result: "fastResult" }; + + simulateRequest(hookResult, slowQuery); + simulateRequest(hookResult, fastQuery); + + // advance to fastQuery response, check the setter call + jest.advanceTimersToNextTimer(); + expect(mockSetter).toHaveBeenCalledTimes(1); + expect(mockSetter).toHaveBeenLastCalledWith(fastQuery.result); + + // advance time to slowQuery response, check the setter has _not_ been + // called again and that the result is still from the fast query + jest.advanceTimersToNextTimer(); + expect(mockSetter).toHaveBeenCalledTimes(1); + expect(mockSetter).toHaveBeenLastCalledWith(fastQuery.result); + }); + + it("should return expected results when all response times similar", () => { + const { result: hookResult } = renderHook(() => useLatestResult(mockSetter)); + + const commonDelayInMs = 180; + const query1 = { id: "q1", delayInMs: commonDelayInMs, result: "r1" }; + const query2 = { id: "q2", delayInMs: commonDelayInMs, result: "r2" }; + const query3 = { id: "q3", delayInMs: commonDelayInMs, result: "r3" }; + + // ELAPSED: 0ms, no queries sent + simulateRequest(hookResult, query1); + jest.advanceTimersByTime(100); + + // ELAPSED: 100ms, query1 sent, no responses + expect(mockSetter).not.toHaveBeenCalled(); + simulateRequest(hookResult, query2); + jest.advanceTimersByTime(70); + + // ELAPSED: 170ms, query1 and query2 sent, no responses + expect(mockSetter).not.toHaveBeenCalled(); + simulateRequest(hookResult, query3); + jest.advanceTimersByTime(70); + + // ELAPSED: 240ms, all queries sent, responses for query1 and query2 + expect(mockSetter).not.toHaveBeenCalled(); + + // ELAPSED: 360ms, all queries sent, all queries have responses + jest.advanceTimersByTime(120); + expect(mockSetter).toHaveBeenLastCalledWith(query3.result); + }); + + it("should prevent out of order results", () => { + const { result: hookResult } = renderHook(() => useLatestResult(mockSetter)); + + const query1 = { id: "q1", delayInMs: 0, result: "r1" }; + const query2 = { id: "q2", delayInMs: 50, result: "r2" }; + const query3 = { id: "q3", delayInMs: 1, result: "r3" }; + + // ELAPSED: 0ms, no queries sent + simulateRequest(hookResult, query1); + jest.advanceTimersByTime(5); + + // ELAPSED: 5ms, query1 sent, response from query1 + expect(mockSetter).toHaveBeenCalledTimes(1); + expect(mockSetter).toHaveBeenLastCalledWith(query1.result); + simulateRequest(hookResult, query2); + jest.advanceTimersByTime(5); + + // ELAPSED: 10ms, query1 and query2 sent, response from query1 + simulateRequest(hookResult, query3); + jest.advanceTimersByTime(5); + + // ELAPSED: 15ms, all queries sent, responses from query1 and query3 + expect(mockSetter).toHaveBeenCalledTimes(2); + expect(mockSetter).toHaveBeenLastCalledWith(query3.result); + + // ELAPSED: 65ms, all queries sent, all queries have responses + // so check that the result is still from query3, not query2 + jest.advanceTimersByTime(50); + expect(mockSetter).toHaveBeenLastCalledWith(query3.result); }); }); diff --git a/test/hooks/useProfileInfo-test.tsx b/test/hooks/useProfileInfo-test.tsx index 2619b7354d..8416d7c893 100644 --- a/test/hooks/useProfileInfo-test.tsx +++ b/test/hooks/useProfileInfo-test.tsx @@ -14,28 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -// eslint-disable-next-line deprecate/import -import { mount } from "enzyme"; +import { waitFor } from "@testing-library/react"; +import { renderHook, act } from "@testing-library/react-hooks/dom"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { sleep } from "matrix-js-sdk/src/utils"; -import React from "react"; -import { act } from "react-dom/test-utils"; import { useProfileInfo } from "../../src/hooks/useProfileInfo"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import { stubClient } from "../test-utils/test-utils"; -function ProfileInfoComponent({ onClick }: { onClick(hook: ReturnType): void }) { - const profileInfo = useProfileInfo(); - - const { ready, loading, profile } = profileInfo; - - return ( -
onClick(profileInfo)}> - {(!ready || loading) && `ready: ${ready}, loading: ${loading}`} - {profile && `Name: ${profile.display_name}`} -
- ); +function render() { + return renderHook(() => useProfileInfo()); } describe("useProfileInfo", () => { @@ -55,66 +43,44 @@ describe("useProfileInfo", () => { it("should display user profile when searching", async () => { const query = "@user:home.server"; - const wrapper = mount( - { - hook.search({ - query, - }); - }} - />, - ); + const { result } = render(); - await act(async () => { - await sleep(1); - wrapper.simulate("click"); - return act(() => sleep(1)); + act(() => { + result.current.search({ query }); }); - expect(wrapper.text()).toContain(query); + await waitFor(() => expect(result.current.ready).toBe(true)); + + expect(result.current.profile?.display_name).toBe(query); }); it("should work with empty queries", async () => { - const wrapper = mount( - { - hook.search({ - query: "", - }); - }} - />, - ); + const query = ""; - await act(async () => { - await sleep(1); - wrapper.simulate("click"); - return act(() => sleep(1)); + const { result } = render(); + + act(() => { + result.current.search({ query }); }); - expect(wrapper.text()).toBe(""); + await waitFor(() => expect(result.current.ready).toBe(true)); + + expect(result.current.profile).toBeNull(); }); it("should treat invalid mxids as empty queries", async () => { const queries = ["@user", "user@home.server"]; for (const query of queries) { - const wrapper = mount( - { - hook.search({ - query, - }); - }} - />, - ); + const { result } = render(); - await act(async () => { - await sleep(1); - wrapper.simulate("click"); - return act(() => sleep(1)); + act(() => { + result.current.search({ query }); }); - expect(wrapper.text()).toBe(""); + await waitFor(() => expect(result.current.ready).toBe(true)); + + expect(result.current.profile).toBeNull(); } }); @@ -124,43 +90,29 @@ describe("useProfileInfo", () => { }; const query = "@user:home.server"; - const wrapper = mount( - { - hook.search({ - query, - }); - }} - />, - ); - await act(async () => { - await sleep(1); - wrapper.simulate("click"); - return act(() => sleep(1)); + const { result } = render(); + + act(() => { + result.current.search({ query }); }); - expect(wrapper.text()).toBe(""); + await waitFor(() => expect(result.current.ready).toBe(true)); + + expect(result.current.profile).toBeNull(); }); it("should be able to handle an empty result", async () => { cli.getProfileInfo = () => null as unknown as Promise<{}>; const query = "@user:home.server"; - const wrapper = mount( - { - hook.search({ - query, - }); - }} - />, - ); - await act(async () => { - await sleep(1); - wrapper.simulate("click"); - return act(() => sleep(1)); + const { result } = render(); + + act(() => { + result.current.search({ query }); }); - expect(wrapper.text()).toBe(""); + await waitFor(() => expect(result.current.ready).toBe(true)); + + expect(result.current.profile?.display_name).toBeUndefined(); }); });