From 4556b23b1767b289cef3b399f358fb16059a7be7 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 12:53:44 +0100 Subject: [PATCH 01/10] feat(useSearchParams): remove trailing question mark --- packages/wouter/src/index.js | 5 ++++- packages/wouter/test/use-search-params.test.tsx | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js index 957a7be6..98f16376 100644 --- a/packages/wouter/src/index.js +++ b/packages/wouter/src/index.js @@ -231,7 +231,10 @@ export function useSearchParams() { tempSearchParams = new URLSearchParams( typeof nextInit === "function" ? nextInit(tempSearchParams) : nextInit ); - navigate(location + "?" + tempSearchParams, options); + navigate( + location + (tempSearchParams.size ? "?" + tempSearchParams : ""), + options + ); }); return [searchParams, setSearchParams]; diff --git a/packages/wouter/test/use-search-params.test.tsx b/packages/wouter/test/use-search-params.test.tsx index 151e68ee..ed1ce05f 100644 --- a/packages/wouter/test/use-search-params.test.tsx +++ b/packages/wouter/test/use-search-params.test.tsx @@ -60,3 +60,10 @@ it("is safe against parameter injection", () => { expect(result.current[0].get("search")).toBe("foo¶meter_injection=bar"); }); + +it("does not add question mark when search string is empty", () => { + const { result } = renderHook(() => useSearchParams()); + + act(() => result.current[1]({})); + expect(location.href).toBe("https://wouter.dev/"); +}); From 5f4dc5e02df609a9885f0fad26c1b3f7398db763 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 15:46:54 +0100 Subject: [PATCH 02/10] style: fix formatting --- packages/wouter/test/view-transitions.test.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/wouter/test/view-transitions.test.tsx b/packages/wouter/test/view-transitions.test.tsx index 6c44c8bd..6a1b663c 100644 --- a/packages/wouter/test/view-transitions.test.tsx +++ b/packages/wouter/test/view-transitions.test.tsx @@ -1,6 +1,11 @@ import { test, expect, describe, mock, afterEach } from "bun:test"; import { render, cleanup, fireEvent } from "@testing-library/react"; -import { Router, Link, useLocation, type AroundNavHandler } from "../src/index.js"; +import { + Router, + Link, + useLocation, + type AroundNavHandler, +} from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; afterEach(cleanup); @@ -66,7 +71,8 @@ describe("view transitions", () => { expect(aroundNav).toHaveBeenCalledTimes(1); - const [, to, options] = (aroundNav as ReturnType).mock.calls[0]; + const [, to, options] = (aroundNav as ReturnType).mock + .calls[0]; expect(to).toBe("/about"); expect(options.transition).toBe(true); }); From 29a1b271e9849b4fc53d1766848246826a478932 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 15:47:09 +0100 Subject: [PATCH 03/10] test: remove useless test --- packages/wouter/test/redirect.test.tsx | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/packages/wouter/test/redirect.test.tsx b/packages/wouter/test/redirect.test.tsx index 9f1f1066..871d04c4 100644 --- a/packages/wouter/test/redirect.test.tsx +++ b/packages/wouter/test/redirect.test.tsx @@ -1,21 +1,8 @@ import { test, expect } from "bun:test"; import { render } from "@testing-library/react"; -import { useState } from "react"; import { Redirect, Router } from "../src/index.js"; -export const customHookWithReturn = - (initialPath = "/") => - () => { - const [path, updatePath] = useState(initialPath); - const navigate = (path: string) => { - updatePath(path); - return "foo"; - }; - - return [path, navigate]; - }; - test("renders nothing", () => { const { container, unmount } = render(); expect(container.childNodes.length).toBe(0); @@ -69,15 +56,3 @@ test("supports history state", () => { expect(history.state).toStrictEqual(testState); unmount(); }); - -test("useLayoutEffect should return nothing", () => { - const { unmount } = render( - // @ts-expect-error - - - - ); - - expect(location.pathname).toBe("/users"); - unmount(); -}); From d917c7a841fc007325590222fb5daaf2a9a5f2a5 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 15:46:14 +0100 Subject: [PATCH 04/10] test: remove manual unmounts --- packages/wouter/test/history-patch.test.ts | 4 +- packages/wouter/test/link.test.tsx | 6 +-- packages/wouter/test/memory-location.test.ts | 32 +++++--------- packages/wouter/test/redirect.test.tsx | 18 +++----- packages/wouter/test/route.test.tsx | 11 ++--- packages/wouter/test/setup.ts | 5 ++- packages/wouter/test/switch.test.tsx | 11 ++--- .../wouter/test/use-browser-location.test.tsx | 44 ++++++------------- .../wouter/test/use-hash-location.test.tsx | 3 +- packages/wouter/test/use-location.test.tsx | 35 +++++---------- packages/wouter/test/use-params.test.tsx | 5 +-- packages/wouter/test/use-search.test.tsx | 5 +-- .../wouter/test/view-transitions.test.tsx | 6 +-- 13 files changed, 60 insertions(+), 125 deletions(-) diff --git a/packages/wouter/test/history-patch.test.ts b/packages/wouter/test/history-patch.test.ts index 5ec4e20d..ba777900 100644 --- a/packages/wouter/test/history-patch.test.ts +++ b/packages/wouter/test/history-patch.test.ts @@ -12,7 +12,7 @@ describe("history patch", () => { test("history should be patched once", () => { const fn = mock(); - const { result, unmount } = renderHook(() => reactHook()); + const { result } = renderHook(() => reactHook()); addEventListener("pushState", (e) => { fn(); @@ -26,7 +26,5 @@ describe("history patch", () => { expect(result.current[0]).toBe("/world"); expect(fn).toHaveBeenCalledTimes(2); - - unmount(); }); }); diff --git a/packages/wouter/test/link.test.tsx b/packages/wouter/test/link.test.tsx index 326d594c..13050709 100644 --- a/packages/wouter/test/link.test.tsx +++ b/packages/wouter/test/link.test.tsx @@ -1,12 +1,10 @@ import { type MouseEventHandler } from "react"; -import { test, expect, afterEach, mock, describe } from "bun:test"; -import { render, cleanup, fireEvent, act } from "@testing-library/react"; +import { test, expect, mock, describe } from "bun:test"; +import { render, fireEvent, act } from "@testing-library/react"; import { Router, Link } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; -afterEach(cleanup); - describe("", () => { test("renders a link with proper attributes", () => { const { getByText } = render( diff --git a/packages/wouter/test/memory-location.test.ts b/packages/wouter/test/memory-location.test.ts index 3010bdfa..41a41b0c 100644 --- a/packages/wouter/test/memory-location.test.ts +++ b/packages/wouter/test/memory-location.test.ts @@ -5,32 +5,29 @@ import { memoryLocation } from "../src/memory-location.js"; test("returns a hook that is compatible with location spec", () => { const { hook } = memoryLocation(); - const { result, unmount } = renderHook(() => hook()); + const { result } = renderHook(() => hook()); const [value, update] = result.current; expect(typeof value).toBe("string"); expect(typeof update).toBe("function"); - unmount(); }); test("should support initial path", () => { const { hook } = memoryLocation({ path: "/test-case" }); - const { result, unmount } = renderHook(() => hook()); + const { result } = renderHook(() => hook()); const [value] = result.current; expect(value).toBe("/test-case"); - unmount(); }); test("should support initial path with query", () => { const { searchHook } = memoryLocation({ path: "/test-case?foo=bar" }); - const { result, unmount } = renderHook(() => searchHook()); + const { result } = renderHook(() => searchHook()); const value = result.current; expect(value).toBe("foo=bar"); - unmount(); }); test("should support search path as parameter", () => { @@ -39,55 +36,50 @@ test("should support search path as parameter", () => { searchPath: "key=value", }); - const { result, unmount } = renderHook(() => searchHook()); + const { result } = renderHook(() => searchHook()); const value = result.current; expect(value).toBe("foo=bar&key=value"); - unmount(); }); test('should return location hook that has initial path "/" by default', () => { const { hook } = memoryLocation(); - const { result, unmount } = renderHook(() => hook()); + const { result } = renderHook(() => hook()); const [value] = result.current; expect(value).toBe("/"); - unmount(); }); test('should return search hook that has initial query "" by default', () => { const { searchHook } = memoryLocation(); - const { result, unmount } = renderHook(() => searchHook()); + const { result } = renderHook(() => searchHook()); const value = result.current; expect(value).toBe(""); - unmount(); }); test("should return standalone `navigate` method", () => { const { hook, navigate } = memoryLocation(); - const { result, unmount } = renderHook(() => hook()); + const { result } = renderHook(() => hook()); act(() => navigate("/standalone")); const [value] = result.current; expect(value).toBe("/standalone"); - unmount(); }); test("should return location hook that supports navigation", () => { const { hook } = memoryLocation(); - const { result, unmount } = renderHook(() => hook()); + const { result } = renderHook(() => hook()); act(() => result.current[1]("/location")); const [value] = result.current; expect(value).toBe("/location"); - unmount(); }); test("should record all history when `record` option is provided", () => { @@ -97,7 +89,7 @@ test("should record all history when `record` option is provided", () => { navigate: standalone, } = memoryLocation({ record: true, path: "/test" }); - const { result, unmount } = renderHook(() => hook()); + const { result } = renderHook(() => hook()); act(() => standalone("/standalone")); act(() => result.current[1]("/location")); @@ -113,8 +105,6 @@ test("should record all history when `record` option is provided", () => { act(() => result.current[1]("/location", { replace: true })); expect(history).toStrictEqual(["/test", "/standalone", "/location"]); - - unmount(); }); test("should not have history when `record` option is falsy", () => { @@ -145,7 +135,7 @@ test("should have reset method that reset hook location", () => { record: true, path: "/test", }); - const { result, unmount } = renderHook(() => hook()); + const { result } = renderHook(() => hook()); act(() => navigate("/location")); @@ -158,6 +148,4 @@ test("should have reset method that reset hook location", () => { expect(history).toStrictEqual(["/test"]); expect(result.current[0]).toBe("/test"); - - unmount(); }); diff --git a/packages/wouter/test/redirect.test.tsx b/packages/wouter/test/redirect.test.tsx index 871d04c4..b607ef98 100644 --- a/packages/wouter/test/redirect.test.tsx +++ b/packages/wouter/test/redirect.test.tsx @@ -4,55 +4,49 @@ import { render } from "@testing-library/react"; import { Redirect, Router } from "../src/index.js"; test("renders nothing", () => { - const { container, unmount } = render(); + const { container } = render(); expect(container.childNodes.length).toBe(0); - unmount(); }); test("results in change of current location", () => { - const { unmount } = render(); + render(); expect(location.pathname).toBe("/users"); - unmount(); }); test("supports `base` routers with relative path", () => { - const { unmount } = render( + render( ); expect(location.pathname).toBe("/app/nested"); - unmount(); }); test("supports `base` routers with absolute path", () => { - const { unmount } = render( + render( ); expect(location.pathname).toBe("/absolute"); - unmount(); }); test("supports replace navigation", () => { const histBefore = history.length; - const { unmount } = render(); + render(); expect(location.pathname).toBe("/users"); expect(history.length).toBe(histBefore); - unmount(); }); test("supports history state", () => { const testState = { hello: "world" }; - const { unmount } = render(); + render(); expect(location.pathname).toBe("/users"); expect(history.state).toStrictEqual(testState); - unmount(); }); diff --git a/packages/wouter/test/route.test.tsx b/packages/wouter/test/route.test.tsx index 1d17ad2d..17e39694 100644 --- a/packages/wouter/test/route.test.tsx +++ b/packages/wouter/test/route.test.tsx @@ -1,13 +1,10 @@ -import { it, expect, afterEach } from "bun:test"; -import { render, act, cleanup } from "@testing-library/react"; +import { it, expect } from "bun:test"; +import { render, act } from "@testing-library/react"; import { Router, Route } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; import { ReactElement } from "react"; -// Clean up after each test to avoid DOM pollution -afterEach(cleanup); - const testRouteRender = (initialPath: string, jsx: ReactElement) => { return render( {jsx} @@ -87,7 +84,7 @@ it("supports `component` prop similar to React-Router", () => { }); it("supports `base` routers with relative path", () => { - const { container, unmount } = render( + const { container } = render(

Nested

@@ -102,8 +99,6 @@ it("supports `base` routers with relative path", () => { expect(container.children).toHaveLength(1); expect(container.firstChild).toHaveProperty("tagName", "H1"); - - unmount(); }); it("supports `path` prop with regex", () => { diff --git a/packages/wouter/test/setup.ts b/packages/wouter/test/setup.ts index 8f17b3ba..2b59ce56 100644 --- a/packages/wouter/test/setup.ts +++ b/packages/wouter/test/setup.ts @@ -1,6 +1,7 @@ import { GlobalRegistrator } from "@happy-dom/global-registrator"; -import { expect } from "bun:test"; +import { expect, afterEach } from "bun:test"; import * as matchers from "@testing-library/jest-dom/matchers"; +import { cleanup } from "@testing-library/react"; // Register happy-dom globals (document, window, etc.) GlobalRegistrator.register({ @@ -26,3 +27,5 @@ export const withoutLocation = (fn: () => T): T => { globalThis.location = original; } }; + +afterEach(cleanup); diff --git a/packages/wouter/test/switch.test.tsx b/packages/wouter/test/switch.test.tsx index cf9315f8..4abedd62 100644 --- a/packages/wouter/test/switch.test.tsx +++ b/packages/wouter/test/switch.test.tsx @@ -1,14 +1,11 @@ -import { it, expect, afterEach } from "bun:test"; +import { it, expect } from "bun:test"; import { Router, Route, Switch } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; -import { render, act, cleanup } from "@testing-library/react"; +import { render, act } from "@testing-library/react"; import { PropsWithChildren, ReactElement, JSX } from "react"; -// Clean up after each test to avoid DOM pollution -afterEach(cleanup); - const raf = () => new Promise((resolve) => requestAnimationFrame(resolve)); const testRouteRender = (initialPath: string, jsx: ReactElement) => { @@ -111,7 +108,7 @@ it("allows to specify which routes to render via `location` prop", () => { it("always ensures the consistency of inner routes rendering", async () => { history.replaceState(null, "", "/foo/bar"); - const { unmount } = render( + render( {(params) => { @@ -127,8 +124,6 @@ it("always ensures the consistency of inner routes rendering", async () => { await raf(); history.pushState(null, "", "/"); }); - - unmount(); }); it("supports catch-all routes with wildcard segments", async () => { diff --git a/packages/wouter/test/use-browser-location.test.tsx b/packages/wouter/test/use-browser-location.test.tsx index 97c01cc9..d254ef6f 100644 --- a/packages/wouter/test/use-browser-location.test.tsx +++ b/packages/wouter/test/use-browser-location.test.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import { test, expect, describe, beforeEach, afterEach } from "bun:test"; -import { renderHook, act, waitFor, cleanup } from "@testing-library/react"; +import { test, expect, describe, beforeEach } from "bun:test"; +import { renderHook, act, waitFor } from "@testing-library/react"; import { useBrowserLocation, navigate, @@ -8,39 +8,34 @@ import { useHistoryState, } from "../src/use-browser-location.js"; -afterEach(cleanup); - test("returns a pair [value, update]", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); const [value, update] = result.current; expect(typeof value).toBe("string"); expect(typeof update).toBe("function"); - unmount(); }); describe("`value` first argument", () => { beforeEach(() => history.replaceState(null, "", "/")); test("reflects the current pathname", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); expect(result.current[0]).toBe("/"); - unmount(); }); test("reacts to `pushState` / `replaceState`", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); act(() => history.pushState(null, "", "/foo")); expect(result.current[0]).toBe("/foo"); act(() => history.replaceState(null, "", "/bar")); expect(result.current[0]).toBe("/bar"); - unmount(); }); test("supports history.back() navigation", async () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); act(() => history.pushState(null, "", "/foo")); await waitFor(() => expect(result.current[0]).toBe("/foo")); @@ -59,23 +54,17 @@ describe("`value` first argument", () => { }); await waitFor(() => expect(result.current[0]).toBe("/"), { timeout: 1000 }); - unmount(); }); test("supports history state", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); - const { result: state, unmount: unmountState } = renderHook(() => - useHistoryState() - ); + const { result } = renderHook(() => useBrowserLocation()); + const { result: state } = renderHook(() => useHistoryState()); const navigate = result.current[1]; act(() => navigate("/path", { state: { hello: "world" } })); expect(state.current).toStrictEqual({ hello: "world" }); - - unmount(); - unmountState(); }); test("uses fail-safe escaping", () => { @@ -152,36 +141,33 @@ describe("`useSearch` hook", () => { describe("`update` second parameter", () => { test("rerenders the component", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); const update = result.current[1]; act(() => update("/about")); expect(result.current[0]).toBe("/about"); - unmount(); }); test("changes the current location", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); const update = result.current[1]; act(() => update("/about")); expect(location.pathname).toBe("/about"); - unmount(); }); test("saves a new entry in the History object", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); const update = result.current[1]; const histBefore = history.length; act(() => update("/about")); expect(history.length).toBe(histBefore + 1); - unmount(); }); test("replaces last entry with a new entry in the History object", () => { - const { result, unmount } = renderHook(() => useBrowserLocation()); + const { result } = renderHook(() => useBrowserLocation()); const update = result.current[1]; const histBefore = history.length; @@ -189,19 +175,15 @@ describe("`update` second parameter", () => { expect(history.length).toBe(histBefore); expect(location.pathname).toBe("/foo"); - unmount(); }); test("stays the same reference between re-renders (function ref)", () => { - const { result, rerender, unmount } = renderHook(() => - useBrowserLocation() - ); + const { result, rerender } = renderHook(() => useBrowserLocation()); const updateWas = result.current[1]; rerender(); const updateNow = result.current[1]; expect(updateWas).toBe(updateNow); - unmount(); }); }); diff --git a/packages/wouter/test/use-hash-location.test.tsx b/packages/wouter/test/use-hash-location.test.tsx index 336af81e..8a08a603 100644 --- a/packages/wouter/test/use-hash-location.test.tsx +++ b/packages/wouter/test/use-hash-location.test.tsx @@ -195,7 +195,7 @@ test("works even if `hashchange` listeners are called asynchronously ", async () location.hash = "#/a"; }); - const { unmount } = render( + render( @@ -215,7 +215,6 @@ test("works even if `hashchange` listeners are called asynchronously ", async () // paths should not contain "b", because the outer route // does not match, so inner component should not be rendered expect(paths).toEqual(["/a"]); - unmount(); }); test("defines a custom way of rendering link hrefs", () => { diff --git a/packages/wouter/test/use-location.test.tsx b/packages/wouter/test/use-location.test.tsx index 3baee573..254ceee6 100644 --- a/packages/wouter/test/use-location.test.tsx +++ b/packages/wouter/test/use-location.test.tsx @@ -61,19 +61,18 @@ describe.each([ beforeEach(() => stub.clear()); it("returns a pair [value, update]", () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = 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", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/app", hook: stub.hook, @@ -82,11 +81,10 @@ describe.each([ await stub.act(() => stub.navigate("/app")); expect(result.current[0]).toBe("/"); - unmount(); }); it("basepath should be case-insensitive", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/MyApp", hook: stub.hook, @@ -95,11 +93,10 @@ describe.each([ 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", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/MyApp", hook: stub.hook, @@ -108,11 +105,10 @@ describe.each([ await stub.act(() => stub.navigate("/MyOtherApp/users/JohnDoe")); expect(result.current[0]).toBe("~/MyOtherApp/users/JohnDoe"); - unmount(); }); it("automatically unescapes specials characters", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook, }), @@ -127,11 +123,10 @@ describe.each([ await stub.act(() => stub.navigate("/%D1%88%D0%B5%D0%BB%D0%BB%D1%8B")); expect(result.current[0]).toBe("/шеллы"); - unmount(); }); it("can accept unescaped basepaths", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/hello мир", // basepath is not escaped hook: stub.hook, @@ -140,12 +135,10 @@ describe.each([ await stub.act(() => stub.navigate("/hello%20%D0%BC%D0%B8%D1%80/rel")); expect(result.current[0]).toBe("/rel"); - - unmount(); }); it("can accept unescaped basepaths", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/hello%20%D0%BC%D0%B8%D1%80", // basepath is already escaped hook: stub.hook, @@ -154,25 +147,22 @@ describe.each([ await stub.act(() => stub.navigate("/hello мир/rel")); expect(result.current[0]).toBe("/rel"); - - unmount(); }); }); describe("`update` second parameter", () => { it("rerenders the component", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook }), }); const update = result.current[1]; await stub.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(), { + const { result, rerender } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook }), }); @@ -181,11 +171,10 @@ describe.each([ const updateNow = result.current[1]; expect(updateWas).toBe(updateNow); - unmount(); }); it("supports a basepath", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/app", hook: stub.hook, @@ -196,11 +185,10 @@ describe.each([ await stub.act(() => update("/dashboard")); expect(stub.location()).toBe("/app/dashboard"); - unmount(); }); it("ignores the '/' basepath", async () => { - const { result, unmount } = renderHook(() => useLocation(), { + const { result } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/", hook: stub.hook, @@ -211,7 +199,6 @@ describe.each([ await stub.act(() => update("/dashboard")); expect(stub.location()).toBe("/dashboard"); - unmount(); }); }); }); diff --git a/packages/wouter/test/use-params.test.tsx b/packages/wouter/test/use-params.test.tsx index a88be6b3..391d1e61 100644 --- a/packages/wouter/test/use-params.test.tsx +++ b/packages/wouter/test/use-params.test.tsx @@ -1,11 +1,10 @@ -import { act, renderHook, cleanup } from "@testing-library/react"; -import { test, expect, beforeEach, afterEach } from "bun:test"; +import { act, renderHook } from "@testing-library/react"; +import { test, expect, beforeEach } from "bun:test"; import { useParams, Router, Route, Switch } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; beforeEach(() => history.replaceState(null, "", "/")); -afterEach(cleanup); test("returns empty object when used outside of ", () => { const { result } = renderHook(() => useParams()); diff --git a/packages/wouter/test/use-search.test.tsx b/packages/wouter/test/use-search.test.tsx index cacd4577..66dd0cff 100644 --- a/packages/wouter/test/use-search.test.tsx +++ b/packages/wouter/test/use-search.test.tsx @@ -1,11 +1,10 @@ -import { renderHook, act, cleanup } from "@testing-library/react"; +import { renderHook, act } from "@testing-library/react"; import { useSearch, Router } from "../src/index.js"; import { navigate } from "../src/use-browser-location.js"; import { memoryLocation } from "../src/memory-location.js"; -import { test, expect, beforeEach, afterEach } from "bun:test"; +import { test, expect, beforeEach } from "bun:test"; beforeEach(() => history.replaceState(null, "", "/")); -afterEach(cleanup); test("returns browser search string", () => { history.replaceState(null, "", "/users?active=true"); diff --git a/packages/wouter/test/view-transitions.test.tsx b/packages/wouter/test/view-transitions.test.tsx index 6a1b663c..93dc7ebd 100644 --- a/packages/wouter/test/view-transitions.test.tsx +++ b/packages/wouter/test/view-transitions.test.tsx @@ -1,5 +1,5 @@ -import { test, expect, describe, mock, afterEach } from "bun:test"; -import { render, cleanup, fireEvent } from "@testing-library/react"; +import { test, expect, describe, mock } from "bun:test"; +import { render, fireEvent } from "@testing-library/react"; import { Router, Link, @@ -8,8 +8,6 @@ import { } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; -afterEach(cleanup); - describe("view transitions", () => { test("Link with transition prop triggers aroundNav with transition in options", () => { // 1. Setup: create aroundNav callback that captures calls From b095aa76c4dce0a7813977dd6c4d068778924f9e Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 16:13:58 +0100 Subject: [PATCH 05/10] test: add missing removeEventListener --- packages/wouter/test/history-patch.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/wouter/test/history-patch.test.ts b/packages/wouter/test/history-patch.test.ts index ba777900..4884e5e0 100644 --- a/packages/wouter/test/history-patch.test.ts +++ b/packages/wouter/test/history-patch.test.ts @@ -11,20 +11,20 @@ describe("history patch", () => { }); test("history should be patched once", () => { - const fn = mock(); + const pushStateFn = mock(); const { result } = renderHook(() => reactHook()); - addEventListener("pushState", (e) => { - fn(); - }); + addEventListener("pushState", pushStateFn); expect(result.current[0]).toBe("/"); - expect(fn).toHaveBeenCalledTimes(0); + expect(pushStateFn).toHaveBeenCalledTimes(0); act(() => result.current[1]("/hello")); act(() => result.current[1]("/world")); expect(result.current[0]).toBe("/world"); - expect(fn).toHaveBeenCalledTimes(2); + expect(pushStateFn).toHaveBeenCalledTimes(2); + + removeEventListener("pushstate", pushStateFn); }); }); From d687293bf5eba59dcbda1f5b58dff8d855ebf307 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 15:51:25 +0100 Subject: [PATCH 06/10] test: avoid negative expects --- packages/wouter/test/link.test.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/wouter/test/link.test.tsx b/packages/wouter/test/link.test.tsx index 13050709..74adf677 100644 --- a/packages/wouter/test/link.test.tsx +++ b/packages/wouter/test/link.test.tsx @@ -100,22 +100,22 @@ describe("", () => { clickEvt.preventDefault(); fireEvent(getByTestId("link"), clickEvt); - expect(location.pathname).not.toBe("/users"); + expect(location.pathname).toBe("/"); }); test("ignores the navigation when event is cancelled", () => { - const clickHandler: MouseEventHandler = (e) => { - e.preventDefault(); - }; - const { getByTestId } = render( - + e.preventDefault()} + > click ); fireEvent.click(getByTestId("link")); - expect(location.pathname).not.toBe("/users"); + expect(location.pathname).toBe("/"); }); test("accepts an `onClick` prop, fired before the navigation", () => { From 3610583c98ffeb22617a28b76571126034cf85b7 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 16:55:15 +0100 Subject: [PATCH 07/10] test: reset history before each test --- packages/wouter/test/setup.ts | 7 ++++++- packages/wouter/test/use-browser-location.test.tsx | 6 +----- packages/wouter/test/use-hash-location.test.tsx | 7 +------ packages/wouter/test/use-params.test.tsx | 4 +--- packages/wouter/test/use-search-params.test.tsx | 4 +--- packages/wouter/test/use-search.test.tsx | 4 +--- 6 files changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/wouter/test/setup.ts b/packages/wouter/test/setup.ts index 2b59ce56..731fbd5a 100644 --- a/packages/wouter/test/setup.ts +++ b/packages/wouter/test/setup.ts @@ -1,5 +1,5 @@ import { GlobalRegistrator } from "@happy-dom/global-registrator"; -import { expect, afterEach } from "bun:test"; +import { expect, beforeEach, afterEach } from "bun:test"; import * as matchers from "@testing-library/jest-dom/matchers"; import { cleanup } from "@testing-library/react"; @@ -28,4 +28,9 @@ export const withoutLocation = (fn: () => T): T => { } }; +beforeEach(() => { + history.go(-history.length + 1); + history.replaceState(null, "", "/"); +}); + afterEach(cleanup); diff --git a/packages/wouter/test/use-browser-location.test.tsx b/packages/wouter/test/use-browser-location.test.tsx index d254ef6f..059d3dea 100644 --- a/packages/wouter/test/use-browser-location.test.tsx +++ b/packages/wouter/test/use-browser-location.test.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { test, expect, describe, beforeEach } from "bun:test"; +import { test, expect, describe } from "bun:test"; import { renderHook, act, waitFor } from "@testing-library/react"; import { useBrowserLocation, @@ -17,8 +17,6 @@ test("returns a pair [value, update]", () => { }); describe("`value` first argument", () => { - beforeEach(() => history.replaceState(null, "", "/")); - test("reflects the current pathname", () => { const { result } = renderHook(() => useBrowserLocation()); expect(result.current[0]).toBe("/"); @@ -80,8 +78,6 @@ describe("`value` first argument", () => { }); describe("`useSearch` hook", () => { - beforeEach(() => history.replaceState(null, "", "/")); - test("allows to get current search string", () => { const { result: searchResult } = renderHook(() => useSearch()); act(() => navigate("/foo?hello=world&whats=up")); diff --git a/packages/wouter/test/use-hash-location.test.tsx b/packages/wouter/test/use-hash-location.test.tsx index 8a08a603..46521c45 100644 --- a/packages/wouter/test/use-hash-location.test.tsx +++ b/packages/wouter/test/use-hash-location.test.tsx @@ -1,4 +1,4 @@ -import { test, expect, beforeEach, mock } from "bun:test"; +import { test, expect, mock } from "bun:test"; import { renderHook, render, act } from "@testing-library/react"; import { renderToStaticMarkup } from "react-dom/server"; @@ -8,11 +8,6 @@ import { useHashLocation } from "../src/use-hash-location.js"; import { waitForHashChangeEvent } from "./test-utils"; import { ReactNode, useSyncExternalStore } from "react"; -beforeEach(() => { - history.replaceState(null, "", "/"); - location.hash = ""; -}); - test("gets current location from `location.hash`", () => { location.hash = "/app/users"; const { result } = renderHook(() => useHashLocation()); diff --git a/packages/wouter/test/use-params.test.tsx b/packages/wouter/test/use-params.test.tsx index 391d1e61..eb433d21 100644 --- a/packages/wouter/test/use-params.test.tsx +++ b/packages/wouter/test/use-params.test.tsx @@ -1,11 +1,9 @@ import { act, renderHook } from "@testing-library/react"; -import { test, expect, beforeEach } from "bun:test"; +import { test, expect } from "bun:test"; import { useParams, Router, Route, Switch } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; -beforeEach(() => history.replaceState(null, "", "/")); - test("returns empty object when used outside of ", () => { const { result } = renderHook(() => useParams()); expect(result.current).toEqual({}); diff --git a/packages/wouter/test/use-search-params.test.tsx b/packages/wouter/test/use-search-params.test.tsx index ed1ce05f..27a0d2bf 100644 --- a/packages/wouter/test/use-search-params.test.tsx +++ b/packages/wouter/test/use-search-params.test.tsx @@ -1,9 +1,7 @@ import { renderHook, act } from "@testing-library/react"; import { useSearchParams, Router } from "../src/index.js"; import { navigate } from "../src/use-browser-location.js"; -import { it, expect, beforeEach } from "bun:test"; - -beforeEach(() => history.replaceState(null, "", "/")); +import { it, expect } from "bun:test"; it("can return browser search params", () => { history.replaceState(null, "", "/users?active=true"); diff --git a/packages/wouter/test/use-search.test.tsx b/packages/wouter/test/use-search.test.tsx index 66dd0cff..0005ffbd 100644 --- a/packages/wouter/test/use-search.test.tsx +++ b/packages/wouter/test/use-search.test.tsx @@ -2,9 +2,7 @@ import { renderHook, act } from "@testing-library/react"; import { useSearch, Router } from "../src/index.js"; import { navigate } from "../src/use-browser-location.js"; import { memoryLocation } from "../src/memory-location.js"; -import { test, expect, beforeEach } from "bun:test"; - -beforeEach(() => history.replaceState(null, "", "/")); +import { test, expect } from "bun:test"; test("returns browser search string", () => { history.replaceState(null, "", "/users?active=true"); From 69ba3361809c1901430faabe7f6985db6ce14f2d Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 15:55:42 +0100 Subject: [PATCH 08/10] test: add missing acts --- packages/wouter/test/use-hash-location.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wouter/test/use-hash-location.test.tsx b/packages/wouter/test/use-hash-location.test.tsx index 46521c45..3a2e7b17 100644 --- a/packages/wouter/test/use-hash-location.test.tsx +++ b/packages/wouter/test/use-hash-location.test.tsx @@ -233,14 +233,14 @@ test("handles navigation with data: protocol", async () => { const initialHistoryLength = history.length; await waitForHashChangeEvent(() => { - navigate("/new-path"); + act(() => navigate("/new-path")); }); expect(location.hash).toBe("#/new-path"); expect(history.length).toBe(initialHistoryLength + 1); await waitForHashChangeEvent(() => { - navigate("/another-path", { replace: true }); + act(() => navigate("/another-path", { replace: true })); }); expect(location.hash).toBe("#/another-path"); From 5259e10a08830b87f59aa554ae49b2f9c83e0c09 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 15:55:48 +0100 Subject: [PATCH 09/10] test: remove unneeded hash reset before replaceState --- packages/wouter/test/use-location.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wouter/test/use-location.test.tsx b/packages/wouter/test/use-location.test.tsx index 254ceee6..df4f3265 100644 --- a/packages/wouter/test/use-location.test.tsx +++ b/packages/wouter/test/use-location.test.tsx @@ -43,7 +43,6 @@ describe.each([ navigate: hashNavigation, act: (cb: () => void) => waitForHashChangeEvent(() => act(cb)), clear: () => { - location.hash = ""; history.replaceState(null, "", "/"); }, }, From e31f290251c478ae94b9fb9188c84c3446c00976 Mon Sep 17 00:00:00 2001 From: zsugabubus Date: Sat, 21 Mar 2026 16:50:31 +0100 Subject: [PATCH 10/10] refactor: fix lint issues --- packages/wouter-preact/src/react-deps.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wouter-preact/src/react-deps.js b/packages/wouter-preact/src/react-deps.js index 3753c661..25de4db5 100644 --- a/packages/wouter-preact/src/react-deps.js +++ b/packages/wouter-preact/src/react-deps.js @@ -42,7 +42,7 @@ export function useSyncExternalStore(subscribe, getSnapshot, getSSRSnapshot) { if (!is(_instance._value, getSnapshot())) { forceUpdate({ _instance }); } - }, [subscribe, value, getSnapshot]); + }, [subscribe, value, getSnapshot]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!is(_instance._value, _instance._getSnapshot())) { @@ -54,7 +54,7 @@ export function useSyncExternalStore(subscribe, getSnapshot, getSSRSnapshot) { forceUpdate({ _instance }); } }); - }, [subscribe]); + }, [subscribe]); // eslint-disable-line react-hooks/exhaustive-deps return value; }