Skip to content

Commit dace9d7

Browse files
committed
dd
1 parent 6d5e2f1 commit dace9d7

3 files changed

Lines changed: 319 additions & 1 deletion

File tree

pages/js/index.template.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,17 @@ <h2>mouseenter, mouseleave, mouseover, mouseout, input[type="range"]</h2>
763763
</script>
764764

765765
<h2>new URL() new URLSearchParams()</h2>
766-
<a href="/viewer.html?file=%2Ftests%2Furl%2F001_url.unit.ts">urlwizzard.schema://urlwizzard.hostnegotiated/viewer.html?file=%2Ftests%2Furl%2F001_url.unit.ts</a>
766+
<script type="editor" data-lang="js">
767+
768+
// Tests for new URL() (and some tests covers also basics of new URLSearchParams())
769+
// urlwizzard.schema://urlwizzard.hostnegotiated/viewer.html?file=%2Ftests%2Furl%2F001_url.unit.ts
770+
771+
// also I've created set of tools to work with new URLSearchParams()
772+
// urlwizzard.schema://urlwizzard.hostnegotiated/viewer.html?file=%2Ftests%2Furl%2FtoolsURLSearchParams.ts
773+
// and tests for it: urlwizzard.schema://urlwizzard.hostnegotiated/viewer.html?file=%2Ftests%2Furl%2FtoolsURLSearchParams.test.ts
774+
775+
</script>
776+
</script>
767777
<h2>findLast</h2>
768778
<script type="editor" data-lang="js">
769779

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import assert from "node:assert";
2+
import { describe, it } from "node:test";
3+
4+
import { mergeURLSearchParams, cloneSearchParams, normalizeSearchParams, compareNormalizedSearchParams, sortSearchParamsByKeyThenValue } from "./toolsURLSearchParams.ts";
5+
6+
/**
7+
* node --test tests/url/toolsURLSearchParams.test.ts
8+
*/
9+
describe("mergeURLSearchParams", () => {
10+
it("should merge two URLSearchParams with distinct keys", () => {
11+
const a = new URLSearchParams("a=1&b=2");
12+
const b = new URLSearchParams("d=44&c=3&d=4");
13+
const merged = mergeURLSearchParams(a, b);
14+
assert.strictEqual(merged.toString(), "a=1&b=2&c=3&d=44&d=4");
15+
});
16+
17+
it("should merge two URLSearchParams overwriting older keys and sorting", () => {
18+
const a = new URLSearchParams("a=1&b=2&d=99");
19+
const b = new URLSearchParams("d=44&c=3");
20+
const merged = mergeURLSearchParams(a, b);
21+
assert.strictEqual(merged.toString(), "a=1&b=2&c=3&d=44");
22+
});
23+
24+
it("should merge three URLSearchParams where the last one wins", () => {
25+
const a = new URLSearchParams("a=1&b=1");
26+
const b = new URLSearchParams("b=2&c=2");
27+
const c = new URLSearchParams("c=3&d=3&b=3&c=33");
28+
const merged = mergeURLSearchParams(a, b, c);
29+
30+
// a=1 (from a)
31+
// b=3 (from c)
32+
// c=3&c=33 (from c)
33+
// d=3 (from c)
34+
assert.strictEqual(merged.toString(), "a=1&b=3&c=3&c=33&d=3");
35+
});
36+
37+
it("should correctly handle interleaving keys in multiple URLSearchParams", () => {
38+
const a = new URLSearchParams("z=9&x=1");
39+
const b = new URLSearchParams("y=2&x=22");
40+
const c = new URLSearchParams("a=5");
41+
const merged = mergeURLSearchParams(a, b, c);
42+
43+
// Expected sorted: a=5, x=22, y=2, z=9
44+
assert.strictEqual(merged.toString(), "a=5&x=22&y=2&z=9");
45+
});
46+
47+
it("should overwrite keys from earlier arguments with later ones", () => {
48+
const a = new URLSearchParams("a=1&b=2");
49+
const b = new URLSearchParams("b=3&c=4");
50+
const merged = mergeURLSearchParams(a, b);
51+
assert.strictEqual(merged.get("a"), "1");
52+
assert.strictEqual(merged.get("b"), "3");
53+
assert.strictEqual(merged.get("c"), "4");
54+
assert.strictEqual(merged.toString(), "a=1&b=3&c=4");
55+
});
56+
57+
it("should handle empty arguments", () => {
58+
const merged = mergeURLSearchParams();
59+
assert.strictEqual(merged.toString(), "");
60+
});
61+
62+
it("should merge more than two URLSearchParams", () => {
63+
const a = new URLSearchParams("a=1");
64+
const b = new URLSearchParams("b=2");
65+
const c = new URLSearchParams("c=3");
66+
const d = new URLSearchParams("a=4");
67+
const merged = mergeURLSearchParams(a, b, c, d);
68+
assert.strictEqual(merged.toString(), "a=4&b=2&c=3");
69+
});
70+
71+
it("should merge two URLSearchParams with distinct keys", () => {
72+
const a = new URLSearchParams("a=1&b=2&d=99");
73+
const b = new URLSearchParams("d=44&c=3");
74+
const merged = mergeURLSearchParams(a, b);
75+
merged.sort();
76+
assert.strictEqual(merged.toString(), "a=1&b=2&c=3&d=44");
77+
});
78+
79+
it("should only copy keys specified in the string array from subsequent URLSearchParams", () => {
80+
const a = new URLSearchParams("a=1&b=2");
81+
const b = new URLSearchParams("b=99&c=3&d=4");
82+
const merged = mergeURLSearchParams(a, ["b", "c"], b);
83+
84+
// a=1 (from a, kept because a is first)
85+
// b=99 (from b, allowed by filter)
86+
// c=3 (from b, allowed by filter)
87+
// d=4 is IGNORED because it's not in the filter
88+
assert.strictEqual(merged.toString(), "a=1&b=99&c=3");
89+
});
90+
91+
it("empty on start", () => {
92+
const z = new URLSearchParams("");
93+
const a = new URLSearchParams("a=1&b=2");
94+
const b = new URLSearchParams("b=99&c=3&d=4");
95+
const merged = mergeURLSearchParams(z, a, ["b", "c"], b);
96+
97+
// a=1 (from a, kept because a is first)
98+
// b=99 (from b, allowed by filter)
99+
// c=3 (from b, allowed by filter)
100+
// d=4 is IGNORED because it's not in the filter
101+
assert.strictEqual(merged.toString(), "b=99&c=3");
102+
});
103+
104+
it("repack multipe", () => {
105+
const z = new URLSearchParams("");
106+
const a = new URLSearchParams("a=a1&b=2&a=a2&z=zzz");
107+
const b = new URLSearchParams("b=99&n=nn&a=3&d=4&a=5");
108+
const merged = mergeURLSearchParams(z, a, ["a", "b", "c"], b);
109+
110+
assert.strictEqual(merged.toString(), "a=3&a=5&b=99");
111+
});
112+
113+
it("repack multipe - keep z", () => {
114+
const z = new URLSearchParams("z=zzz");
115+
const a = new URLSearchParams("a=a1&b=2&a=a2&");
116+
const b = new URLSearchParams("b=99&n=nn&a=3&d=4&a=5");
117+
const merged = mergeURLSearchParams(z, a, ["a", "b", "c"], b);
118+
119+
assert.strictEqual(merged.toString(), "a=3&a=5&b=99&z=zzz");
120+
});
121+
122+
it("repack multipe - without filter", () => {
123+
const z = new URLSearchParams("z=zzz");
124+
const a = new URLSearchParams("a=a1&b=2&a=a2&");
125+
const b = new URLSearchParams("b=99&n=nn&a=3&d=4&a=5");
126+
const merged = mergeURLSearchParams(z, a, b);
127+
128+
assert.strictEqual(merged.toString(), "a=3&a=5&b=99&d=4&n=nn&z=zzz");
129+
});
130+
});
131+
132+
describe("cloneSearchParams", () => {
133+
it("should create a new URLSearchParams instance with identical parameters", () => {
134+
const original = new URLSearchParams("a=1&b=2&a=3");
135+
const cloned = cloneSearchParams(original);
136+
137+
// Verify it contains the exact same data
138+
assert.strictEqual(cloned.toString(), original.toString());
139+
assert.strictEqual(cloned.get("a"), "1");
140+
assert.deepStrictEqual(cloned.getAll("a"), ["1", "3"]);
141+
142+
// Verify it is NOT the same object reference
143+
assert.notStrictEqual(cloned, original);
144+
145+
// Verify mutation of clone does not affect original
146+
cloned.set("c", "4");
147+
assert.strictEqual(cloned.has("c"), true);
148+
assert.strictEqual(original.has("c"), false);
149+
});
150+
});
151+
152+
describe("normalizeSearchParams", () => {
153+
it("should return a new sorted URLSearchParams instance", () => {
154+
const original = new URLSearchParams("z=1&a=2&m=3");
155+
const normalized = normalizeSearchParams(original);
156+
157+
assert.strictEqual(normalized.toString(), "a=2&m=3&z=1");
158+
assert.notStrictEqual(normalized, original);
159+
});
160+
});
161+
162+
describe("compareNormalizedSearchParams", () => {
163+
it("should return true for identical unsorted URLSearchParams", () => {
164+
const a = new URLSearchParams("z=1&a=2");
165+
const b = new URLSearchParams("a=2&z=1");
166+
167+
assert.strictEqual(compareNormalizedSearchParams(a, b), true);
168+
});
169+
170+
it("should return false for different URLSearchParams", () => {
171+
const a = new URLSearchParams("a=1&b=2");
172+
const b = new URLSearchParams("a=1&b=3");
173+
174+
assert.strictEqual(compareNormalizedSearchParams(a, b), false);
175+
});
176+
177+
it("should return true for complex overlapping search params", () => {
178+
const a = new URLSearchParams("foo=bar&baz=qux&foo=baz");
179+
const b = new URLSearchParams("baz=qux&foo=bar&foo=baz");
180+
assert.strictEqual(compareNormalizedSearchParams(a, b), true);
181+
});
182+
});
183+
184+
describe("sortSearchParamsByKeyThenValue", () => {
185+
it("should sort parameters primarily by key, and secondarily by value", () => {
186+
const original = new URLSearchParams("b=2&a=3&a=1");
187+
const sorted = sortSearchParamsByKeyThenValue(original);
188+
189+
// 'a' comes before 'b', and 'a=1' comes before 'a=3'
190+
assert.strictEqual(sorted.toString(), "a=1&a=3&b=2");
191+
});
192+
193+
it("should correctly sort values lexicographically for identical keys", () => {
194+
const original = new URLSearchParams("x=zebra&x=apple&y=test&x=banana");
195+
const sorted = sortSearchParamsByKeyThenValue(original);
196+
197+
// Keys sorted: x, x, x, y
198+
// x values sorted: apple, banana, zebra
199+
assert.strictEqual(sorted.toString(), "x=apple&x=banana&x=zebra&y=test");
200+
});
201+
202+
it("should preserve empty URLSearchParams", () => {
203+
const original = new URLSearchParams("");
204+
const sorted = sortSearchParamsByKeyThenValue(original);
205+
206+
assert.strictEqual(sorted.toString(), "");
207+
});
208+
});

