Browse Source

url: make WHATWG URL properties spec compliant

* Set exposed attributes of the interface enumerable
  and configurable, as required by the spec.
  See: https://heycam.github.io/webidl/#es-attributes
* Make sure `URL#searchParams` returns `[[SameObject]]`
* Add the missing `URL#href` setter
* Reorder the properties to match
  https://url.spec.whatwg.org/#api
* Add tests for the ECMAScript property attributes

PR-URL: https://github.com/nodejs/node/pull/10408
Fixes: https://github.com/nodejs/node/issues/10376
Reviewed-By: James M Snell <jasnell@gmail.com>
v6
Joyee Cheung 8 years ago
committed by James M Snell
parent
commit
508d97628b
  1. 716
      lib/internal/url.js
  2. 127
      test/parallel/test-whatwg-url-properties.js

716
lib/internal/url.js

@ -85,34 +85,39 @@ class TupleOrigin {
}
}
// Reused by URL constructor and URL#href setter.
function parse(url, input, base) {
input = String(input);
const base_context = base ? base[context] : undefined;
url[context] = new StorageObject();
binding.parse(input.trim(), -1, base_context, undefined,
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
throw new TypeError('Invalid URL');
url[context].flags = flags;
url[context].scheme = protocol;
url[context].username = username;
url[context].password = password;
url[context].port = port;
url[context].path = path;
url[context].query = query;
url[context].fragment = fragment;
url[context].host = host;
if (url[searchParams]) { // invoked from href setter
initSearchParams(url[searchParams], query);
} else {
url[searchParams] = new URLSearchParams(query);
}
url[searchParams][context] = url;
});
}
class URL {
constructor(input, base) {
if (base !== undefined && !(base instanceof URL))
base = new URL(String(base));
input = String(input);
const base_context = base ? base[context] : undefined;
this[context] = new StorageObject();
binding.parse(input.trim(), -1, base_context, undefined,
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
throw new TypeError('Invalid URL');
this[context].flags = flags;
this[context].scheme = protocol;
this[context].username = username;
this[context].password = password;
this[context].port = port;
this[context].path = path;
this[context].query = query;
this[context].fragment = fragment;
this[context].host = host;
this[searchParams] = new URLSearchParams(query);
this[searchParams][context] = this;
});
}
get origin() {
return URL.originFor(this).toString(true);
parse(this, input, base);
}
get [special]() {
@ -123,309 +128,6 @@ class URL {
return (this[context].flags & binding.URL_FLAGS_CANNOT_BE_BASE) != 0;
}
get protocol() {
return this[context].scheme;
}
get searchParams() {
return this[searchParams];
}
set protocol(scheme) {
scheme = String(scheme);
if (scheme.length === 0)
return;
binding.parse(scheme,
binding.kSchemeStart,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) != 0;
if ((this[special] && !newIsSpecial) ||
(!this[special] && newIsSpecial) ||
(newIsSpecial && !this[special] &&
this[context].host === undefined)) {
return;
}
if (newIsSpecial) {
this[context].flags |= binding.URL_FLAGS_SPECIAL;
} else {
this[context].flags &= ~binding.URL_FLAGS_SPECIAL;
}
if (protocol) {
this[context].scheme = protocol;
this[context].flags |= binding.URL_FLAGS_HAS_SCHEME;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_SCHEME;
}
});
}
get username() {
return this[context].username || '';
}
set username(username) {
username = String(username);
if (!this.hostname)
return;
if (!username) {
this[context].username = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_USERNAME;
return;
}
this[context].username = binding.encodeAuth(username);
this[context].flags |= binding.URL_FLAGS_HAS_USERNAME;
}
get password() {
return this[context].password || '';
}
set password(password) {
password = String(password);
if (!this.hostname)
return;
if (!password) {
this[context].password = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_PASSWORD;
return;
}
this[context].password = binding.encodeAuth(password);
this[context].flags |= binding.URL_FLAGS_HAS_PASSWORD;
}
get host() {
var ret = this[context].host || '';
if (this[context].port !== undefined)
ret += `:${this[context].port}`;
return ret;
}
set host(host) {
host = String(host);
if (this[cannotBeBase] ||
(this[special] && host.length === 0)) {
// Cannot set the host if cannot-be-base is set or
// scheme is special and host length is zero
return;
}
if (!host) {
this[context].host = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
return;
}
binding.parse(host, binding.kHost, null, this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (host) {
this[context].host = host;
this[context].flags |= binding.URL_FLAGS_HAS_HOST;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
}
if (port !== undefined)
this[context].port = port;
});
}
get hostname() {
return this[context].host || '';
}
set hostname(host) {
host = String(host);
if (this[cannotBeBase] ||
(this[special] && host.length === 0)) {
// Cannot set the host if cannot-be-base is set or
// scheme is special and host length is zero
return;
}
if (!host) {
this[context].host = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
return;
}
binding.parse(host,
binding.kHostname,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (host) {
this[context].host = host;
this[context].flags |= binding.URL_FLAGS_HAS_HOST;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
}
});
}
get port() {
const port = this[context].port;
return port === undefined ? '' : String(port);
}
set port(port) {
if (!this[context].host || this[cannotBeBase] || this.protocol === 'file:')
return;
port = String(port);
if (port === '') {
// Currently, if port number is empty, left unchanged.
// TODO(jasnell): This might be changing in the spec
return;
}
binding.parse(port, binding.kPort, null, this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
this[context].port = port;
});
}
get pathname() {
if (this[cannotBeBase])
return this[context].path[0];
return this[context].path !== undefined ?
`/${this[context].path.join('/')}` : '';
}
set pathname(path) {
if (this[cannotBeBase])
return;
path = String(path);
binding.parse(path,
binding.kPathStart,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (path) {
this[context].path = path;
this[context].flags |= binding.URL_FLAGS_HAS_PATH;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_PATH;
}
});
}
get search() {
return !this[context].query ? '' : `?${this[context].query}`;
}
set search(search) {
search = String(search);
if (search[0] === '?') search = search.slice(1);
if (!search) {
this[context].query = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
this[searchParams][searchParams] = {};
return;
}
this[context].query = '';
binding.parse(search,
binding.kQuery,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (query) {
this[context].query = query;
this[context].flags |= binding.URL_FLAGS_HAS_QUERY;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
}
});
this[searchParams][searchParams] = querystring.parse(search);
}
get hash() {
return !this[context].fragment ? '' : `#${this[context].fragment}`;
}
set hash(hash) {
hash = String(hash);
if (this.protocol === 'javascript:')
return;
if (!hash) {
this[context].fragment = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
return;
}
if (hash[0] === '#') hash = hash.slice(1);
this[context].fragment = '';
binding.parse(hash,
binding.kFragment,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (fragment) {
this[context].fragment = fragment;
this[context].flags |= binding.URL_FLAGS_HAS_FRAGMENT;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
}
});
}
get href() {
return this.toString();
}
toString(options) {
options = options || {};
const fragment =
options.fragment !== undefined ?
!!options.fragment : true;
const unicode = !!options.unicode;
var ret;
if (this.protocol)
ret = this.protocol;
if (this[context].host !== undefined) {
ret += '//';
const has_username = typeof this[context].username === 'string';
const has_password = typeof this[context].password === 'string';
if (has_username || has_password) {
if (has_username)
ret += this[context].username;
if (has_password)
ret += `:${this[context].password}`;
ret += '@';
}
if (unicode) {
ret += punycode.toUnicode(this.hostname);
if (this.port !== undefined)
ret += `:${this.port}`;
} else {
ret += this.host;
}
} else if (this[context].scheme === 'file:') {
ret += '//';
}
if (this.pathname)
ret += this.pathname;
if (typeof this[context].query === 'string')
ret += `?${this[context].query}`;
if (fragment & typeof this[context].fragment === 'string')
ret += `#${this[context].fragment}`;
return ret;
}
inspect(depth, opts) {
var ret = 'URL {\n';
ret += ` href: ${this.href}\n`;
@ -456,6 +158,353 @@ class URL {
}
}
Object.defineProperties(URL.prototype, {
toString: {
// https://heycam.github.io/webidl/#es-stringifier
writable: true,
enumerable: true,
configurable: true,
// eslint-disable-next-line func-name-matching
value: function toString(options) {
options = options || {};
const fragment =
options.fragment !== undefined ?
!!options.fragment : true;
const unicode = !!options.unicode;
var ret;
if (this.protocol)
ret = this.protocol;
if (this[context].host !== undefined) {
ret += '//';
const has_username = typeof this[context].username === 'string';
const has_password = typeof this[context].password === 'string';
if (has_username || has_password) {
if (has_username)
ret += this[context].username;
if (has_password)
ret += `:${this[context].password}`;
ret += '@';
}
if (unicode) {
ret += punycode.toUnicode(this.hostname);
if (this.port !== undefined)
ret += `:${this.port}`;
} else {
ret += this.host;
}
} else if (this[context].scheme === 'file:') {
ret += '//';
}
if (this.pathname)
ret += this.pathname;
if (typeof this[context].query === 'string')
ret += `?${this[context].query}`;
if (fragment & typeof this[context].fragment === 'string')
ret += `#${this[context].fragment}`;
return ret;
}
},
href: {
enumerable: true,
configurable: true,
get() {
return this.toString();
},
set(input) {
parse(this, input);
}
},
origin: { // readonly
enumerable: true,
configurable: true,
get() {
return originFor(this).toString(true);
}
},
protocol: {
enumerable: true,
configurable: true,
get() {
return this[context].scheme;
},
set(scheme) {
scheme = String(scheme);
if (scheme.length === 0)
return;
binding.parse(scheme,
binding.kSchemeStart,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) != 0;
if ((this[special] && !newIsSpecial) ||
(!this[special] && newIsSpecial) ||
(newIsSpecial && !this[special] &&
this[context].host === undefined)) {
return;
}
if (newIsSpecial) {
this[context].flags |= binding.URL_FLAGS_SPECIAL;
} else {
this[context].flags &= ~binding.URL_FLAGS_SPECIAL;
}
if (protocol) {
this[context].scheme = protocol;
this[context].flags |= binding.URL_FLAGS_HAS_SCHEME;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_SCHEME;
}
});
}
},
username: {
enumerable: true,
configurable: true,
get() {
return this[context].username || '';
},
set(username) {
username = String(username);
if (!this.hostname)
return;
if (!username) {
this[context].username = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_USERNAME;
return;
}
this[context].username = binding.encodeAuth(username);
this[context].flags |= binding.URL_FLAGS_HAS_USERNAME;
}
},
password: {
enumerable: true,
configurable: true,
get() {
return this[context].password || '';
},
set(password) {
password = String(password);
if (!this.hostname)
return;
if (!password) {
this[context].password = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_PASSWORD;
return;
}
this[context].password = binding.encodeAuth(password);
this[context].flags |= binding.URL_FLAGS_HAS_PASSWORD;
}
},
host: {
enumerable: true,
configurable: true,
get() {
var ret = this[context].host || '';
if (this[context].port !== undefined)
ret += `:${this[context].port}`;
return ret;
},
set(host) {
host = String(host);
if (this[cannotBeBase] ||
(this[special] && host.length === 0)) {
// Cannot set the host if cannot-be-base is set or
// scheme is special and host length is zero
return;
}
if (!host) {
this[context].host = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
return;
}
binding.parse(host, binding.kHost, null, this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (host) {
this[context].host = host;
this[context].flags |= binding.URL_FLAGS_HAS_HOST;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
}
if (port !== undefined)
this[context].port = port;
});
}
},
hostname: {
enumerable: true,
configurable: true,
get() {
return this[context].host || '';
},
set(host) {
host = String(host);
if (this[cannotBeBase] ||
(this[special] && host.length === 0)) {
// Cannot set the host if cannot-be-base is set or
// scheme is special and host length is zero
return;
}
if (!host) {
this[context].host = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
return;
}
binding.parse(host,
binding.kHostname,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (host) {
this[context].host = host;
this[context].flags |= binding.URL_FLAGS_HAS_HOST;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
}
});
}
},
port: {
enumerable: true,
configurable: true,
get() {
const port = this[context].port;
return port === undefined ? '' : String(port);
},
set(port) {
if (!this[context].host || this[cannotBeBase] ||
this.protocol === 'file:')
return;
port = String(port);
if (port === '') {
// Currently, if port number is empty, left unchanged.
// TODO(jasnell): This might be changing in the spec
return;
}
binding.parse(port, binding.kPort, null, this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
this[context].port = port;
});
}
},
pathname: {
enumerable: true,
configurable: true,
get() {
if (this[cannotBeBase])
return this[context].path[0];
return this[context].path !== undefined ?
`/${this[context].path.join('/')}` : '';
},
set(path) {
if (this[cannotBeBase])
return;
path = String(path);
binding.parse(path,
binding.kPathStart,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (path) {
this[context].path = path;
this[context].flags |= binding.URL_FLAGS_HAS_PATH;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_PATH;
}
});
}
},
search: {
enumerable: true,
configurable: true,
get() {
return !this[context].query ? '' : `?${this[context].query}`;
},
set(search) {
search = String(search);
if (search[0] === '?') search = search.slice(1);
if (!search) {
this[context].query = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
this[searchParams][searchParams] = {};
return;
}
this[context].query = '';
binding.parse(search,
binding.kQuery,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (query) {
this[context].query = query;
this[context].flags |= binding.URL_FLAGS_HAS_QUERY;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
}
});
this[searchParams][searchParams] = querystring.parse(search);
}
},
searchParams: { // readonly
enumerable: true,
configurable: true,
get() {
return this[searchParams];
}
},
hash: {
enumerable: true,
configurable: true,
get() {
return !this[context].fragment ? '' : `#${this[context].fragment}`;
},
set(hash) {
hash = String(hash);
if (this.protocol === 'javascript:')
return;
if (!hash) {
this[context].fragment = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
return;
}
if (hash[0] === '#') hash = hash.slice(1);
this[context].fragment = '';
binding.parse(hash,
binding.kFragment,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (fragment) {
this[context].fragment = fragment;
this[context].flags |= binding.URL_FLAGS_HAS_FRAGMENT;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
}
});
}
}
});
var hexTable = new Array(256);
for (var i = 0; i < 256; ++i)
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
@ -546,6 +595,12 @@ function getSearchParamPairs(target) {
return values;
}
// Reused by the URL parse function invoked by
// the href setter, and the URLSearchParams constructor
function initSearchParams(url, init) {
url[searchParams] = querystring.parse(init);
}
class URLSearchParams {
constructor(init = '') {
if (init instanceof URLSearchParams) {
@ -554,7 +609,7 @@ class URLSearchParams {
} else {
init = String(init);
if (init[0] === '?') init = init.slice(1);
this[searchParams] = querystring.parse(init);
initSearchParams(this, init);
}
// "associated url object"
@ -790,7 +845,7 @@ Object.defineProperty(URLSearchParamsIteratorPrototype, Symbol.toStringTag, {
configurable: true
});
URL.originFor = function(url, base) {
function originFor(url, base) {
if (!(url instanceof URL))
url = new URL(url, base);
var origin;
@ -822,8 +877,9 @@ URL.originFor = function(url, base) {
origin = new OpaqueOrigin();
}
return origin;
};
}
URL.originFor = originFor;
URL.domainToASCII = function(domain) {
return binding.domainToASCII(String(domain));
};

127
test/parallel/test-whatwg-url-properties.js

@ -0,0 +1,127 @@
'use strict';
require('../common');
const URL = require('url').URL;
const assert = require('assert');
const url = new URL('http://user:pass@foo.bar.com:21/aaa/zzz?l=24#test');
const oldParams = url.searchParams; // for test of [SameObject]
// To retrieve enumerable but not necessarily own properties,
// we need to use the for-in loop.
const props = [];
for (const prop in url) {
props.push(prop);
}
// See: https://url.spec.whatwg.org/#api
// https://heycam.github.io/webidl/#es-attributes
// https://heycam.github.io/webidl/#es-stringifier
const expected = ['toString',
'href', 'origin', 'protocol',
'username', 'password', 'host', 'hostname', 'port',
'pathname', 'search', 'searchParams', 'hash'];
assert.deepStrictEqual(props, expected);
// href is writable (not readonly) and is stringifier
assert.strictEqual(url.toString(), url.href);
url.href = 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test';
assert.strictEqual(url.href,
'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
assert.strictEqual(url.toString(), url.href);
// Return true because it's configurable, but because the properties
// are defined on the prototype per the spec, the deletion has no effect
assert.strictEqual((delete url.href), true);
assert.strictEqual(url.href,
'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
assert.strictEqual(url.searchParams, oldParams); // [SameObject]
// searchParams is readonly. Under strict mode setting a
// non-writable property should throw.
// Note: this error message is subject to change in V8 updates
assert.throws(() => url.origin = 'http://foo.bar.com:22',
new RegExp('TypeError: Cannot set property origin of' +
' \\[object Object\\] which has only a getter'));
assert.strictEqual(url.origin, 'http://foo.bar.com:21');
assert.strictEqual(url.toString(),
'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
assert.strictEqual((delete url.origin), true);
assert.strictEqual(url.origin, 'http://foo.bar.com:21');
// The following properties should be writable (not readonly)
url.protocol = 'https:';
assert.strictEqual(url.protocol, 'https:');
assert.strictEqual(url.toString(),
'https://user:pass@foo.bar.com:21/aaa/zzz?l=25#test');
assert.strictEqual((delete url.protocol), true);
assert.strictEqual(url.protocol, 'https:');
url.username = 'user2';
assert.strictEqual(url.username, 'user2');
assert.strictEqual(url.toString(),
'https://user2:pass@foo.bar.com:21/aaa/zzz?l=25#test');
assert.strictEqual((delete url.username), true);
assert.strictEqual(url.username, 'user2');
url.password = 'pass2';
assert.strictEqual(url.password, 'pass2');
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.com:21/aaa/zzz?l=25#test');
assert.strictEqual((delete url.password), true);
assert.strictEqual(url.password, 'pass2');
url.host = 'foo.bar.net:22';
assert.strictEqual(url.host, 'foo.bar.net:22');
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.net:22/aaa/zzz?l=25#test');
assert.strictEqual((delete url.host), true);
assert.strictEqual(url.host, 'foo.bar.net:22');
url.hostname = 'foo.bar.org';
assert.strictEqual(url.hostname, 'foo.bar.org');
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.org:22/aaa/zzz?l=25#test');
assert.strictEqual((delete url.hostname), true);
assert.strictEqual(url.hostname, 'foo.bar.org');
url.port = '23';
assert.strictEqual(url.port, '23');
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.org:23/aaa/zzz?l=25#test');
assert.strictEqual((delete url.port), true);
assert.strictEqual(url.port, '23');
url.pathname = '/aaa/bbb';
assert.strictEqual(url.pathname, '/aaa/bbb');
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.org:23/aaa/bbb?l=25#test');
assert.strictEqual((delete url.pathname), true);
assert.strictEqual(url.pathname, '/aaa/bbb');
url.search = '?k=99';
assert.strictEqual(url.search, '?k=99');
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#test');
assert.strictEqual((delete url.search), true);
assert.strictEqual(url.search, '?k=99');
url.hash = '#abcd';
assert.strictEqual(url.hash, '#abcd');
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#abcd');
assert.strictEqual((delete url.hash), true);
assert.strictEqual(url.hash, '#abcd');
// searchParams is readonly. Under strict mode setting a
// non-writable property should throw.
// Note: this error message is subject to change in V8 updates
assert.throws(() => url.searchParams = '?k=88',
new RegExp('TypeError: Cannot set property searchParams of' +
' \\[object Object\\] which has only a getter'));
assert.strictEqual(url.searchParams, oldParams);
assert.strictEqual(url.toString(),
'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#abcd');
assert.strictEqual((delete url.searchParams), true);
assert.strictEqual(url.searchParams, oldParams);
Loading…
Cancel
Save