Skip to content

Commit 2300a51

Browse files
committed
url: extend URLSearchParams constructor
Fixes: nodejs#10635 Refs: whatwg/url#175 Refs: web-platform-tests/wpt#4523
1 parent 18d4ee9 commit 2300a51

File tree

2 files changed

+110
-13
lines changed

2 files changed

+110
-13
lines changed

lib/internal/url.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -649,10 +649,49 @@ function getObjectFromParams(array) {
649649

650650
class URLSearchParams {
651651
constructor(init = '') {
652-
if (init instanceof URLSearchParams) {
653-
const childParams = init[searchParams];
654-
this[searchParams] = childParams.slice();
652+
if (init === null || init === undefined) {
653+
// record<USVString, USVString>
654+
this[searchParams] = [];
655+
} else if (typeof init === 'object') {
656+
const method = init[Symbol.iterator];
657+
if (method === this[Symbol.iterator]) {
658+
// While the spec does not have this branch, we can use it as a
659+
// shortcut to avoid having to go through the costly generic iterator.
660+
const childParams = init[searchParams];
661+
this[searchParams] = childParams.slice();
662+
} else if (method !== null && method !== undefined) {
663+
if (typeof method !== 'function') {
664+
throw new TypeError('Query pairs must be iterable');
665+
}
666+
667+
// sequence<sequence<USVString>>
668+
// Note: per spec we have to first exhaust the lists then process them
669+
const pairs = [];
670+
for (const pair of init) {
671+
if (typeof pair !== 'object' ||
672+
typeof pair[Symbol.iterator] !== 'function') {
673+
throw new TypeError('Each query pair must be iterable');
674+
}
675+
pairs.push(Array.from(pair));
676+
}
677+
678+
this[searchParams] = [];
679+
for (const pair of pairs) {
680+
if (pair.length !== 2) {
681+
throw new TypeError('Each query pair must be a name/value tuple');
682+
}
683+
this[searchParams].push(String(pair[0]), String(pair[1]));
684+
}
685+
} else {
686+
// record<USVString, USVString>
687+
this[searchParams] = [];
688+
for (const key of Object.keys(init)) {
689+
const value = String(init[key]);
690+
this[searchParams].push(key, value);
691+
}
692+
}
655693
} else {
694+
// USVString
656695
init = String(init);
657696
if (init[0] === '?') init = init.slice(1);
658697
initSearchParams(this, init);

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

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,24 @@ 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, empty string as argument
2827
params = new URLSearchParams('');
29-
assert.notStrictEqual(params, null, 'constructor returned non-null value.');
28+
// eslint-disable-next-line no-restricted-properties
29+
assert.notEqual(params, null, 'constructor returned non-null value.');
3030
// eslint-disable-next-line no-proto
3131
assert.strictEqual(params.__proto__, URLSearchParams.prototype,
3232
'expected URLSearchParams.prototype as prototype.');
33+
34+
// URLSearchParams constructor, {} as argument
3335
params = new URLSearchParams({});
34-
// assert.strictEqual(params + '', '%5Bobject+Object%5D=');
35-
assert.strictEqual(params + '', '%5Bobject%20Object%5D=');
36+
assert.strictEqual(params + '', '');
3637

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

0 commit comments

Comments
 (0)