diff --git a/lib/internal/url.js b/lib/internal/url.js index 113746a24d..da4eb07a81 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -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> + // 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 + 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); diff --git a/test/parallel/test-whatwg-url-searchparams-constructor.js b/test/parallel/test-whatwg-url-searchparams-constructor.js index 9b7ca1e4e9..cb6ee0c9c0 100644 --- a/test/parallel/test-whatwg-url-searchparams-constructor.js +++ b/test/parallel/test-whatwg-url-searchparams-constructor.js @@ -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'); @@ -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$/);