Skip to content

Commit

Permalink
url: extend URLSearchParams constructor
Browse files Browse the repository at this point in the history
PR-URL: nodejs#11060
Fixes: nodejs#10635
Ref: whatwg/url#175
Ref: web-platform-tests/wpt#4523
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
  • Loading branch information
TimothyGu authored and italoacasas committed Feb 2, 2017
1 parent 598d35c commit 9bf29db
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 14 deletions.
50 changes: 46 additions & 4 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,11 +677,53 @@ function defineIDLClass(proto, classStr, obj) {
}

class URLSearchParams {
constructor(init = '') {
if (init instanceof URLSearchParams) {
const childParams = init[searchParams];
this[searchParams] = childParams.slice();
// URL Standard says the default value is '', but as undefined and '' have
// the same result, undefined is used to prevent unnecessary parsing.
// Default parameter is necessary to keep URLSearchParams.length === 0 in
// accordance with Web IDL spec.
constructor(init = undefined) {
if (init === null || init === undefined) {
this[searchParams] = [];
} else if (typeof init === 'object') {
const method = init[Symbol.iterator];
if (method === this[Symbol.iterator]) {
// While the spec does not have this branch, we can use it as a
// shortcut to avoid having to go through the costly generic iterator.
const childParams = init[searchParams];
this[searchParams] = childParams.slice();
} else if (method !== null && method !== undefined) {
if (typeof method !== 'function') {
throw new TypeError('Query pairs must be iterable');
}

// sequence<sequence<USVString>>
// Note: per spec we have to first exhaust the lists then process them
const pairs = [];
for (const pair of init) {
if (typeof pair !== 'object' ||
typeof pair[Symbol.iterator] !== 'function') {
throw new TypeError('Each query pair must be iterable');
}
pairs.push(Array.from(pair));
}

this[searchParams] = [];
for (const pair of pairs) {
if (pair.length !== 2) {
throw new TypeError('Each query pair must be a name/value tuple');
}
this[searchParams].push(String(pair[0]), String(pair[1]));
}
} else {
// record<USVString, USVString>
this[searchParams] = [];
for (const key of Object.keys(init)) {
const value = String(init[key]);
this[searchParams].push(key, value);
}
}
} else {
// USVString
init = String(init);
if (init[0] === '?') init = init.slice(1);
initSearchParams(this, init);
Expand Down
80 changes: 70 additions & 10 deletions test/parallel/test-whatwg-url-searchparams-constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,30 @@ assert.strictEqual(params + '', 'a=b');
params = new URLSearchParams(params);
assert.strictEqual(params + '', 'a=b');

// URLSearchParams constructor, empty.
// URLSearchParams constructor, no arguments
params = new URLSearchParams();
assert.strictEqual(params.toString(), '');

assert.throws(() => URLSearchParams(), TypeError,
'Calling \'URLSearchParams\' without \'new\' should throw.');
// assert.throws(() => new URLSearchParams(DOMException.prototype), TypeError);
assert.throws(() => {
new URLSearchParams({
toString() { throw new TypeError('Illegal invocation'); }
});
}, TypeError);

// URLSearchParams constructor, undefined and null as argument
params = new URLSearchParams(undefined);
assert.strictEqual(params.toString(), '');
params = new URLSearchParams(null);
assert.strictEqual(params.toString(), '');

// URLSearchParams constructor, empty string as argument
params = new URLSearchParams('');
assert.notStrictEqual(params, null, 'constructor returned non-null value.');
// eslint-disable-next-line no-restricted-properties
assert.notEqual(params, null, 'constructor returned non-null value.');
// eslint-disable-next-line no-proto
assert.strictEqual(params.__proto__, URLSearchParams.prototype,
'expected URLSearchParams.prototype as prototype.');

// URLSearchParams constructor, {} as argument
params = new URLSearchParams({});
// assert.strictEqual(params + '', '%5Bobject+Object%5D=');
assert.strictEqual(params + '', '%5Bobject%20Object%5D=');
assert.strictEqual(params + '', '');

// URLSearchParams constructor, string.
params = new URLSearchParams('a=b');
Expand Down Expand Up @@ -128,3 +135,56 @@ params = new URLSearchParams('a=b%f0%9f%92%a9c');
assert.strictEqual(params.get('a'), 'b\uD83D\uDCA9c');
params = new URLSearchParams('a%f0%9f%92%a9b=c');
assert.strictEqual(params.get('a\uD83D\uDCA9b'), 'c');

// Constructor with sequence of sequences of strings
params = new URLSearchParams([]);
// eslint-disable-next-line no-restricted-properties
assert.notEqual(params, null, 'constructor returned non-null value.');
params = new URLSearchParams([['a', 'b'], ['c', 'd']]);
assert.strictEqual(params.get('a'), 'b');
assert.strictEqual(params.get('c'), 'd');
assert.throws(() => new URLSearchParams([[1]]),
/^TypeError: Each query pair must be a name\/value tuple$/);
assert.throws(() => new URLSearchParams([[1, 2, 3]]),
/^TypeError: Each query pair must be a name\/value tuple$/);

[
// Further confirmation needed
// https://github.com/w3c/web-platform-tests/pull/4523#discussion_r98337513
// {
// input: {'+': '%C2'},
// output: [[' ', '\uFFFD']],
// name: 'object with +'
// },
{
input: {c: 'x', a: '?'},
output: [['c', 'x'], ['a', '?']],
name: 'object with two keys'
},
{
input: [['c', 'x'], ['a', '?']],
output: [['c', 'x'], ['a', '?']],
name: 'array with two keys'
}
].forEach((val) => {
const params = new URLSearchParams(val.input);
assert.deepStrictEqual(Array.from(params), val.output,
`Construct with ${val.name}`);
});

// Custom [Symbol.iterator]
params = new URLSearchParams();
params[Symbol.iterator] = function *() {
yield ['a', 'b'];
};
const params2 = new URLSearchParams(params);
assert.strictEqual(params2.get('a'), 'b');

assert.throws(() => new URLSearchParams({ [Symbol.iterator]: 42 }),
/^TypeError: Query pairs must be iterable$/);
assert.throws(() => new URLSearchParams([{}]),
/^TypeError: Each query pair must be iterable$/);
assert.throws(() => new URLSearchParams(['a']),
/^TypeError: Each query pair must be iterable$/);
assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]),
/^TypeError: Each query pair must be iterable$/);

0 comments on commit 9bf29db

Please sign in to comment.