Skip to content

Commit cc48f21

Browse files
committed
url: extend URLSearchParams constructor
PR-URL: #11060 Fixes: #10635 Ref: whatwg/url#175 Ref: web-platform-tests/wpt#4523 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
1 parent a1c91ec commit cc48f21

File tree

2 files changed

+116
-14
lines changed

2 files changed

+116
-14
lines changed

lib/internal/url.js

+46-4
Original file line numberDiff line numberDiff line change
@@ -677,11 +677,53 @@ function defineIDLClass(proto, classStr, obj) {
677677
}
678678

679679
class URLSearchParams {
680-
constructor(init = '') {
681-
if (init instanceof URLSearchParams) {
682-
const childParams = init[searchParams];
683-
this[searchParams] = childParams.slice();
680+
// URL Standard says the default value is '', but as undefined and '' have
681+
// the same result, undefined is used to prevent unnecessary parsing.
682+
// Default parameter is necessary to keep URLSearchParams.length === 0 in
683+
// accordance with Web IDL spec.
684+
constructor(init = undefined) {
685+
if (init === null || init === undefined) {
686+
this[searchParams] = [];
687+
} else if (typeof init === 'object') {
688+
const method = init[Symbol.iterator];
689+
if (method === this[Symbol.iterator]) {
690+
// While the spec does not have this branch, we can use it as a
691+
// shortcut to avoid having to go through the costly generic iterator.
692+
const childParams = init[searchParams];
693+
this[searchParams] = childParams.slice();
694+
} else if (method !== null && method !== undefined) {
695+
if (typeof method !== 'function') {
696+
throw new TypeError('Query pairs must be iterable');
697+
}
698+
699+
// sequence<sequence<USVString>>
700+
// Note: per spec we have to first exhaust the lists then process them
701+
const pairs = [];
702+
for (const pair of init) {
703+
if (typeof pair !== 'object' ||
704+
typeof pair[Symbol.iterator] !== 'function') {
705+
throw new TypeError('Each query pair must be iterable');
706+
}
707+
pairs.push(Array.from(pair));
708+
}
709+
710+
this[searchParams] = [];
711+
for (const pair of pairs) {
712+
if (pair.length !== 2) {
713+
throw new TypeError('Each query pair must be a name/value tuple');
714+
}
715+
this[searchParams].push(String(pair[0]), String(pair[1]));
716+
}
717+
} else {
718+
// record<USVString, USVString>
719+
this[searchParams] = [];
720+
for (const key of Object.keys(init)) {
721+
const value = String(init[key]);
722+
this[searchParams].push(key, value);
723+
}
724+
}
684725
} else {
726+
// USVString
685727
init = String(init);
686728
if (init[0] === '?') init = init.slice(1);
687729
initSearchParams(this, init);

test/parallel/test-whatwg-url-searchparams-constructor.js

+70-10
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,30 @@ assert.strictEqual(params + '', 'a=b');
1616
params = new URLSearchParams(params);
1717
assert.strictEqual(params + '', 'a=b');
1818

19-
// URLSearchParams constructor, empty.
19+
// URLSearchParams constructor, no arguments
20+
params = new URLSearchParams();
21+
assert.strictEqual(params.toString(), '');
22+
2023
assert.throws(() => URLSearchParams(), TypeError,
2124
'Calling \'URLSearchParams\' without \'new\' should throw.');
22-
// assert.throws(() => new URLSearchParams(DOMException.prototype), TypeError);
23-
assert.throws(() => {
24-
new URLSearchParams({
25-
toString() { throw new TypeError('Illegal invocation'); }
26-
});
27-
}, TypeError);
25+
26+
// URLSearchParams constructor, undefined and null as argument
27+
params = new URLSearchParams(undefined);
28+
assert.strictEqual(params.toString(), '');
29+
params = new URLSearchParams(null);
30+
assert.strictEqual(params.toString(), '');
31+
32+
// URLSearchParams constructor, empty string as argument
2833
params = new URLSearchParams('');
29-
assert.notStrictEqual(params, null, 'constructor returned non-null value.');
34+
// eslint-disable-next-line no-restricted-properties
35+
assert.notEqual(params, null, 'constructor returned non-null value.');
3036
// eslint-disable-next-line no-proto
3137
assert.strictEqual(params.__proto__, URLSearchParams.prototype,
3238
'expected URLSearchParams.prototype as prototype.');
39+
40+
// URLSearchParams constructor, {} as argument
3341
params = new URLSearchParams({});
34-
// assert.strictEqual(params + '', '%5Bobject+Object%5D=');
35-
assert.strictEqual(params + '', '%5Bobject%20Object%5D=');
42+
assert.strictEqual(params + '', '');
3643

3744
// URLSearchParams constructor, string.
3845
params = new URLSearchParams('a=b');
@@ -128,3 +135,56 @@ params = new URLSearchParams('a=b%f0%9f%92%a9c');
128135
assert.strictEqual(params.get('a'), 'b\uD83D\uDCA9c');
129136
params = new URLSearchParams('a%f0%9f%92%a9b=c');
130137
assert.strictEqual(params.get('a\uD83D\uDCA9b'), 'c');
138+
139+
// Constructor with sequence of sequences of strings
140+
params = new URLSearchParams([]);
141+
// eslint-disable-next-line no-restricted-properties
142+
assert.notEqual(params, null, 'constructor returned non-null value.');
143+
params = new URLSearchParams([['a', 'b'], ['c', 'd']]);
144+
assert.strictEqual(params.get('a'), 'b');
145+
assert.strictEqual(params.get('c'), 'd');
146+
assert.throws(() => new URLSearchParams([[1]]),
147+
/^TypeError: Each query pair must be a name\/value tuple$/);
148+
assert.throws(() => new URLSearchParams([[1, 2, 3]]),
149+
/^TypeError: Each query pair must be a name\/value tuple$/);
150+
151+
[
152+
// Further confirmation needed
153+
// https://github.com/w3c/web-platform-tests/pull/4523#discussion_r98337513
154+
// {
155+
// input: {'+': '%C2'},
156+
// output: [[' ', '\uFFFD']],
157+
// name: 'object with +'
158+
// },
159+
{
160+
input: {c: 'x', a: '?'},
161+
output: [['c', 'x'], ['a', '?']],
162+
name: 'object with two keys'
163+
},
164+
{
165+
input: [['c', 'x'], ['a', '?']],
166+
output: [['c', 'x'], ['a', '?']],
167+
name: 'array with two keys'
168+
}
169+
].forEach((val) => {
170+
const params = new URLSearchParams(val.input);
171+
assert.deepStrictEqual(Array.from(params), val.output,
172+
`Construct with ${val.name}`);
173+
});
174+
175+
// Custom [Symbol.iterator]
176+
params = new URLSearchParams();
177+
params[Symbol.iterator] = function *() {
178+
yield ['a', 'b'];
179+
};
180+
const params2 = new URLSearchParams(params);
181+
assert.strictEqual(params2.get('a'), 'b');
182+
183+
assert.throws(() => new URLSearchParams({ [Symbol.iterator]: 42 }),
184+
/^TypeError: Query pairs must be iterable$/);
185+
assert.throws(() => new URLSearchParams([{}]),
186+
/^TypeError: Each query pair must be iterable$/);
187+
assert.throws(() => new URLSearchParams(['a']),
188+
/^TypeError: Each query pair must be iterable$/);
189+
assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]),
190+
/^TypeError: Each query pair must be iterable$/);

0 commit comments

Comments
 (0)