Skip to content

Commit 00a2ef1

Browse files
adrian-niculescuAdrian Niculescu
authored andcommitted
fix: URLSearchParams construction and iteration spec compliance
The constructor previously handled only the string init form; any object init was stringified to "[object Object]", collapsing every query parameter into a single bogus key. It now follows the WHATWG/WebIDL init dispatch (https://url.spec.whatwg.org/#interface-urlsearchparams): record, sequence (any iterable of pairs: Array, Map, Set, another URLSearchParams, generators - with IteratorClose on abrupt completion), other primitives (USVString coercion; Symbol throws), and null/undefined (empty). entries()/keys()/values() returned plain v8::Arrays and the type exposed no @@iterator, so for..of, spread, and the copy-constructor form did not work; the array path also returned the first value for every occurrence of a repeated key. They now return genuine live ES iterators (reflecting mutations mid-iteration, per spec), and @@iterator aliases entries(). Also aligned to spec: get() returns null (not undefined) for a missing name; delete(name, value) and has(name, value) honor the optional value argument (coerced to USVString; an explicit undefined is treated as omitted, matching WPT).
1 parent ac4e641 commit 00a2ef1

2 files changed

Lines changed: 757 additions & 89 deletions

File tree

test-app/app/src/main/assets/app/tests/testURLSearchParamsImpl.js

Lines changed: 301 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
describe("Test URLSearchParams ", function () {
22
const fooBar = "foo=1&bar=2";
33
it("Test URLSearchParams keys", function(){
4+
// keys() returns a spec iterator, not an array — consume it via spread.
45
const params = new URLSearchParams(fooBar);
5-
const keys = params.keys();
6+
const keys = [...params.keys()];
67
expect(keys[0]).toBe("foo");
78
expect(keys[1]).toBe("bar");
89
});
910

1011
it("Test URLSearchParams values", function(){
1112
const params = new URLSearchParams(fooBar);
12-
const values = params.values();
13+
const values = [...params.values()];
1314
expect(values[0]).toBe("1");
1415
expect(values[1]).toBe("2");
1516
});
1617

1718

1819
it("Test URLSearchParams entries", function(){
1920
const params = new URLSearchParams(fooBar);
20-
const entries = params.entries();
21+
const entries = [...params.entries()];
2122
expect(entries[0][0]).toBe("foo");
2223
expect(entries[0][1]).toBe("1");
2324

@@ -26,6 +27,74 @@ describe("Test URLSearchParams ", function () {
2627

2728
});
2829

30+
it("Test URLSearchParams keys/values/entries return spec iterators", function(){
31+
const params = new URLSearchParams(fooBar);
32+
// A spec iterator has a next() and is itself iterable.
33+
expect(typeof params.entries().next).toBe("function");
34+
expect(typeof params.keys().next).toBe("function");
35+
expect(typeof params.values().next).toBe("function");
36+
const it = params.entries();
37+
const first = it.next();
38+
expect(first.done).toBe(false);
39+
expect(first.value[0]).toBe("foo");
40+
expect(first.value[1]).toBe("1");
41+
});
42+
43+
it("Test URLSearchParams entries preserves duplicate keys", function(){
44+
// Regression: the old get_keys()+get() path returned the first value for
45+
// every occurrence of a repeated key.
46+
const params = new URLSearchParams("a=1&a=2&b=3");
47+
const entries = [...params.entries()];
48+
expect(entries.length).toBe(3);
49+
expect(entries[0][1]).toBe("1");
50+
expect(entries[1][1]).toBe("2");
51+
expect(entries[2][1]).toBe("3");
52+
expect([...params.values()].join(",")).toBe("1,2,3");
53+
});
54+
55+
it("Test URLSearchParams default iterator aliases entries", function(){
56+
// The default @@iterator IS the entries method (browser identity). This binding
57+
// installs members per-instance (not on the prototype), so assert on an instance
58+
// AND assert it is actually a function — not the vacuous undefined === undefined.
59+
const params = new URLSearchParams(fooBar);
60+
expect(typeof params[Symbol.iterator]).toBe("function");
61+
expect(params[Symbol.iterator]).toBe(params.entries);
62+
});
63+
64+
it("Test URLSearchParams iterator carries the spec brand", function(){
65+
const params = new URLSearchParams(fooBar);
66+
expect(Object.prototype.toString.call(params.entries())).toBe("[object URLSearchParams Iterator]");
67+
});
68+
69+
it("Test URLSearchParams iterator is live", function(){
70+
// Spec iterators reflect mutations made after they are created.
71+
const params = new URLSearchParams("a=1&b=2");
72+
const it = params.entries();
73+
expect(it.next().value[0]).toBe("a"); // consume "a"
74+
params.append("c", "3"); // mutate mid-iteration
75+
const rest = [];
76+
let r;
77+
while (!(r = it.next()).done) {
78+
rest.push(r.value[0]);
79+
}
80+
expect(rest.join(",")).toBe("b,c"); // sees the appended "c"
81+
});
82+
83+
it("Test URLSearchParams closes the source iterator on a bad pair", function(){
84+
// On an abrupt completion (a too-long pair) the source iterator must be
85+
// closed, so a generator's finally runs and resource-backed iterables free.
86+
let closed = false;
87+
function* gen() {
88+
try {
89+
yield ["a", "1", "2"]; // 3-element pair → TypeError
90+
} finally {
91+
closed = true;
92+
}
93+
}
94+
expect(function(){ new URLSearchParams(gen()); }).toThrow();
95+
expect(closed).toBe(true);
96+
});
97+
2998

3099
it("Test URLSearchParams size", function(){
31100
const params = new URLSearchParams(fooBar);
@@ -43,7 +112,27 @@ describe("Test URLSearchParams ", function () {
43112
const params = new URLSearchParams(fooBar);
44113
params.append("first", "Osei");
45114
params.delete("first");
46-
expect(params.get("first")).toBe(undefined);
115+
// Spec: get() returns null for a missing name (url.bs:4016).
116+
expect(params.get("first")).toBe(null);
117+
});
118+
119+
it("Test URLSearchParams get returns null for a missing name", function(){
120+
// Spec get(name): "...otherwise null" (url.bs:4016).
121+
const params = new URLSearchParams("a=1");
122+
expect(params.get("missing")).toBe(null);
123+
});
124+
125+
it("Test URLSearchParams delete with value removes only matching pairs", function(){
126+
// Spec delete(name, value): when value is given, remove only tuples
127+
// matching BOTH name and value (url.bs:4000). The value is a USVString,
128+
// so a non-string (the number 1) coerces to "1".
129+
const params = new URLSearchParams("a=1&a=2&a=1&b=1");
130+
params.delete("a", 1);
131+
expect(params.getAll("a").join(",")).toBe("2");
132+
expect(params.getAll("b").join(",")).toBe("1");
133+
// Single-arg delete still removes every pair with that name.
134+
params.delete("a");
135+
expect(params.has("a")).toBe(false);
47136
});
48137

49138

@@ -52,6 +141,47 @@ describe("Test URLSearchParams ", function () {
52141
expect(params.has("foo")).toBe(true);
53142
});
54143

144+
it("Test URLSearchParams has with value matches name and value", function(){
145+
// Spec has(name, value): true only for a tuple matching BOTH (url.bs:4028).
146+
// The value is a USVString, so non-strings (number, boolean) coerce.
147+
const params = new URLSearchParams("a=1&a=2&flag=true");
148+
expect(params.has("a", "1")).toBe(true);
149+
expect(params.has("a", 2)).toBe(true); // number coerces to "2"
150+
expect(params.has("a", "3")).toBe(false);
151+
expect(params.has("flag", true)).toBe(true); // boolean coerces to "true"
152+
expect(params.has("missing", "1")).toBe(false);
153+
// Single-arg has still matches by name only.
154+
expect(params.has("a")).toBe(true);
155+
});
156+
157+
it("Test URLSearchParams has/delete throw when the value cannot be coerced", function(){
158+
// The value argument is a USVString; a Symbol (or a throwing toString)
159+
// cannot convert, so the call must throw rather than silently matching ""
160+
// (WebIDL USVString conversion, url.bs:4000 / 4028).
161+
const params = new URLSearchParams("a=1");
162+
expect(function(){ params.has("a", Symbol("x")); }).toThrow();
163+
expect(function(){ params.delete("a", Symbol("x")); }).toThrow();
164+
});
165+
166+
it("Test URLSearchParams has/delete treat an explicit undefined value as omitted", function(){
167+
// Per WPT (urlsearchparams-has / -delete "respects undefined as second
168+
// arg"), an explicit `undefined` second argument is treated as omitted
169+
// (name-only), NOT as the string "undefined".
170+
const params = new URLSearchParams("a=b&a=d&c&e&");
171+
expect(params.has("a", "b")).toBe(true);
172+
expect(params.has("a", "c")).toBe(false);
173+
expect(params.has("a", undefined)).toBe(true); // undefined -> name-only
174+
175+
const del = new URLSearchParams();
176+
del.append("a", "b");
177+
del.append("a", "c");
178+
del.append("b", "c");
179+
del.append("b", "d");
180+
del.delete("b", "c");
181+
del.delete("a", undefined); // undefined -> delete by name
182+
expect(del.toString()).toBe("b=d");
183+
});
184+
55185
it("Test URLSearchParams changes propagates to URL parent", function(){
56186
const toBe = 'https://github.com/triniwiz?first=Osei';
57187
const url = new URL('https://github.com/triniwiz');
@@ -114,4 +244,171 @@ describe("Test URLSearchParams ", function () {
114244
expect(results[2].value).toBe("3");
115245
});
116246

247+
it("Test URLSearchParams from record object", function(){
248+
const params = new URLSearchParams({ foo: "1", bar: "2" });
249+
expect(params.get("foo")).toBe("1");
250+
expect(params.get("bar")).toBe("2");
251+
expect(params.size).toBe(2);
252+
// A plain object must expand to its entries, not collapse into a
253+
// single "[object Object]" key.
254+
expect(params.has("[object Object]")).toBe(false);
255+
});
256+
257+
it("Test URLSearchParams from record serializes every pair in toString", function(){
258+
const params = new URLSearchParams({ one: "1", two: "2" });
259+
expect(params.toString()).toBe("one=1&two=2");
260+
});
261+
262+
it("Test URLSearchParams from record coerces values to strings", function(){
263+
const params = new URLSearchParams({ a: 1, b: true });
264+
expect(params.get("a")).toBe("1");
265+
expect(params.get("b")).toBe("true");
266+
});
267+
268+
it("Test URLSearchParams from record encodes special characters", function(){
269+
const params = new URLSearchParams({ q: "a b&c" });
270+
expect(params.get("q")).toBe("a b&c");
271+
expect(params.toString()).toBe("q=a+b%26c");
272+
});
273+
274+
it("Test URLSearchParams from array of pairs", function(){
275+
const params = new URLSearchParams([["foo", "1"], ["bar", "2"], ["foo", "3"]]);
276+
expect(params.get("foo")).toBe("1");
277+
expect(params.getAll("foo").length).toBe(2);
278+
expect(params.get("bar")).toBe("2");
279+
expect(params.size).toBe(3);
280+
});
281+
282+
it("Test URLSearchParams empty record and no-arg produce empty query", function(){
283+
expect(new URLSearchParams().toString()).toBe("");
284+
expect(new URLSearchParams({}).toString()).toBe("");
285+
});
286+
287+
it("Test URLSearchParams from record throws when a value cannot be coerced to a string", function(){
288+
// Per spec the record/sequence init coerces every value to a USVString;
289+
// a value that cannot convert (a Symbol here) must throw rather than
290+
// silently dropping or emptying the entry.
291+
expect(function(){ new URLSearchParams({ bad: Symbol("x") }); }).toThrow();
292+
});
293+
294+
// --- Iterable (sequence) init: any iterable of pairs, not only arrays. ---
295+
296+
it("Test URLSearchParams from a Map", function(){
297+
const params = new URLSearchParams(new Map([["a", "1"], ["b", "2"]]));
298+
expect(params.toString()).toBe("a=1&b=2");
299+
});
300+
301+
it("Test URLSearchParams from a Set of pairs", function(){
302+
const params = new URLSearchParams(new Set([["x", "1"], ["y", "2"]]));
303+
expect(params.toString()).toBe("x=1&y=2");
304+
});
305+
306+
it("Test URLSearchParams copy-constructs from another URLSearchParams", function(){
307+
// A URLSearchParams is iterable, so per spec it resolves to the sequence
308+
// (copy) form — including duplicate keys, which proves the @@iterator walks
309+
// entries rather than collapsing them.
310+
const source = new URLSearchParams("a=1&a=2&b=3");
311+
const copy = new URLSearchParams(source);
312+
expect(copy.toString()).toBe("a=1&a=2&b=3");
313+
expect(copy.getAll("a").length).toBe(2);
314+
});
315+
316+
it("Test URLSearchParams from a generator of pairs", function(){
317+
function* pairs() {
318+
yield ["a", "1"];
319+
yield ["b", "2"];
320+
}
321+
const params = new URLSearchParams(pairs());
322+
expect(params.toString()).toBe("a=1&b=2");
323+
});
324+
325+
it("Test URLSearchParams from sequence with non-array inner pairs", function(){
326+
// Each pair need only be a 2-element iterable, not specifically an array.
327+
// A Set is iterable but not an Array, so it exercises the inner iterator path.
328+
const params = new URLSearchParams([new Set(["k", "v"])]);
329+
expect(params.get("k")).toBe("v");
330+
});
331+
332+
it("Test URLSearchParams sequence init throws on a too-long pair", function(){
333+
expect(function(){ new URLSearchParams([["a", "1", "2"]]); }).toThrow();
334+
});
335+
336+
it("Test URLSearchParams sequence init throws on a too-short pair", function(){
337+
expect(function(){ new URLSearchParams([["a"]]); }).toThrow();
338+
});
339+
340+
it("Test URLSearchParams sequence init throws on a non-iterable element", function(){
341+
expect(function(){ new URLSearchParams([null]); }).toThrow();
342+
expect(function(){ new URLSearchParams([1]); }).toThrow();
343+
});
344+
345+
it("Test URLSearchParams sequence init throws on a primitive string element", function(){
346+
// WebIDL converts each element to sequence<USVString>, whose first step throws
347+
// when the element is not an Object. A 2-code-point string must NOT be accepted
348+
// as the pair ("a","b").
349+
expect(function(){ new URLSearchParams(["ab"]); }).toThrow();
350+
});
351+
352+
it("Test URLSearchParams sequence init accepts a String-object element", function(){
353+
// A String *object* IS an Object and is iterable, so it is a valid 2-char pair.
354+
const params = new URLSearchParams([new String("ab")]);
355+
expect(params.get("a")).toBe("b");
356+
});
357+
358+
it("Test URLSearchParams throws when @@iterator is present but not callable", function(){
359+
// Per WebIDL GetMethod, a non-callable @@iterator is a TypeError, not a
360+
// silent fall-back to the record form.
361+
expect(function(){ new URLSearchParams({ [Symbol.iterator]: 5 }); }).toThrow();
362+
});
363+
364+
// --- The type itself is iterable<USVString, USVString>. ---
365+
366+
it("Test URLSearchParams is spread-iterable via Symbol.iterator", function(){
367+
const params = new URLSearchParams("a=1&b=2");
368+
const entries = [...params];
369+
expect(entries.length).toBe(2);
370+
expect(entries[0][0]).toBe("a");
371+
expect(entries[0][1]).toBe("1");
372+
expect(entries[1][0]).toBe("b");
373+
expect(entries[1][1]).toBe("2");
374+
});
375+
376+
it("Test URLSearchParams works in a for..of loop", function(){
377+
const params = new URLSearchParams("a=1&a=2");
378+
const seen = [];
379+
for (const [key, value] of params) {
380+
seen.push(key + "=" + value);
381+
}
382+
expect(seen.length).toBe(2);
383+
expect(seen[0]).toBe("a=1");
384+
expect(seen[1]).toBe("a=2");
385+
});
386+
387+
// --- Primitive init: coerced to USVString, then parsed. ---
388+
389+
it("Test URLSearchParams from a number", function(){
390+
expect(new URLSearchParams(123).toString()).toBe("123=");
391+
});
392+
393+
it("Test URLSearchParams from a boolean", function(){
394+
expect(new URLSearchParams(true).toString()).toBe("true=");
395+
});
396+
397+
it("Test URLSearchParams from a bigint", function(){
398+
expect(new URLSearchParams(10n).toString()).toBe("10=");
399+
});
400+
401+
it("Test URLSearchParams strips a single leading question mark", function(){
402+
expect(new URLSearchParams("?a=1").get("a")).toBe("1");
403+
});
404+
405+
it("Test URLSearchParams throws when init is a Symbol", function(){
406+
expect(function(){ new URLSearchParams(Symbol("x")); }).toThrow();
407+
});
408+
409+
it("Test URLSearchParams from null or undefined is empty", function(){
410+
expect(new URLSearchParams(null).toString()).toBe("");
411+
expect(new URLSearchParams(undefined).toString()).toBe("");
412+
});
413+
117414
});

0 commit comments

Comments
 (0)