tests/url/toolsURLSearchParams.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Merges multiple URLSearchParams instances. The first parameter acts as the base.
3+
* Subsequent parameters overwrite existing keys. If a string array is provided,
4+
* it acts as an allowlist, meaning only those keys will be merged from subsequent instances.
5+
* The final result is sorted alphabetically.
6+
*/
7+
export function mergeURLSearchParams(
8+
...args: (URLSearchParams | string[])[]
9+
): URLSearchParams {
10+
const result = new URLSearchParams();
11+
if (args.length === 0) return result;
12+
13+
// Find the filter array if it exists
14+
const filterIndex = args.findIndex((arg) => Array.isArray(arg));
15+
const filterKeys = filterIndex !== -1 ? (args[filterIndex] as string[]) : null;
16+
const filterSet = filterKeys ? new Set(filterKeys) : null;
17+
18+
for (let i = 0; i < args.length; i++) {
19+
const params = args[i];
20+
if (params instanceof URLSearchParams) {
21+
// The first URLSearchParams contributes all its keys.
22+
// Subsequent ones only contribute keys in the filterSet (if defined).
23+
const isFirstParams = i === args.findIndex(arg => arg instanceof URLSearchParams);
24+
25+
const uniqueKeys = new Set<string>();
26+
params.forEach((_, key) => uniqueKeys.add(key));
27+
const keys = Array.from(uniqueKeys);
28+
const keysToProcess = (!isFirstParams && filterSet)
29+
? keys.filter(k => filterSet.has(k))
30+
: keys;
31+
32+
for (const key of keysToProcess) {
33+
result.delete(key);
34+
params.getAll(key).forEach((value) => {
35+
result.append(key, value);
36+
});
37+
}
38+
}
39+
}
40+
41+
result.sort();
42+
43+
return result;
44+
}
45+
46+
/**
47+
* Creates a deep, distinct copy of a URLSearchParams instance, preserving its data
48+
* but decoupling its object reference so mutations don't affect the original.
49+
*/
50+
export function cloneSearchParams(
51+
params: URLSearchParams
52+
): URLSearchParams {
53+
return new URLSearchParams(params);
54+
}
55+
56+
/**
57+
* Returns a new URLSearchParams instance with all keys sorted alphabetically.
58+
* Useful for ensuring consistent parameter ordering before serialization.
59+
*/
60+
export function normalizeSearchParams(
61+
params: URLSearchParams
62+
): URLSearchParams {
63+
const result = new URLSearchParams(params);
64+
result.sort();
65+
return result;
66+
}
67+
68+
/**
69+
* Compares two URLSearchParams instances for equality by normalizing and sorting them.
70+
* Returns true if they contain the exact same keys and values regardless of original order.
71+
*/
72+
export function compareNormalizedSearchParams(
73+
a: URLSearchParams,
74+
b: URLSearchParams
75+
): boolean {
76+
const na = normalizeSearchParams(a);
77+
const nb = normalizeSearchParams(b);
78+
79+
return na.toString() === nb.toString();
80+
}
81+
82+
/**
83+
* Returns a new URLSearchParams instance sorted primarily by keys,
84+
* and secondarily by values if the keys are identical.
85+
*/
86+
export function sortSearchParamsByKeyThenValue(
87+
params: URLSearchParams
88+
): URLSearchParams {
89+
const entries: [string, string][] = [];
90+
params.forEach((value, key) => {
91+
entries.push([key, value]);
92+
});
93+
94+
return new URLSearchParams(
95+
entries.sort(([k1, v1], [k2, v2]) => {
96+
if (k1 === k2) return v1.localeCompare(v2);
97+
return k1.localeCompare(k2);
98+
})
99+
);
100+
}

0 commit comments

Comments
 (0)