diff --git a/lib/internal/url.js b/lib/internal/url.js index ba54aa8c233354..9d7448be8f6a20 100644 --- a/lib/internal/url.js +++ b/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)); }; diff --git a/test/parallel/test-whatwg-url-properties.js b/test/parallel/test-whatwg-url-properties.js new file mode 100644 index 00000000000000..60cf581ad8da4d --- /dev/null +++ b/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);