Skip to content

Commit 5dc00b5

Browse files
frano-mclaude
andcommitted
test: cover wasPop and friends in UseStateSync/utils (#928)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7b408b1 commit 5dc00b5

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import {
2+
hasParams,
3+
isSynced,
4+
stringifyQuery,
5+
wasPop,
6+
} from "../src/hooks/stateSyncManager/hooks/UseStateSync/utils";
7+
import { NextHistoryState } from "../src/services/beforePopState/types";
8+
9+
/**
10+
* Builds a minimal NextHistoryState for tests.
11+
* @param url - The resolved URL the browser is navigating to (the form
12+
* Next.js stores in history state — always the actual URL, never a route
13+
* pattern).
14+
* @returns NextHistoryState with the given url and no-op as/options.
15+
*/
16+
function buildHistoryState(url: string): NextHistoryState {
17+
return { as: url, options: {}, url };
18+
}
19+
20+
describe("wasPop", () => {
21+
it("returns false when nextHistoryState is undefined", () => {
22+
expect(wasPop("", "/projects", undefined)).toBe(false);
23+
});
24+
25+
it("returns true when pathname matches the path component of the history URL", () => {
26+
expect(wasPop("", "/projects", buildHistoryState("/projects"))).toBe(true);
27+
});
28+
29+
it("strips the query string off the history URL before comparing", () => {
30+
expect(
31+
wasPop("", "/projects", buildHistoryState("/projects?filter=foo")),
32+
).toBe(true);
33+
});
34+
35+
it("returns false when pathname does not match", () => {
36+
expect(wasPop("", "/projects", buildHistoryState("/files"))).toBe(false);
37+
});
38+
39+
it("defaults basePath to empty string when not provided", () => {
40+
expect(wasPop(undefined, "/projects", buildHistoryState("/projects"))).toBe(
41+
true,
42+
);
43+
});
44+
45+
it("prepends basePath to pathname before comparing", () => {
46+
expect(
47+
wasPop("/data", "/projects", buildHistoryState("/data/projects")),
48+
).toBe(true);
49+
});
50+
51+
it("returns false when basePath is set but missing from the history URL", () => {
52+
expect(wasPop("/data", "/projects", buildHistoryState("/projects"))).toBe(
53+
false,
54+
);
55+
});
56+
57+
// Documents the contract the usePathname() migration relies on: pathname is
58+
// the resolved URL (e.g. /anvil-cmg/abc-123), not the route pattern
59+
// (/[entityListType]/[entityId]). The first matches; the second does not.
60+
it("matches when pathname is the resolved URL (post-migration form)", () => {
61+
expect(
62+
wasPop(
63+
"",
64+
"/anvil-cmg/abc-123",
65+
buildHistoryState("/anvil-cmg/abc-123?filter=foo"),
66+
),
67+
).toBe(true);
68+
});
69+
70+
it("does not match when pathname is a route pattern (pre-migration form on dynamic routes)", () => {
71+
expect(
72+
wasPop(
73+
"",
74+
"/[entityListType]/[entityId]",
75+
buildHistoryState("/anvil-cmg/abc-123"),
76+
),
77+
).toBe(false);
78+
});
79+
});
80+
81+
describe("hasParams", () => {
82+
it("returns true when any param key has a defined value in the query", () => {
83+
expect(hasParams({ filter: "foo" }, ["filter"])).toBe(true);
84+
});
85+
86+
it("returns true when at least one of multiple param keys is present", () => {
87+
expect(hasParams({ sort: "asc" }, ["filter", "sort"])).toBe(true);
88+
});
89+
90+
it("returns false when none of the param keys are in the query", () => {
91+
expect(hasParams({ other: "x" }, ["filter", "sort"])).toBe(false);
92+
});
93+
94+
it("returns false for an empty paramKeys list", () => {
95+
expect(hasParams({ filter: "foo" }, [])).toBe(false);
96+
});
97+
98+
it("returns false when a param key is present but undefined", () => {
99+
expect(hasParams({ filter: undefined }, ["filter"])).toBe(false);
100+
});
101+
});
102+
103+
describe("isSynced", () => {
104+
it("returns true for two empty queries", () => {
105+
expect(isSynced({}, {})).toBe(true);
106+
});
107+
108+
it("returns true when queries have the same keys/values in different order", () => {
109+
// eslint-disable-next-line sort-keys -- intentionally unsorted to exercise insertion-order independence.
110+
expect(isSynced({ a: "1", b: "2" }, { b: "2", a: "1" })).toBe(true);
111+
});
112+
113+
it("returns false when queries differ in value", () => {
114+
expect(isSynced({ a: "1" }, { a: "2" })).toBe(false);
115+
});
116+
117+
it("returns false when one query has extra keys", () => {
118+
expect(isSynced({ a: "1" }, { a: "1", b: "2" })).toBe(false);
119+
});
120+
});
121+
122+
describe("stringifyQuery", () => {
123+
it("produces identical output regardless of insertion order", () => {
124+
expect(stringifyQuery({ a: "1", b: "2" })).toBe(
125+
// eslint-disable-next-line sort-keys -- intentionally unsorted to exercise insertion-order independence.
126+
stringifyQuery({ b: "2", a: "1" }),
127+
);
128+
});
129+
130+
it("produces empty-object JSON for an empty query", () => {
131+
expect(stringifyQuery({})).toBe("{}");
132+
});
133+
});

0 commit comments

Comments
 (0)