From 740b734b44c8b2f979ad778996670ab4318e11a2 Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Fri, 13 Oct 2023 18:20:00 +0300 Subject: [PATCH 01/10] move `absolute` and `relative` paths to core functionality --- packages/wouter/src/index.js | 14 +++++++++++++- packages/wouter/src/memory-location.js | 12 +++--------- packages/wouter/src/use-browser-location.js | 12 ++---------- packages/wouter/src/use-hash-location.js | 19 +++++++------------ packages/wouter/test/memory-location.test.ts | 4 ++-- .../wouter/test/use-browser-location.test.tsx | 10 +++++----- 6 files changed, 32 insertions(+), 39 deletions(-) diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js index 9d1a9462..af4cb10b 100644 --- a/packages/wouter/src/index.js +++ b/packages/wouter/src/index.js @@ -14,6 +14,7 @@ import { useIsomorphicLayoutEffect, useEvent, } from "./react-deps.js"; +import { absolutePath, relativePath } from "./paths.js"; /* * Router and router context. Router is a lightweight object that represents the current @@ -41,7 +42,18 @@ export const useRouter = () => useContext(RouterCtx); */ // Internal version of useLocation to avoid redundant useRouter calls -const useLocationFromRouter = (router) => router.hook(router); + +const useLocationFromRouter = (router) => { + const [location, navigate] = router.hook(router); + + // the function reference should stay the same between re-renders, so that + // it can be passed down as an element prop without any performance concerns. + // (This is achieved via `useEvent`.) + return [ + relativePath(router.base, location), + useEvent((to, navOpts) => navigate(absolutePath(to, router.base), navOpts)), + ]; +}; export const useLocation = () => useLocationFromRouter(useRouter()); diff --git a/packages/wouter/src/memory-location.js b/packages/wouter/src/memory-location.js index bab1ba9f..f9ebe717 100644 --- a/packages/wouter/src/memory-location.js +++ b/packages/wouter/src/memory-location.js @@ -1,6 +1,5 @@ import mitt from "mitt"; -import { useSyncExternalStore, useEvent } from "./react-deps.js"; -import { absolutePath, relativePath } from "./paths.js"; +import { useSyncExternalStore } from "./react-deps.js"; /** * In-memory location that supports navigation @@ -35,13 +34,8 @@ export const memoryLocation = ({ return () => emitter.off("navigate", cb); }; - const useMemoryLocation = ({ base } = {}) => { - const location = useSyncExternalStore(subscribe, () => currentPath); - - return [ - relativePath(base, location), - useEvent((to, options) => navigate(absolutePath(to, base), options)), - ]; + const useMemoryLocation = () => { + return [useSyncExternalStore(subscribe, () => currentPath), navigate]; }; return { diff --git a/packages/wouter/src/use-browser-location.js b/packages/wouter/src/use-browser-location.js index f3d03e0f..22ffb8ec 100644 --- a/packages/wouter/src/use-browser-location.js +++ b/packages/wouter/src/use-browser-location.js @@ -1,5 +1,4 @@ -import { useSyncExternalStore, useEvent } from "./react-deps.js"; -import { absolutePath, relativePath } from "./paths.js"; +import { useSyncExternalStore } from "./react-deps.js"; /** * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History @@ -51,14 +50,7 @@ export const navigate = (to, { replace = false, state = null } = {}) => // the 2nd argument of the `useBrowserLocation` return value is a function // that allows to perform a navigation. -// -// the function reference should stay the same between re-renders, so that -// it can be passed down as an element prop without any performance concerns. -// (This is achieved via `useEvent`.) -export const useBrowserLocation = (opts = {}) => [ - relativePath(opts.base, usePathname(opts)), - useEvent((to, navOpts) => navigate(absolutePath(to, opts.base), navOpts)), -]; +export const useBrowserLocation = (opts = {}) => [usePathname(opts), navigate]; const patchKey = Symbol.for("wouter_v3"); diff --git a/packages/wouter/src/use-hash-location.js b/packages/wouter/src/use-hash-location.js index 833019e1..b9b4bc72 100644 --- a/packages/wouter/src/use-hash-location.js +++ b/packages/wouter/src/use-hash-location.js @@ -1,5 +1,4 @@ -import { useSyncExternalStore, useEvent } from "./react-deps.js"; -import { absolutePath, relativePath } from "./paths.js"; +import { useSyncExternalStore } from "./react-deps.js"; // fortunately `hashchange` is a native event, so there is no need to // patch `history` object (unlike `pushState/replaceState` events) @@ -21,15 +20,11 @@ export const navigate = (to, { state = null } = {}) => { ); }; -export const useHashLocation = ({ base, ssrPath = "/" } = {}) => [ - relativePath( - base, - useSyncExternalStore( - subscribeToHashUpdates, - currentHashLocation, - () => ssrPath - ) +export const useHashLocation = ({ ssrPath = "/" } = {}) => [ + useSyncExternalStore( + subscribeToHashUpdates, + currentHashLocation, + () => ssrPath ), - - useEvent((to, navOpts) => navigate(absolutePath(to, base), navOpts)), + navigate, ]; diff --git a/packages/wouter/test/memory-location.test.ts b/packages/wouter/test/memory-location.test.ts index 9ee97618..7ef06479 100644 --- a/packages/wouter/test/memory-location.test.ts +++ b/packages/wouter/test/memory-location.test.ts @@ -33,7 +33,7 @@ it('should return location hook that has initial path "/" by default', () => { unmount(); }); -it("should return location hook that supports `base` option for nested routing", () => { +it.skip("should return location hook that supports `base` option for nested routing", () => { const { hook } = memoryLocation({ path: "/nested/test" }); const { result, unmount } = renderHook(() => hook({ base: "/nested" })); @@ -43,7 +43,7 @@ it("should return location hook that supports `base` option for nested routing", unmount(); }); -it("should return location hook that handle `base` option while navigation", () => { +it.skip("should return location hook that handle `base` option while navigation", () => { const { hook, history } = memoryLocation({ path: "/nested/test", record: true, diff --git a/packages/wouter/test/use-browser-location.test.tsx b/packages/wouter/test/use-browser-location.test.tsx index ff28e5cb..80b05f1e 100644 --- a/packages/wouter/test/use-browser-location.test.tsx +++ b/packages/wouter/test/use-browser-location.test.tsx @@ -51,7 +51,7 @@ describe("`value` first argument", () => { unmount(); }); - it("returns a pathname without a basepath", () => { + it.skip("returns a pathname without a basepath", () => { const { result, unmount } = renderHook(() => useBrowserLocation({ base: "/app" }) ); @@ -61,7 +61,7 @@ describe("`value` first argument", () => { unmount(); }); - it("returns `/` when URL contains only a basepath", () => { + it.skip("returns `/` when URL contains only a basepath", () => { const { result, unmount } = renderHook(() => useBrowserLocation({ base: "/app" }) ); @@ -71,7 +71,7 @@ describe("`value` first argument", () => { unmount(); }); - it("basepath should be case-insensitive", () => { + it.skip("basepath should be case-insensitive", () => { const { result, unmount } = renderHook(() => useBrowserLocation({ base: "/MyApp" }) ); @@ -81,7 +81,7 @@ describe("`value` first argument", () => { unmount(); }); - it("returns an absolute path in case of unmatched base path", () => { + it.skip("returns an absolute path in case of unmatched base path", () => { const { result, unmount } = renderHook(() => useBrowserLocation({ base: "/MyApp" }) ); @@ -216,7 +216,7 @@ describe("`update` second parameter", () => { unmount(); }); - it("supports a basepath", () => { + it.skip("supports a basepath", () => { const { result, unmount } = renderHook(() => useBrowserLocation({ base: "/app" }) ); From fcb9ca56fcc0d6aa447f116a9861ba421f3b9590 Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Sun, 15 Oct 2023 12:52:06 +0300 Subject: [PATCH 02/10] implement test spec for all locations --- packages/wouter/test/memory-location.test.ts | 30 ---- .../wouter/test/use-browser-location.test.tsx | 51 ------ packages/wouter/test/use-location.test.tsx | 150 ++++++++++++++++++ 3 files changed, 150 insertions(+), 81 deletions(-) create mode 100644 packages/wouter/test/use-location.test.tsx diff --git a/packages/wouter/test/memory-location.test.ts b/packages/wouter/test/memory-location.test.ts index 7ef06479..44c5d03e 100644 --- a/packages/wouter/test/memory-location.test.ts +++ b/packages/wouter/test/memory-location.test.ts @@ -33,36 +33,6 @@ it('should return location hook that has initial path "/" by default', () => { unmount(); }); -it.skip("should return location hook that supports `base` option for nested routing", () => { - const { hook } = memoryLocation({ path: "/nested/test" }); - - const { result, unmount } = renderHook(() => hook({ base: "/nested" })); - const [value] = result.current; - - expect(value).toBe("/test"); - unmount(); -}); - -it.skip("should return location hook that handle `base` option while navigation", () => { - const { hook, history } = memoryLocation({ - path: "/nested/test", - record: true, - }); - - const { result, unmount } = renderHook(() => hook({ base: "/nested" })); - - act(() => result.current[1]("/change-1")); - act(() => result.current[1]("/change-2")); - - expect(history).toStrictEqual([ - "/nested/test", - "/nested/change-1", - "/nested/change-2", - ]); - - unmount(); -}); - it("should return standalone `navigate` method", () => { const { hook, navigate } = memoryLocation(); diff --git a/packages/wouter/test/use-browser-location.test.tsx b/packages/wouter/test/use-browser-location.test.tsx index 80b05f1e..bb26fee5 100644 --- a/packages/wouter/test/use-browser-location.test.tsx +++ b/packages/wouter/test/use-browser-location.test.tsx @@ -51,46 +51,6 @@ describe("`value` first argument", () => { unmount(); }); - it.skip("returns a pathname without a basepath", () => { - const { result, unmount } = renderHook(() => - useBrowserLocation({ base: "/app" }) - ); - - act(() => history.pushState(null, "", "/app/dashboard")); - expect(result.current[0]).toBe("/dashboard"); - unmount(); - }); - - it.skip("returns `/` when URL contains only a basepath", () => { - const { result, unmount } = renderHook(() => - useBrowserLocation({ base: "/app" }) - ); - - act(() => history.pushState(null, "", "/app")); - expect(result.current[0]).toBe("/"); - unmount(); - }); - - it.skip("basepath should be case-insensitive", () => { - const { result, unmount } = renderHook(() => - useBrowserLocation({ base: "/MyApp" }) - ); - - act(() => history.pushState(null, "", "/myAPP/users/JohnDoe")); - expect(result.current[0]).toBe("/users/JohnDoe"); - unmount(); - }); - - it.skip("returns an absolute path in case of unmatched base path", () => { - const { result, unmount } = renderHook(() => - useBrowserLocation({ base: "/MyApp" }) - ); - - act(() => history.pushState(null, "", "/MyOtherApp/users/JohnDoe")); - expect(result.current[0]).toBe("~/MyOtherApp/users/JohnDoe"); - unmount(); - }); - it("supports search url", () => { // count how many times each hook is rendered const locationRenders = { current: 0 }; @@ -215,15 +175,4 @@ describe("`update` second parameter", () => { expect(updateWas).toBe(updateNow); unmount(); }); - - it.skip("supports a basepath", () => { - const { result, unmount } = renderHook(() => - useBrowserLocation({ base: "/app" }) - ); - const update = result.current[1]; - - act(() => update("/dashboard")); - expect(location.pathname).toBe("/app/dashboard"); - unmount(); - }); }); diff --git a/packages/wouter/test/use-location.test.tsx b/packages/wouter/test/use-location.test.tsx new file mode 100644 index 00000000..9850b005 --- /dev/null +++ b/packages/wouter/test/use-location.test.tsx @@ -0,0 +1,150 @@ +import { ComponentProps, ReactNode } from "react"; +import { it, expect, describe } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import { Router, useLocation } from "wouter"; +import { + useBrowserLocation, + navigate as browserNavigation, + BaseLocationHook, +} from "wouter/use-browser-location"; + +import { + useHashLocation, + navigate as hashNavigation, +} from "wouter/use-hash-location"; + +import { memoryLocation } from "wouter/memory-location"; + +function createContainer( + options: Omit, "children"> = {} +) { + return ({ children }: { children: ReactNode }) => ( + {children} + ); +} + +type StubType = { + name: string; + hook: BaseLocationHook; + location: () => string; + navigate: ReturnType[1]; +}; + +function createLocationSpec(stub: StubType) { + describe(stub.name, () => { + it("returns a pair [value, update]", () => { + const { result, unmount } = renderHook(() => useLocation(), { + wrapper: createContainer({ hook: stub.hook }), + }); + const [value, update] = result.current; + + expect(typeof value).toBe("string"); + expect(typeof update).toBe("function"); + unmount(); + }); + + describe("`value` first argument", () => { + it("returns `/` when URL contains only a basepath", () => { + const { result, unmount } = renderHook(() => useLocation(), { + wrapper: createContainer({ + base: "/app", + hook: stub.hook, + }), + }); + + act(() => stub.navigate("/app")); + expect(result.current[0]).toBe("/"); + unmount(); + }); + + it("basepath should be case-insensitive", () => { + const { result, unmount } = renderHook(() => useLocation(), { + wrapper: createContainer({ + base: "/MyApp", + hook: stub.hook, + }), + }); + + act(() => stub.navigate("/myAPP/users/JohnDoe")); + expect(result.current[0]).toBe("/users/JohnDoe"); + unmount(); + }); + + it("returns an absolute path in case of unmatched base path", () => { + const { result, unmount } = renderHook(() => useLocation(), { + wrapper: createContainer({ + base: "/MyApp", + hook: stub.hook, + }), + }); + + act(() => stub.navigate("/MyOtherApp/users/JohnDoe")); + expect(result.current[0]).toBe("~/MyOtherApp/users/JohnDoe"); + unmount(); + }); + }); + + describe("`update` second parameter", () => { + it("rerenders the component", () => { + const { result, unmount } = renderHook(() => useLocation(), { + wrapper: createContainer({ hook: stub.hook }), + }); + const update = result.current[1]; + + act(() => update("/about")); + expect(stub.location()).toBe("/about"); + unmount(); + }); + + it("stays the same reference between re-renders (function ref)", () => { + const { result, rerender, unmount } = renderHook(() => useLocation(), { + wrapper: createContainer({ hook: stub.hook }), + }); + + const updateWas = result.current[1]; + rerender(); + const updateNow = result.current[1]; + + expect(updateWas).toBe(updateNow); + unmount(); + }); + + it("supports a basepath", () => { + const { result, unmount } = renderHook(() => useLocation(), { + wrapper: createContainer({ + base: "/app", + hook: stub.hook, + }), + }); + + const update = result.current[1]; + + act(() => update("/dashboard")); + expect(stub.location()).toBe("/app/dashboard"); + unmount(); + }); + }); + }); +} + +createLocationSpec({ + name: "useBrowserLocation", + hook: useBrowserLocation, + location: () => location.pathname, + navigate: browserNavigation, +}); + +createLocationSpec({ + name: "useHashLocation", + hook: useHashLocation, + location: () => location.hash.replace(/^#?\/?/, ""), + navigate: hashNavigation, +}); + +const memory = memoryLocation({ record: true }); +createLocationSpec({ + name: "memoryLocation", + hook: memory.hook, + location: () => memory.history.at(-1) ?? "", + navigate: memory.navigate, +}); From 490548dc36e4d533d2bc28589a4b1d3bae459a33 Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Sun, 15 Oct 2023 13:33:11 +0300 Subject: [PATCH 03/10] clean history before each test --- packages/wouter/test/use-location.test.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/wouter/test/use-location.test.tsx b/packages/wouter/test/use-location.test.tsx index 9850b005..584e33e2 100644 --- a/packages/wouter/test/use-location.test.tsx +++ b/packages/wouter/test/use-location.test.tsx @@ -1,5 +1,5 @@ import { ComponentProps, ReactNode } from "react"; -import { it, expect, describe } from "vitest"; +import { it, expect, describe, beforeEach } from "vitest"; import { renderHook, act } from "@testing-library/react"; import { Router, useLocation } from "wouter"; import { @@ -28,10 +28,13 @@ type StubType = { hook: BaseLocationHook; location: () => string; navigate: ReturnType[1]; + clear: () => void; }; function createLocationSpec(stub: StubType) { describe(stub.name, () => { + beforeEach(() => stub.clear()); + it("returns a pair [value, update]", () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook }), @@ -132,13 +135,20 @@ createLocationSpec({ hook: useBrowserLocation, location: () => location.pathname, navigate: browserNavigation, + clear: () => { + history.replaceState(null, "", "/"); + }, }); createLocationSpec({ name: "useHashLocation", hook: useHashLocation, - location: () => location.hash.replace(/^#?\/?/, ""), + location: () => "/" + location.hash.replace(/^#?\/?/, ""), navigate: hashNavigation, + clear: () => { + location.hash = ""; + history.replaceState(null, "", "/"); + }, }); const memory = memoryLocation({ record: true }); @@ -147,4 +157,5 @@ createLocationSpec({ hook: memory.hook, location: () => memory.history.at(-1) ?? "", navigate: memory.navigate, + clear: () => null, }); From 921972330629ef0cd576a746da63dce84750482d Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Sun, 15 Oct 2023 13:33:23 +0300 Subject: [PATCH 04/10] fix types --- packages/wouter/test/use-browser-location.test-d.ts | 4 ++-- packages/wouter/test/use-hash-location.test-d.ts | 6 +++--- packages/wouter/types/use-browser-location.d.ts | 1 - packages/wouter/types/use-hash-location.d.ts | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/wouter/test/use-browser-location.test-d.ts b/packages/wouter/test/use-browser-location.test-d.ts index 0aa06d41..21af150a 100644 --- a/packages/wouter/test/use-browser-location.test-d.ts +++ b/packages/wouter/test/use-browser-location.test-d.ts @@ -29,8 +29,8 @@ describe("useBrowserLocation", () => { assertType(navigate("/path", { unknownOption: true })); }); - it("should support base option", () => { - assertType(useBrowserLocation({ base: "/something" })); + it("should support `ssrPath` option", () => { + assertType(useBrowserLocation({ ssrPath: "/something" })); // @ts-expect-error assertType(useBrowserLocation({ foo: "bar" })); }); diff --git a/packages/wouter/test/use-hash-location.test-d.ts b/packages/wouter/test/use-hash-location.test-d.ts index cb771c99..434d7f5a 100644 --- a/packages/wouter/test/use-hash-location.test-d.ts +++ b/packages/wouter/test/use-hash-location.test-d.ts @@ -7,9 +7,9 @@ it("is a location hook", () => { expectTypeOf(useHashLocation()).toMatchTypeOf<[string, Function]>(); }); -it("accepts a `base` path option", () => { - useHashLocation({ base: "/foo" }); - useHashLocation({ base: "" }); +it("accepts a `ssrPath` path option", () => { + useHashLocation({ ssrPath: "/foo" }); + useHashLocation({ ssrPath: "" }); // @ts-expect-error useHashLocation({ base: 123 }); diff --git a/packages/wouter/types/use-browser-location.d.ts b/packages/wouter/types/use-browser-location.d.ts index 6f2495f4..9f1a3882 100644 --- a/packages/wouter/types/use-browser-location.d.ts +++ b/packages/wouter/types/use-browser-location.d.ts @@ -57,7 +57,6 @@ export const navigate: ( // It operates on current URL using History API, supports base path and can // navigate with `pushState` or `replaceState`. export type LocationHook = (options?: { - base?: Path; ssrPath?: Path; }) => [Path, typeof navigate]; diff --git a/packages/wouter/types/use-hash-location.d.ts b/packages/wouter/types/use-hash-location.d.ts index a004f070..ac95fbc0 100644 --- a/packages/wouter/types/use-hash-location.d.ts +++ b/packages/wouter/types/use-hash-location.d.ts @@ -3,6 +3,5 @@ import { Path } from "./use-browser-location"; export function navigate(to: Path, options?: { state: S }): void; export function useHashLocation(options?: { - base?: Path; ssrPath?: Path; }): [Path, typeof navigate]; From f9507b80334ef77a22749cdb4d0222ddf4447f87 Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Sun, 15 Oct 2023 13:33:50 +0300 Subject: [PATCH 05/10] ignore `useHashLocation` --- packages/wouter/test/use-location.test.tsx | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/wouter/test/use-location.test.tsx b/packages/wouter/test/use-location.test.tsx index 584e33e2..834e252b 100644 --- a/packages/wouter/test/use-location.test.tsx +++ b/packages/wouter/test/use-location.test.tsx @@ -8,10 +8,10 @@ import { BaseLocationHook, } from "wouter/use-browser-location"; -import { - useHashLocation, - navigate as hashNavigation, -} from "wouter/use-hash-location"; +// import { +// useHashLocation, +// navigate as hashNavigation, +// } from "wouter/use-hash-location"; import { memoryLocation } from "wouter/memory-location"; @@ -140,16 +140,16 @@ createLocationSpec({ }, }); -createLocationSpec({ - name: "useHashLocation", - hook: useHashLocation, - location: () => "/" + location.hash.replace(/^#?\/?/, ""), - navigate: hashNavigation, - clear: () => { - location.hash = ""; - history.replaceState(null, "", "/"); - }, -}); +// createLocationSpec({ +// name: "useHashLocation", +// hook: useHashLocation, +// location: () => "/" + location.hash.replace(/^#?\/?/, ""), +// navigate: hashNavigation, +// clear: () => { +// location.hash = ""; +// history.replaceState(null, "", "/"); +// }, +// }); const memory = memoryLocation({ record: true }); createLocationSpec({ From 14b9edf83362f673fdc35f7983da7d3da847af49 Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Mon, 16 Oct 2023 14:08:08 +0300 Subject: [PATCH 06/10] fix hash-location tests --- packages/wouter/test/use-location.test.tsx | 71 ++++++++++++++-------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/packages/wouter/test/use-location.test.tsx b/packages/wouter/test/use-location.test.tsx index 834e252b..375c869c 100644 --- a/packages/wouter/test/use-location.test.tsx +++ b/packages/wouter/test/use-location.test.tsx @@ -8,10 +8,10 @@ import { BaseLocationHook, } from "wouter/use-browser-location"; -// import { -// useHashLocation, -// navigate as hashNavigation, -// } from "wouter/use-hash-location"; +import { + useHashLocation, + navigate as hashNavigation, +} from "wouter/use-hash-location"; import { memoryLocation } from "wouter/memory-location"; @@ -28,9 +28,29 @@ type StubType = { hook: BaseLocationHook; location: () => string; navigate: ReturnType[1]; + act: (cb: () => void) => Promise; clear: () => void; }; +const waitForHashChangeEvent = async (cb: () => void, throwAfter = 1000) => + new Promise((resolve, reject) => { + let timeout: ReturnType; + + const onChange = () => { + resolve(); + clearTimeout(timeout); + window.removeEventListener("hashchange", onChange); + }; + + window.addEventListener("hashchange", onChange); + cb(); + + timeout = setTimeout(() => { + reject(new Error("Timed out: `hashchange` event did not fire!")); + window.removeEventListener("hashchange", onChange); + }, throwAfter); + }); + function createLocationSpec(stub: StubType) { describe(stub.name, () => { beforeEach(() => stub.clear()); @@ -47,7 +67,7 @@ function createLocationSpec(stub: StubType) { }); describe("`value` first argument", () => { - it("returns `/` when URL contains only a basepath", () => { + it("returns `/` when URL contains only a basepath", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/app", @@ -55,12 +75,12 @@ function createLocationSpec(stub: StubType) { }), }); - act(() => stub.navigate("/app")); + await stub.act(() => stub.navigate("/app")); expect(result.current[0]).toBe("/"); unmount(); }); - it("basepath should be case-insensitive", () => { + it("basepath should be case-insensitive", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/MyApp", @@ -68,12 +88,12 @@ function createLocationSpec(stub: StubType) { }), }); - act(() => stub.navigate("/myAPP/users/JohnDoe")); + await stub.act(() => stub.navigate("/myAPP/users/JohnDoe")); expect(result.current[0]).toBe("/users/JohnDoe"); unmount(); }); - it("returns an absolute path in case of unmatched base path", () => { + it("returns an absolute path in case of unmatched base path", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/MyApp", @@ -81,20 +101,20 @@ function createLocationSpec(stub: StubType) { }), }); - act(() => stub.navigate("/MyOtherApp/users/JohnDoe")); + await stub.act(() => stub.navigate("/MyOtherApp/users/JohnDoe")); expect(result.current[0]).toBe("~/MyOtherApp/users/JohnDoe"); unmount(); }); }); describe("`update` second parameter", () => { - it("rerenders the component", () => { + it("rerenders the component", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook }), }); const update = result.current[1]; - act(() => update("/about")); + await stub.act(() => update("/about")); expect(stub.location()).toBe("/about"); unmount(); }); @@ -112,7 +132,7 @@ function createLocationSpec(stub: StubType) { unmount(); }); - it("supports a basepath", () => { + it("supports a basepath", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/app", @@ -122,7 +142,7 @@ function createLocationSpec(stub: StubType) { const update = result.current[1]; - act(() => update("/dashboard")); + await stub.act(() => update("/dashboard")); expect(stub.location()).toBe("/app/dashboard"); unmount(); }); @@ -135,21 +155,23 @@ createLocationSpec({ hook: useBrowserLocation, location: () => location.pathname, navigate: browserNavigation, + act, clear: () => { history.replaceState(null, "", "/"); }, }); -// createLocationSpec({ -// name: "useHashLocation", -// hook: useHashLocation, -// location: () => "/" + location.hash.replace(/^#?\/?/, ""), -// navigate: hashNavigation, -// clear: () => { -// location.hash = ""; -// history.replaceState(null, "", "/"); -// }, -// }); +createLocationSpec({ + name: "useHashLocation", + hook: useHashLocation, + location: () => "/" + location.hash.replace(/^#?\/?/, ""), + navigate: hashNavigation, + act: (cb) => waitForHashChangeEvent(() => act(cb)), + clear: () => { + location.hash = ""; + history.replaceState(null, "", "/"); + }, +}); const memory = memoryLocation({ record: true }); createLocationSpec({ @@ -157,5 +179,6 @@ createLocationSpec({ hook: memory.hook, location: () => memory.history.at(-1) ?? "", navigate: memory.navigate, + act, clear: () => null, }); From 9c72439d25b9b03e2673707412d59de4462fcf5b Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Mon, 16 Oct 2023 14:10:52 +0300 Subject: [PATCH 07/10] dedupe `waitForHashChangeEvent` --- packages/wouter/test/test-utils.ts | 22 ++++++++++++++++ .../wouter/test/use-hash-location.test.tsx | 25 ++----------------- packages/wouter/test/use-location.test.tsx | 20 +-------------- 3 files changed, 25 insertions(+), 42 deletions(-) create mode 100644 packages/wouter/test/test-utils.ts diff --git a/packages/wouter/test/test-utils.ts b/packages/wouter/test/test-utils.ts new file mode 100644 index 00000000..4b2d6a86 --- /dev/null +++ b/packages/wouter/test/test-utils.ts @@ -0,0 +1,22 @@ +/** + * Executes a callback and returns a promise that resolve when `hashchange` event is fired. + * Rejects after `throwAfter` milliseconds. + */ +export const waitForHashChangeEvent = async (cb: () => void, throwAfter = 1000) => + new Promise((resolve, reject) => { + let timeout: ReturnType; + + const onChange = () => { + resolve(); + clearTimeout(timeout); + window.removeEventListener("hashchange", onChange); + }; + + window.addEventListener("hashchange", onChange); + cb(); + + timeout = setTimeout(() => { + reject(new Error("Timed out: `hashchange` event did not fire!")); + window.removeEventListener("hashchange", onChange); + }, throwAfter); + }); diff --git a/packages/wouter/test/use-hash-location.test.tsx b/packages/wouter/test/use-hash-location.test.tsx index 27ebce68..458a0cd1 100644 --- a/packages/wouter/test/use-hash-location.test.tsx +++ b/packages/wouter/test/use-hash-location.test.tsx @@ -4,6 +4,8 @@ import { renderToStaticMarkup } from "react-dom/server"; import { useHashLocation } from "wouter/use-hash-location"; +import { waitForHashChangeEvent } from "./test-utils"; + beforeEach(() => { history.replaceState(null, "", "/"); location.hash = ""; @@ -119,26 +121,3 @@ it("is not sensitive to leading / or # when navigating", async () => { expect(location.hash).toBe("#/look-ma-no-hashes"); expect(result.current[0]).toBe("/look-ma-no-hashes"); }); - -/** - * Executes a callback and returns a promise that resolve when `hashchange` event is fired. - * Rejects after `throwAfter` milliseconds. - */ -const waitForHashChangeEvent = async (cb: () => void, throwAfter = 1000) => - new Promise((resolve, reject) => { - let timeout: ReturnType; - - const onChange = () => { - resolve(); - clearTimeout(timeout); - window.removeEventListener("hashchange", onChange); - }; - - window.addEventListener("hashchange", onChange); - cb(); - - timeout = setTimeout(() => { - reject(new Error("Timed out: `hashchange` event did not fire!")); - window.removeEventListener("hashchange", onChange); - }, throwAfter); - }); diff --git a/packages/wouter/test/use-location.test.tsx b/packages/wouter/test/use-location.test.tsx index 375c869c..4827abc9 100644 --- a/packages/wouter/test/use-location.test.tsx +++ b/packages/wouter/test/use-location.test.tsx @@ -12,6 +12,7 @@ import { useHashLocation, navigate as hashNavigation, } from "wouter/use-hash-location"; +import { waitForHashChangeEvent } from "./test-utils"; import { memoryLocation } from "wouter/memory-location"; @@ -32,25 +33,6 @@ type StubType = { clear: () => void; }; -const waitForHashChangeEvent = async (cb: () => void, throwAfter = 1000) => - new Promise((resolve, reject) => { - let timeout: ReturnType; - - const onChange = () => { - resolve(); - clearTimeout(timeout); - window.removeEventListener("hashchange", onChange); - }; - - window.addEventListener("hashchange", onChange); - cb(); - - timeout = setTimeout(() => { - reject(new Error("Timed out: `hashchange` event did not fire!")); - window.removeEventListener("hashchange", onChange); - }, throwAfter); - }); - function createLocationSpec(stub: StubType) { describe(stub.name, () => { beforeEach(() => stub.clear()); From 057c113f23a1db5967e9e7bbcd067d011f85d1de Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Mon, 16 Oct 2023 14:11:04 +0300 Subject: [PATCH 08/10] apply code style --- packages/wouter/test/test-utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/wouter/test/test-utils.ts b/packages/wouter/test/test-utils.ts index 4b2d6a86..b7ab02b9 100644 --- a/packages/wouter/test/test-utils.ts +++ b/packages/wouter/test/test-utils.ts @@ -2,7 +2,10 @@ * Executes a callback and returns a promise that resolve when `hashchange` event is fired. * Rejects after `throwAfter` milliseconds. */ -export const waitForHashChangeEvent = async (cb: () => void, throwAfter = 1000) => +export const waitForHashChangeEvent = async ( + cb: () => void, + throwAfter = 1000 +) => new Promise((resolve, reject) => { let timeout: ReturnType; From 7c7fd5fb1da255d1bfb5c5be0d4cabc67373932b Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Mon, 16 Oct 2023 14:44:45 +0300 Subject: [PATCH 09/10] Update packages/wouter/src/memory-location.js Co-authored-by: Alexey Taktarov --- packages/wouter/src/memory-location.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/wouter/src/memory-location.js b/packages/wouter/src/memory-location.js index f9ebe717..57b416db 100644 --- a/packages/wouter/src/memory-location.js +++ b/packages/wouter/src/memory-location.js @@ -34,9 +34,8 @@ export const memoryLocation = ({ return () => emitter.off("navigate", cb); }; - const useMemoryLocation = () => { - return [useSyncExternalStore(subscribe, () => currentPath), navigate]; - }; + const useMemoryLocation = () => + [useSyncExternalStore(subscribe, () => currentPath), navigate]; return { hook: useMemoryLocation, From b9588bf22b8b380ab0dafdb3f3613bb09451d8e0 Mon Sep 17 00:00:00 2001 From: Dmitry Ivakhnenko Date: Mon, 16 Oct 2023 14:45:53 +0300 Subject: [PATCH 10/10] fix code style --- packages/wouter/src/memory-location.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/wouter/src/memory-location.js b/packages/wouter/src/memory-location.js index 57b416db..a748d2a9 100644 --- a/packages/wouter/src/memory-location.js +++ b/packages/wouter/src/memory-location.js @@ -34,8 +34,10 @@ export const memoryLocation = ({ return () => emitter.off("navigate", cb); }; - const useMemoryLocation = () => - [useSyncExternalStore(subscribe, () => currentPath), navigate]; + const useMemoryLocation = () => [ + useSyncExternalStore(subscribe, () => currentPath), + navigate, + ]; return { hook: useMemoryLocation,