Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
[test]
preload = ["./packages/wouter/test/setup.ts"]
coverageSkipTestFiles = true
coveragePathIgnorePatterns = ["**/test/**"]
coveragePathIgnorePatterns = [
"**/test/**",
# temporary copies of packages/wouter/src/* created by preact.test.tsx;
# already measured at their original path (react-deps.js is preact-specific and stays measured)
"packages/wouter-preact/src/index.js",
"packages/wouter-preact/src/paths.js",
"packages/wouter-preact/src/memory-location.js",
"packages/wouter-preact/src/use-browser-location.js",
"packages/wouter-preact/src/use-hash-location.js",
"packages/wouter-preact/src/use-sync-external-store.js",
"packages/wouter-preact/src/use-sync-external-store.native.js",
]
coverageThreshold = { lines = 0.99 }
50 changes: 50 additions & 0 deletions packages/wouter-preact/test/preact.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,56 @@ describe("Preact support", () => {
});
});

describe("useSyncExternalStore shim", () => {
test("re-renders when the store mutates between render and layout effect", async () => {
// the internal shim is untyped, it mirrors the React useSyncExternalStore signature
const { useSyncExternalStore } = (await import(
// @ts-expect-error
"../src/react-deps.js"
)) as {
useSyncExternalStore: <T>(
subscribe: (cb: () => void) => () => void,
getSnapshot: () => T,
getSSRSnapshot?: () => T
) => T;
};
const { useLayoutEffect } = await import("preact/hooks");

let value = "initial";
const listeners = new Set<() => void>();
const subscribe = (cb: () => void) => {
listeners.add(cb);
return () => listeners.delete(cb);
};

// child layout effects run before the parent's, so this mutation happens
// after the parent has rendered but before its layout effect compares snapshots
const Child = () => {
useLayoutEffect(() => {
value = "mutated";
}, []);
return null;
};

const Parent = () => {
const snapshot = useSyncExternalStore(subscribe, () => value);
return (
<div data-testid="snapshot">
{snapshot}
<Child />
</div>
);
};

const container = document.body.appendChild(document.createElement("div"));
act(() => {
render(<Parent />, container);
});

expect(container.textContent).toBe("mutated");
});
});

describe("Preact SSR", () => {
test.skip("supports SSR (fix: useSyncExternalStore polyfill in Bun)", async () => {
const { Router, useLocation } = await loadPreact();
Expand Down
6 changes: 6 additions & 0 deletions packages/wouter/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ GlobalRegistrator.register({
// Extend Bun's expect with jest-dom matchers
(expect as any).extend(matchers);

// Load the library first so the history monkey-patch in use-browser-location.js
// is applied (and attributed) to this copy of the module, not the temporary
// wouter-preact copy created by preact.test.tsx. Must run after happy-dom
// registration, hence a dynamic import.
await import("../src/use-browser-location.js");

/**
* Runs a function with `location` temporarily removed from globalThis.
* Simulates pure Node.js SSR environment for testing.
Expand Down
8 changes: 8 additions & 0 deletions packages/wouter/test/use-search.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ test("returns browser search string", () => {
expect(result.current).toEqual("active=true");
});

test("returns search string as-is when it contains malformed escapes", () => {
history.replaceState(null, "", "/users?q=100%");
const { result } = renderHook(() => useSearch());

// decodeURI throws on "%", sanitizeSearch falls back to the raw string
expect(result.current).toEqual("q=100%");
});

test("can be customized in the Router", () => {
const customSearchHook = ({ customOption = "unused" }) => "none";

Expand Down
Loading