├── urlshim.d.ts ├── .gitignore ├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── test ├── utils │ └── index.js ├── mdn.js ├── polyfill-io.js └── index.js ├── license ├── package.json ├── readme.md └── src └── index.js /urlshim.d.ts: -------------------------------------------------------------------------------- 1 | import { URL, URLSearchParams } from 'url'; 2 | export { URL, URLSearchParams }; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.lock 4 | *.log 5 | dist 6 | 7 | /.jestcache 8 | /coverage 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [8, 10, 12] 12 | steps: 13 | - uses: actions/checkout@master 14 | with: 15 | fetch-depth: 1 16 | 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.nodejs }} 20 | 21 | - name: Install 22 | run: npm install 23 | 24 | - name: Test 25 | run: npm test 26 | 27 | - name: Report 28 | if: matrix.nodejs >= 12 29 | run: | 30 | bash <(curl -s https://codecov.io/bash) 31 | env: 32 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import * as lib from '../../src'; 3 | 4 | export function parse(obj) { 5 | let k, out={}; 6 | for (k in obj) { 7 | if (k !== 'searchParams' && typeof obj[k] !== 'function') { 8 | out[k] = obj[k]; 9 | } 10 | } 11 | return out; 12 | } 13 | 14 | export function compare(ctor, ...args) { 15 | const local = new lib[ctor](...args); 16 | const native = new url[ctor](...args); 17 | return [local, native].map(ctor == 'URL' ? parse : String); 18 | } 19 | 20 | export function toErrors(ctor, ...args) { 21 | let local, native; 22 | 23 | try { 24 | new lib[ctor](...args); 25 | } catch (err) { 26 | local = { 27 | TypeError: err.name.includes('TypeError'), 28 | message: err.message, 29 | code: err.code, 30 | }; 31 | } 32 | 33 | try { 34 | new url[ctor](...args); 35 | } catch (err) { 36 | native = { 37 | TypeError: err.name.includes('TypeError'), 38 | message: err.message, 39 | code: err.code, 40 | }; 41 | } 42 | 43 | return [local, native]; 44 | } 45 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/mdn.js: -------------------------------------------------------------------------------- 1 | import { URL } from '../src/index.js'; 2 | 3 | /** 4 | * @see https://github.com/jsdom/whatwg-url/blob/master/test/mdn.js 5 | */ 6 | 7 | it('should pass all MDN examples', () => { 8 | const a = new URL('/', 'https://developer.mozilla.org'); 9 | expect(a.href).toBe('https://developer.mozilla.org/'); 10 | 11 | const b = new URL('https://developer.mozilla.org'); 12 | expect(b.href).toBe('https://developer.mozilla.org/'); 13 | 14 | const c = new URL('en-US/docs', b); 15 | expect(c.href).toBe('https://developer.mozilla.org/en-US/docs'); 16 | 17 | const d = new URL('/en-US/docs', b); 18 | expect(d.href).toBe('https://developer.mozilla.org/en-US/docs'); 19 | 20 | const f = new URL('/en-US/docs', d); 21 | expect(f.href).toBe('https://developer.mozilla.org/en-US/docs'); 22 | 23 | const g = new URL('/en-US/docs', 'https://developer.mozilla.org/fr-FR/toto'); 24 | expect(g.href).toBe('https://developer.mozilla.org/en-US/docs'); 25 | 26 | const h = new URL('/en-US/docs', a); 27 | expect(h.href).toBe('https://developer.mozilla.org/en-US/docs'); 28 | 29 | expect(() => new URL('/en-US/docs', '')).toThrow('Invalid URL:'); 30 | expect(() => new URL('/', 'https://')).toThrow('Invalid URL:'); 31 | expect(() => new URL('/en-US/docs')).toThrow('Invalid URL:'); 32 | 33 | const k = new URL('http://www.example.com', 'https://developers.mozilla.com'); 34 | expect(k.href).toBe('http://www.example.com/'); 35 | 36 | const l = new URL('http://www.example.com', b); 37 | expect(l.href).toBe("http://www.example.com/"); 38 | }); 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-shim", 3 | "version": "1.0.1", 4 | "repository": "lukeed/url-shim", 5 | "description": "A 1.53kB browser polyfill for the Node.js `URL` and `URLSearchParams` classes", 6 | "module": "dist/urlshim.mjs", 7 | "unpkg": "dist/urlshim.min.js", 8 | "main": "dist/urlshim.js", 9 | "types": "urlshim.d.ts", 10 | "umd:name": "urlshim", 11 | "license": "MIT", 12 | "author": { 13 | "name": "Luke Edwards", 14 | "email": "luke.edwards05@gmail.com", 15 | "url": "https://lukeed.com" 16 | }, 17 | "files": [ 18 | "*.d.ts", 19 | "dist" 20 | ], 21 | "engines": { 22 | "node": ">= 6" 23 | }, 24 | "scripts": { 25 | "build": "bundt", 26 | "pretest": "npm run build", 27 | "test": "jest --coverage" 28 | }, 29 | "keywords": [ 30 | "nodejs", 31 | "polyfill", 32 | "shim", 33 | "url", 34 | "urlsearchparams" 35 | ], 36 | "devDependencies": { 37 | "@babel/core": "7.7.5", 38 | "@babel/plugin-transform-modules-commonjs": "7.7.5", 39 | "babel-jest": "24.9.0", 40 | "bundt": "0.4.0", 41 | "jest": "24.9.0", 42 | "jest-puppeteer": "4.3.0", 43 | "prettier": "1.19.1", 44 | "puppeteer": "2.0.0" 45 | }, 46 | "jest": { 47 | "cacheDirectory": "/.jestcache", 48 | "coverageDirectory": "/coverage/", 49 | "collectCoverageFrom": [ 50 | "src/**" 51 | ], 52 | "testMatch": [ 53 | "/test/*" 54 | ], 55 | "transform": { 56 | "\\.js$": "babel-jest" 57 | } 58 | }, 59 | "babel": { 60 | "plugins": [ 61 | "@babel/plugin-transform-modules-commonjs" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # url-shim [![codecov](https://badgen.now.sh/codecov/c/github/lukeed/url-shim)](https://codecov.io/gh/lukeed/url-shim) 2 | 3 | > A 1.53kB browser polyfill for the Node.js `URL` and `URLSearchParams` classes. 4 | 5 | **Why?** 6 | 7 | * All browser implementations *are not* 100% identical to the Node.js implementation.
8 | _For example, browsers have issue with custom protocols, which affects the `origin` and `pathname` parsing._ 9 | 10 | * Most polyfills match the browser implementations.
11 | _But what if you have a "universal app" and want to guarantee client/server uniformity?_ 12 | 13 | * Most polyfills immediately (albeit, conditionally) mutate global scope.
14 | _You can't declaratively import their implementations for standalone usage._ 15 | 16 | > **Note:** The only other library that satisfies these requirements is [`whatwg-url`](https://github.com/jsdom/whatwg-url), but it [weighs **87.6 kB (gzip)**](https://bundlephobia.com/result?p=whatwg-url@7.1.0)! 17 | 18 | This module is available in three formats: 19 | 20 | * **ES Module**: `dist/urlshim.mjs` 21 | * **CommonJS**: `dist/urlshim.js` 22 | * **UMD**: `dist/urlshim.min.js` 23 | 24 | 25 | ## Install 26 | 27 | ``` 28 | $ npm install --save url-shim 29 | ``` 30 | 31 | 32 | ## Usage 33 | 34 | ```js 35 | import { URL, URLSearchParams } from 'url-shim'; 36 | 37 | // composition 38 | new URL('/foo', 'https://example.org/').href; 39 | //=> "https://example.org/foo" 40 | 41 | // unicode -> ASCII conversion 42 | new URL('https://測試').href; 43 | //=> "https://xn--g6w251d/" 44 | 45 | // custom protocols w/ path 46 | new URL('webpack:///src/bundle.js'); 47 | //=> { protocol: "webpack:", pathname: "/src/bundle.js", ... } 48 | 49 | // custom protocols w/ hostname 50 | new URL('git://github.com/lukeed/url-shim'); 51 | //=> { protocol: "git:", hostname: "github.com", pathname: "/lukeed/url-shim", ... } 52 | 53 | new URL('http://foobar.com/123?a=1&b=2').searchParams instanceof URLSearchParams; 54 | //=> true 55 | 56 | const params = new URLSearchParams('foo=bar&xyz=baz'); 57 | for (const [name, value] of params) { 58 | console.log(name, value); 59 | } 60 | // Prints: 61 | // foo bar 62 | // xyz baz 63 | ``` 64 | 65 | ## API 66 | 67 | ### URL(input, base?) 68 | > **Size (gzip):** `1.53 kB` 69 | 70 | See [Node.js documentation](https://nodejs.org/dist/latest-v12.x/docs/api/url.html#url_class_url) for info. 71 | 72 | > **Important:** Requires a browser environment because `document.createElement` is used for URL parsing. 73 | 74 | ### URLSearchParams(input?) 75 | > **Size (gzip):** `944 B` 76 | 77 | See [Node.js documentation](https://nodejs.org/dist/latest-v12.x/docs/api/url.html#url_class_urlsearchparams) for info. 78 | 79 | 80 | ## License 81 | 82 | MIT © [Luke Edwards](https://lukeed.com) 83 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | function toErr(msg, code, err) { 2 | err = new TypeError(msg); 3 | err.code = code; 4 | throw err; 5 | } 6 | 7 | function invalid(str) { 8 | toErr('Invalid URL: ' + str, 'ERR_INVALID_URL'); 9 | } 10 | 11 | function args(both, len, x, y) { 12 | x = 'The "name" '; 13 | y = 'argument'; 14 | 15 | if (both) { 16 | x += 'and "value" '; 17 | y += 's'; 18 | } 19 | 20 | if (len < ++both) { 21 | toErr(x + y + ' must be specified', 'ERR_MISSING_ARGS'); 22 | } 23 | } 24 | 25 | function toIter(arr, supported) { 26 | var val, j=0, iter = { 27 | next: function () { 28 | val = arr[j++]; 29 | return { 30 | value: val, 31 | done: j > arr.length 32 | } 33 | } 34 | }; 35 | 36 | if (supported) { 37 | iter[Symbol.iterator] = function () { 38 | return iter; 39 | }; 40 | } 41 | 42 | return iter; 43 | } 44 | 45 | export function URLSearchParams(init, ref) { 46 | var k, i, x, supp, tmp, $=this, list=[]; 47 | 48 | try { 49 | supp = !!Symbol.iterator; 50 | } catch (e) { 51 | supp = false; 52 | } 53 | 54 | if (init) { 55 | if (!!init.keys && !!init.getAll) { 56 | init.forEach(function (v, k) { 57 | toAppend(k, v); 58 | }); 59 | } else if (!!init.pop) { 60 | for (i=0; i < init.length; i++) { 61 | toAppend.apply(0, init[i]); 62 | } 63 | } else if (typeof init == 'object') { 64 | for (k in init) toSet(k, init[k]); 65 | } else if (typeof init == 'string') { 66 | if (init[0] == '?') init = init.substring(1); 67 | x = decodeURIComponent(init).split('&'); 68 | while (k = x.shift()) { 69 | i = k.indexOf('='); 70 | if (!~i) i = k.length; 71 | toAppend( 72 | k.substring(0, i), 73 | k.substring(++i) 74 | ); 75 | } 76 | } 77 | } 78 | 79 | function toSet(key, val) { 80 | args(1, arguments.length); 81 | val = String(val); 82 | x = false; // found? 83 | for (i=list.length; i--;) { 84 | tmp = list[i]; 85 | if (tmp[0] == key) { 86 | if (x) { 87 | list.splice(i, 1); 88 | } else { 89 | tmp[1] = val; 90 | x = true; 91 | } 92 | } 93 | } 94 | x || list.push([key, val]); 95 | cascade(); 96 | } 97 | 98 | function toAppend(key, val) { 99 | args(1, arguments.length); 100 | list.push([key, String(val)]); 101 | cascade(); 102 | } 103 | 104 | function toStr() { 105 | tmp = ''; 106 | for (i=0; i < list.length; i++) { 107 | if (tmp) tmp += '&'; 108 | tmp += encodeURIComponent(list[i][0]) + '=' + encodeURIComponent(list[i][1]); 109 | } 110 | return tmp.replace(/%20/g, '+'); 111 | } 112 | 113 | function cascade() { 114 | if (ref) ref.search = list.length ? ('?' + toStr().replace(/=$/, '')) : ''; 115 | } 116 | 117 | $.append = toAppend; 118 | $.delete = function (key) { 119 | args(0, arguments.length); 120 | for (i=list.length; i--;) { 121 | if (list[i][0] == key) list.splice(i, 1); 122 | } 123 | cascade(); 124 | }; 125 | $.entries = function () { 126 | return toIter(list, supp); 127 | }; 128 | $.forEach = function (fn) { 129 | if (typeof fn != 'function') { 130 | toErr('Callback must be a function', 'ERR_INVALID_CALLBACK'); 131 | } 132 | for (i=0; i < list.length; i++) { 133 | fn(list[i][1], list[i][0]); // (val,key) 134 | } 135 | }; 136 | $.get = function (key) { 137 | args(0, arguments.length); 138 | for (i=0; i < list.length; i++) { 139 | if (list[i][0] == key) return list[i][1]; 140 | } 141 | return null; 142 | }; 143 | $.getAll = function (key) { 144 | args(0, arguments.length); 145 | tmp = []; 146 | for (i=0; i < list.length; i++) { 147 | if (list[i][0] == key) { 148 | tmp.push(list[i][1]); 149 | } 150 | } 151 | return tmp; 152 | }; 153 | $.has = function (key) { 154 | args(0, arguments.length); 155 | for (i=0; i < list.length; i++) { 156 | if (list[i][0] == key) return true; 157 | } 158 | return false; 159 | }; 160 | $.keys = function () { 161 | tmp = []; 162 | for (i=0; i < list.length; i++) { 163 | tmp.push(list[i][0]); 164 | } 165 | return toIter(tmp, supp); 166 | }, 167 | $.set = toSet; 168 | $.sort = function () { 169 | x = []; tmp = []; 170 | for (i=0; i < list.length; x.push(list[i++][0])); 171 | for (x.sort(); k = x.shift();) { 172 | for (i=0; i < list.length; i++) { 173 | if (list[i][0] == k) { 174 | tmp.push(list.splice(i, 1).shift()); 175 | break; 176 | } 177 | } 178 | } 179 | list = tmp; 180 | cascade(); 181 | }; 182 | $.toString = toStr; 183 | $.values = function () { 184 | tmp = []; 185 | for (i=0; i < list.length; i++) { 186 | tmp.push(list[i][1]); 187 | } 188 | return toIter(tmp, supp); 189 | }; 190 | 191 | if (supp) { 192 | $[Symbol.iterator] = $.entries; 193 | } 194 | 195 | return $; 196 | } 197 | 198 | export function URL(url, base) { 199 | var tmp = document.createElement('a'); 200 | var link = document.createElement('a'); 201 | var input = document.createElement('input'); 202 | var segs, usp, $=this, rgx=/(blob|ftp|wss?|https?):/; 203 | 204 | input.type = 'url'; 205 | base = String(base || '').trim(); 206 | if ((input.value = base) && !input.checkValidity()) return invalid(base); 207 | 208 | url = String(url).trim(); 209 | input.value = url || 0; 210 | 211 | if (input.checkValidity()) { 212 | link.href = url; // full 213 | } else if (base) { 214 | link.href = base; 215 | if (url) { // non-empty string 216 | usp = url.match(/^\/+/); 217 | if (usp && usp[0].length == 2) { 218 | link.href = link.protocol + url; 219 | } else if (/[?#]/.test(url[0])) { 220 | link.href += url; 221 | } else if (url[0] == '/' || link.pathname == '/') { 222 | link.href = link.origin + '/' + url.replace(/^\/+/, ''); 223 | } else { 224 | segs = link.pathname.split('/'); 225 | base = url.replace(/^(\.\/)?/, '').split('../'); 226 | link.href = link.origin + segs.slice(0, Math.max(1, segs.length - base.length)).concat(base.pop()).join('/') 227 | } 228 | } 229 | } else { 230 | return invalid(url); 231 | } 232 | 233 | function proxy(key) { 234 | tmp.href=link.href; tmp.protocol='http:'; 235 | if (key == 'protocol' || key == 'href' || rgx.test(link.protocol)) return link[key]; 236 | // @see https://url.spec.whatwg.org/#concept-url-origin 237 | if (key == 'origin') return rgx.test(link.protocol) ? link[key] : 'null'; 238 | return tmp[key]; 239 | } 240 | 241 | function block(key, readonly, getter, out) { 242 | out = { enumerable: true }; 243 | if (!readonly) { 244 | out.set = function (val) { 245 | if (val != null) { 246 | link[key] = String(val); 247 | if (key == 'href' || key == 'search') { 248 | usp = new URLSearchParams(link.search, link); 249 | } 250 | } 251 | } 252 | } 253 | out.get = getter || function () { 254 | return proxy(key); 255 | }; 256 | return out; 257 | } 258 | 259 | usp = new URLSearchParams(link.search, link); 260 | 261 | $.toString = $.toJSON = link.toString.bind(link); 262 | 263 | return Object.defineProperties($, { 264 | href: block('href'), 265 | protocol: block('protocol'), 266 | username: block('username'), 267 | password: block('password'), 268 | hostname: block('hostname'), 269 | host: block('host'), 270 | port: block('port'), 271 | search: block('search'), 272 | hash: block('hash'), 273 | pathname: block('pathname'), 274 | origin: block('origin', 1), 275 | searchParams: block('searchParams', 1, function () { 276 | return usp; 277 | }) 278 | }); 279 | } 280 | -------------------------------------------------------------------------------- /test/polyfill-io.js: -------------------------------------------------------------------------------- 1 | import { URL, URLSearchParams } from '../src'; 2 | 3 | /** 4 | * @see https://raw.githubusercontent.com/Financial-Times/polyfill-library/master/polyfills/URL/tests.js 5 | */ 6 | 7 | it('URL IDL', () => { 8 | const url = new URL('http://example.com:8080/foo/bar?a=1&b=2#p1'); 9 | expect(typeof url.protocol).toBe('string'); 10 | expect(typeof url.host).toBe('string'); 11 | expect(typeof url.hostname).toBe('string'); 12 | expect(typeof url.port).toBe('string'); 13 | expect(typeof url.pathname).toBe('string'); 14 | expect(typeof url.search).toBe('string'); 15 | expect(typeof url.hash).toBe('string'); 16 | expect(typeof url.origin).toBe('string'); 17 | expect(typeof url.href).toBe('string'); 18 | }); 19 | 20 | it('URL Stringifying', function() { 21 | expect(String(new URL('http://example.com'))).toBe('http://example.com/'); 22 | expect(String(new URL('http://example.com:8080'))).toBe('http://example.com:8080/'); 23 | }); 24 | 25 | it('URL Parsing', () => { 26 | const url = new URL('http://example.com:8080/foo/bar?a=1&b=2#p1'); 27 | 28 | expect(url.protocol).toBe('http:'); 29 | expect(url.hostname).toBe('example.com'); 30 | expect(url.port).toBe('8080'); 31 | expect(url.host).toBe('example.com:8080'); 32 | expect(url.pathname).toBe('/foo/bar'); 33 | expect(url.search).toBe('?a=1&b=2'); 34 | expect(url.hash).toBe('#p1'); 35 | expect(url.origin).toBe('http://example.com:8080'); 36 | expect(url.href).toBe('http://example.com:8080/foo/bar?a=1&b=2#p1'); 37 | }); 38 | 39 | describe('URL Mutation', () => { 40 | it('should react to `protocol` updates', () => { 41 | const foo = new URL('http://example.com'); 42 | 43 | expect(foo.href).toBe('http://example.com/'); 44 | expect(foo.origin).toBe('http://example.com'); 45 | expect(foo.host).toBe('example.com'); 46 | 47 | foo.protocol = 'ftp'; 48 | expect(foo.protocol).toBe('ftp:'); 49 | expect(foo.href).toBe('ftp://example.com/'); 50 | 51 | // Fails in native IE13 (Edge) 52 | // Probable bug in IE. https://twitter.com/patrickkettner/status/768726160070934529 53 | expect(foo.origin).toBe('ftp://example.com'); 54 | 55 | expect(foo.host).toBe('example.com'); 56 | 57 | foo.protocol = 'http'; 58 | expect(foo.protocol).toBe('http:'); 59 | expect(foo.href).toBe('http://example.com/'); 60 | expect(foo.origin).toBe('http://example.com'); 61 | expect(foo.host).toBe('example.com'); 62 | }); 63 | 64 | it('should react to `hostname` updates', () => { 65 | const foo = new URL('http://example.com'); 66 | 67 | foo.hostname = 'example.org'; 68 | expect(foo.href).toBe('http://example.org/'); 69 | expect(foo.origin).toBe('http://example.org'); 70 | expect(foo.host).toBe('example.org'); 71 | 72 | foo.hostname = 'example.com'; 73 | expect(foo.href).toBe('http://example.com/'); 74 | expect(foo.origin).toBe('http://example.com'); 75 | expect(foo.host).toBe('example.com'); 76 | }); 77 | 78 | it('should react to `port` updates', () => { 79 | const foo = new URL('http://example.com'); 80 | 81 | foo.port = 8080; 82 | expect(foo.href).toBe('http://example.com:8080/'); 83 | expect(foo.origin).toBe('http://example.com:8080'); 84 | expect(foo.host).toBe('example.com:8080'); 85 | 86 | foo.port = 80; 87 | expect(foo.href).toBe('http://example.com/'); 88 | expect(foo.origin).toBe('http://example.com'); 89 | expect(foo.host).toBe('example.com'); 90 | }); 91 | 92 | it('should react to `pathname` updates', () => { 93 | const foo = new URL('http://example.com'); 94 | 95 | foo.pathname = 'foo'; 96 | expect(foo.href).toBe('http://example.com/foo'); 97 | expect(foo.origin).toBe('http://example.com'); 98 | 99 | foo.pathname = 'foo/bar'; 100 | expect(foo.href).toBe('http://example.com/foo/bar'); 101 | expect(foo.origin).toBe('http://example.com'); 102 | 103 | foo.pathname = ''; 104 | expect(foo.href).toBe('http://example.com/'); 105 | expect(foo.origin).toBe('http://example.com'); 106 | }); 107 | 108 | it('should react to `search` updates', () => { 109 | const foo = new URL('http://example.com'); 110 | 111 | foo.search = 'a=1&b=2'; 112 | expect(foo.href).toBe('http://example.com/?a=1&b=2'); 113 | expect(foo.origin).toBe('http://example.com'); 114 | 115 | foo.search = ''; 116 | expect(foo.href).toBe('http://example.com/'); 117 | expect(foo.origin).toBe('http://example.com'); 118 | }); 119 | 120 | it('should react to `hash` updates', () => { 121 | const foo = new URL('http://example.com'); 122 | 123 | foo.hash = 'p1'; 124 | expect(foo.href).toBe('http://example.com/#p1'); 125 | expect(foo.origin).toBe('http://example.com'); 126 | 127 | foo.hash = ''; 128 | expect(foo.href).toBe('http://example.com/'); 129 | expect(foo.origin).toBe('http://example.com'); 130 | }); 131 | }); 132 | 133 | it('Parameter Mutation', () => { 134 | const foo = new URL('http://example.com'); 135 | 136 | expect(foo.href).toStrictEqual('http://example.com/'); 137 | expect(foo.search).toStrictEqual(''); 138 | expect(foo.searchParams.get('a')).toStrictEqual(null); 139 | expect(foo.searchParams.get('b')).toStrictEqual(null); 140 | 141 | foo.searchParams.append('a', '1'); 142 | expect(foo.searchParams.get('a')).toStrictEqual('1'); 143 | expect(foo.searchParams.getAll('a')).toStrictEqual(['1']); 144 | expect(foo.search).toStrictEqual('?a=1'); 145 | expect(foo.href).toStrictEqual('http://example.com/?a=1'); 146 | 147 | foo.searchParams.append('b', '2'); 148 | expect(foo.searchParams.get('b')).toStrictEqual('2'); 149 | expect(foo.searchParams.getAll('b')).toStrictEqual(['2']); 150 | expect(foo.search).toStrictEqual('?a=1&b=2'); 151 | expect(foo.href).toStrictEqual('http://example.com/?a=1&b=2'); 152 | 153 | foo.searchParams.append('a', '3'); 154 | expect(foo.searchParams.get('a')).toStrictEqual('1'); 155 | expect(foo.searchParams.getAll('a')).toStrictEqual(['1', '3']); 156 | expect(foo.search).toStrictEqual('?a=1&b=2&a=3'); 157 | expect(foo.href).toStrictEqual('http://example.com/?a=1&b=2&a=3'); 158 | 159 | foo.searchParams.delete('a'); 160 | expect(foo.search).toStrictEqual('?b=2'); 161 | expect(foo.searchParams.getAll('a')).toStrictEqual([]); 162 | expect(foo.href).toStrictEqual('http://example.com/?b=2'); 163 | 164 | foo.searchParams.delete('b'); 165 | expect(foo.searchParams.getAll('b')).toStrictEqual([]); 166 | expect(foo.href).toStrictEqual('http://example.com/'); 167 | 168 | foo.href = 'http://example.com?m=9&n=3'; 169 | expect(foo.searchParams.has('a')).toStrictEqual(false); 170 | expect(foo.searchParams.has('b')).toStrictEqual(false); 171 | expect(foo.searchParams.get('m')).toStrictEqual('9'); //~> FIXED: Cannot be number 172 | expect(foo.searchParams.get('n')).toStrictEqual('3'); //~> FIXED: Cannot be number 173 | 174 | foo.href = 'http://example.com'; 175 | foo.searchParams.set('a', '1'); 176 | expect(foo.searchParams.getAll('a')).toStrictEqual(['1']); 177 | 178 | foo.search = 'a=1&b=1&b=2&c=1'; 179 | foo.searchParams.set('b', '3'); 180 | expect(foo.searchParams.getAll('b')).toStrictEqual(['3']); 181 | expect(foo.href).toStrictEqual('http://example.com/?a=1&b=3&c=1'); 182 | }); 183 | 184 | it('Parameter Encoding', () => { 185 | const foo = new URL('http://example.com'); 186 | expect(foo.href).toBe('http://example.com/'); 187 | expect(foo.search).toBe(''); 188 | 189 | foo.searchParams.append('this\x00&that\x7f\xff', '1+2=3'); 190 | expect(foo.searchParams.get('this\x00&that\x7f\xff')).toBe('1+2=3'); 191 | 192 | 193 | // The following fail in FF (tested in 38) against native impl 194 | expect(foo.search).toBe('?this%00%26that%7F%C3%BF=1%2B2%3D3'); 195 | expect(foo.href).toBe('http://example.com/?this%00%26that%7F%C3%BF=1%2B2%3D3'); 196 | 197 | foo.search = ''; 198 | foo.searchParams.append('a b', 'a b'); 199 | expect(foo.search).toBe('?a++b=a++b'); 200 | expect(foo.searchParams.get('a b')).toBe('a b'); 201 | }); 202 | 203 | it('Base URL', () => { 204 | // fully qualified URL 205 | expect(new URL('http://example.com', 'https://example.org').href).toBe('http://example.com/'); 206 | expect(new URL('http://example.com/foo/bar', 'https://example.org').href).toBe('http://example.com/foo/bar'); 207 | 208 | // protocol relative 209 | expect(new URL('//example.com', 'https://example.org').href).toBe('https://example.com/'); 210 | 211 | // path relative 212 | expect(new URL('/foo/bar', 'https://example.org').href).toBe('https://example.org/foo/bar'); 213 | expect(new URL('/foo/bar', 'https://example.org/baz/bat').href).toBe('https://example.org/foo/bar'); 214 | expect(new URL('./bar', 'https://example.org').href).toBe('https://example.org/bar'); 215 | expect(new URL('./bar', 'https://example.org/foo/').href).toBe('https://example.org/foo/bar'); 216 | expect(new URL('bar', 'https://example.org/foo/').href).toBe('https://example.org/foo/bar'); 217 | expect(new URL('../bar', 'https://example.org/foo/').href).toBe('https://example.org/bar'); 218 | expect(new URL('../bar', 'https://example.org/foo/').href).toBe('https://example.org/bar'); 219 | expect(new URL('../../bar', 'https://example.org/foo/baz/bat/').href).toBe('https://example.org/foo/bar'); 220 | expect(new URL('../../bar', 'https://example.org/foo/baz/bat').href).toBe('https://example.org/bar'); 221 | expect(new URL('../../bar', 'https://example.org/foo/baz/').href).toBe('https://example.org/bar'); 222 | expect(new URL('../../bar', 'https://example.org/foo/').href).toBe('https://example.org/bar'); 223 | expect(new URL('../../bar', 'https://example.org/foo/').href).toBe('https://example.org/bar'); 224 | 225 | // search/hash relative 226 | expect(new URL('bar?ab#cd', 'https://example.org/foo/').href).toBe('https://example.org/foo/bar?ab#cd'); 227 | expect(new URL('bar?ab#cd', 'https://example.org/foo').href).toBe('https://example.org/bar?ab#cd'); 228 | expect(new URL('?ab#cd', 'https://example.org/foo').href).toBe('https://example.org/foo?ab#cd'); 229 | expect(new URL('?ab', 'https://example.org/foo').href).toBe('https://example.org/foo?ab'); 230 | expect(new URL('#cd', 'https://example.org/foo').href).toBe('https://example.org/foo#cd'); 231 | }); 232 | 233 | it('URLSearchParams', () => { 234 | const foo = new URL('http://example.com?a=1&b=2'); 235 | expect(foo.searchParams instanceof URLSearchParams).toBe(true); 236 | 237 | expect(String(new URLSearchParams())).toBe(''); 238 | expect(String(new URLSearchParams(''))).toBe(''); 239 | expect(String(new URLSearchParams('a=1'))).toBe('a=1'); 240 | expect(String(new URLSearchParams('a=1&b=1'))).toBe('a=1&b=1'); 241 | expect(String(new URLSearchParams('a=1&b&a'))).toBe('a=1&b=&a='); 242 | 243 | // The following fail in FF (tested in 38) against native impl but FF38 passes the detect 244 | expect(String(new URLSearchParams('?'))).toBe(''); 245 | expect(String(new URLSearchParams('?a=1'))).toBe('a=1'); 246 | expect(String(new URLSearchParams('?a=1&b=1'))).toBe('a=1&b=1'); 247 | expect(String(new URLSearchParams('?a=1&b&a'))).toBe('a=1&b=&a='); 248 | expect(String(new URLSearchParams(new URLSearchParams('?')))).toBe(''); 249 | expect(String(new URLSearchParams(new URLSearchParams('?a=1')))).toBe('a=1'); 250 | expect(String(new URLSearchParams(new URLSearchParams('?a=1&b=1')))).toBe('a=1&b=1'); 251 | expect(String(new URLSearchParams(new URLSearchParams('?a=1&b&a')))).toBe('a=1&b=&a='); 252 | }); 253 | 254 | it('URLSearchParams mutation', () => { 255 | const foo = new URLSearchParams(); 256 | expect(foo.get('a')).toBe(null); 257 | expect(foo.get('b')).toBe(null); 258 | 259 | foo.append('a', '1'); 260 | expect(foo.get('a')).toBe('1'); 261 | expect(foo.getAll('a')).toStrictEqual(['1']); 262 | expect(String(foo)).toBe('a=1'); 263 | 264 | foo.append('b', '2'); 265 | expect(foo.get('b')).toBe('2'); 266 | expect(foo.getAll('b')).toStrictEqual(['2']); 267 | expect(String(foo)).toBe('a=1&b=2'); 268 | 269 | foo.append('a', '3'); 270 | expect(foo.get('a')).toBe('1'); 271 | expect(foo.getAll('a')).toStrictEqual(['1', '3']); 272 | expect(String(foo)).toBe('a=1&b=2&a=3'); 273 | 274 | foo.delete('a'); 275 | expect(String(foo)).toBe('b=2'); 276 | expect(foo.getAll('a')).toStrictEqual([]); 277 | 278 | foo.delete('b'); 279 | expect(foo.getAll('b')).toStrictEqual([]); 280 | 281 | const bar = new URLSearchParams('m=9&n=3'); 282 | expect(bar.has('a')).toBe(false); 283 | expect(bar.has('b')).toBe(false); 284 | expect(bar.get('m')).toBe('9'); // FIXED: Cannot be number 285 | expect(bar.get('n')).toBe('3'); // FIXED: Cannot be number 286 | 287 | const baz = new URLSearchParams(); 288 | baz.set('a', '1'); 289 | expect(baz.getAll('a')).toStrictEqual(['1']); 290 | 291 | const bat = new URLSearchParams('a=1&b=1&b=2&c=1'); 292 | bat.set('b', '3'); 293 | expect(bat.getAll('b')).toStrictEqual(['3']); 294 | expect(String(bat)).toBe('a=1&b=3&c=1'); 295 | 296 | // Ensure copy constructor copies by value, not reference. 297 | const sp1 = new URLSearchParams('a=1'); 298 | expect(String(sp1)).toBe('a=1'); 299 | 300 | const sp2 = new URLSearchParams(sp1); 301 | expect(String(sp2)).toBe('a=1'); 302 | sp1.append('b', '2'); 303 | sp2.append('c', '3'); 304 | expect(String(sp1)).toBe('a=1&b=2'); 305 | expect(String(sp2)).toBe('a=1&c=3'); 306 | }); 307 | 308 | // The following fail in FF (tested in 38) against native impl but FF38 passes the detect 309 | it('URLSearchParams serialization', () => { 310 | const foo = new URLSearchParams(); 311 | foo.append('this\x00&that\x7f\xff', '1+2=3'); 312 | expect(foo.get('this\x00&that\x7f\xff')).toBe('1+2=3'); 313 | expect(String(foo)).toBe('this%00%26that%7F%C3%BF=1%2B2%3D3'); 314 | 315 | const bar = new URLSearchParams(); 316 | bar.append('a b', 'a b'); 317 | expect(String(bar)).toBe('a++b=a++b'); 318 | expect(bar.get('a b')).toBe('a b'); 319 | }); 320 | 321 | it('URLSearchParams iterable methods',() => { 322 | const params = new URLSearchParams('a=1&b=2'); 323 | 324 | expect([...params.entries()]).toStrictEqual([ 325 | ['a', '1'], 326 | ['b', '2'] 327 | ]); 328 | 329 | expect([...params[Symbol.iterator]()]).toStrictEqual([ 330 | ['a', '1'], 331 | ['b', '2'] 332 | ]); 333 | 334 | expect([...params]).toStrictEqual([ 335 | ['a', '1'], 336 | ['b', '2'] 337 | ]); 338 | 339 | expect([...params.keys()]).toStrictEqual(['a', 'b']); 340 | expect([...params.values()]).toStrictEqual(['1', '2']); 341 | }); 342 | 343 | it('Regression tests', () => { 344 | // IE mangles the pathname when assigning to search with 'about:' URLs 345 | const p = new URL('about:blank').searchParams; 346 | p.append('a', 1); 347 | p.append('b', 2); 348 | expect(p.toString()).toBe('a=1&b=2'); 349 | }); 350 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import * as lib from '../src'; 2 | import { compare, parse, toErrors } from './utils'; 3 | 4 | describe('exports', () => { 5 | it('should export an object', () => { 6 | expect(typeof lib).toBe('object'); 7 | }); 8 | 9 | it('should export "URL" function', () => { 10 | expect(typeof lib.URL).toBe('function'); 11 | }); 12 | 13 | it('should export "URLSearchParams" function', () => { 14 | expect(typeof lib.URLSearchParams).toBe('function'); 15 | }); 16 | }); 17 | 18 | describe('URL', () => { 19 | describe('TypeErrors', () => { 20 | it('should throw when `base` is invalid', () => { 21 | const [local, native] = toErrors('URL', '', 'foobar'); 22 | expect(local).toStrictEqual(native); 23 | }); 24 | 25 | it('should throw when `url` is invalid w/o `base` present', () => { 26 | const [local, native] = toErrors('URL', 'foobar'); 27 | expect(local).toStrictEqual(native); 28 | }); 29 | 30 | it('should throw when `url` is an empty string', () => { 31 | const [local, native] = toErrors('URL', ''); 32 | expect(local).toStrictEqual(native); 33 | }); 34 | 35 | it('should throw when no arguments passed', () => { 36 | const [local, native] = toErrors('URL'); 37 | expect(local).toStrictEqual(native); 38 | }); 39 | }); 40 | 41 | describe('Instance', () => { 42 | it('should return an instance of `URL` class', () => { 43 | const local = new lib.URL('http:/foo.com'); 44 | expect(local instanceof lib.URL).toBe(true); 45 | }); 46 | }); 47 | 48 | describe('Matches `URL` from Node.js', () => { 49 | it('file:///C:/demo', () => { 50 | const [local, native] = compare('URL', 'file:///C:/demo'); 51 | expect(local).toStrictEqual(native); 52 | // direct copy from Node.js – insurance 53 | expect(local).toStrictEqual({ 54 | href: 'file:///C:/demo', 55 | origin: 'null', 56 | protocol: 'file:', 57 | username: '', 58 | password: '', 59 | host: '', 60 | hostname: '', 61 | port: '', 62 | pathname: '/C:/demo', 63 | search: '', 64 | hash: '' 65 | }); 66 | }); 67 | 68 | it('webpack:///C:/demo', () => { 69 | const [local, native] = compare('URL', 'webpack:///C:/demo'); 70 | expect(local).toStrictEqual(native); 71 | // direct copy from Node.js – insurance 72 | expect(local).toStrictEqual({ 73 | href: 'webpack:///C:/demo', 74 | origin: 'null', 75 | protocol: 'webpack:', 76 | username: '', 77 | password: '', 78 | host: '', 79 | hostname: '', 80 | port: '', 81 | pathname: '/C:/demo', 82 | search: '', 83 | hash: '' 84 | }); 85 | }); 86 | 87 | it('git://github.com/lukeed/url-shim', () => { 88 | const [local, native] = compare('URL', 'git://github.com/lukeed/url-shim'); 89 | expect(local).toStrictEqual(native); 90 | // direct copy from Node.js – insurance 91 | expect(local).toStrictEqual({ 92 | href: 'git://github.com/lukeed/url-shim', 93 | origin: 'null', 94 | protocol: 'git:', 95 | username: '', 96 | password: '', 97 | host: 'github.com', 98 | hostname: 'github.com', 99 | port: '', 100 | pathname: '/lukeed/url-shim', 101 | search: '', 102 | hash: '' 103 | }); 104 | }); 105 | 106 | it('./hello/world :: http://example.com', () => { 107 | const [local, native] = compare('URL', './hello/world', 'http://example.com'); 108 | expect(local).toStrictEqual(native); 109 | // direct copy from Node.js – insurance 110 | expect(local).toStrictEqual({ 111 | href: 'http://example.com/hello/world', 112 | origin: 'http://example.com', 113 | protocol: 'http:', 114 | username: '', 115 | password: '', 116 | host: 'example.com', 117 | hostname: 'example.com', 118 | port: '', 119 | pathname: '/hello/world', 120 | search: '', 121 | hash: '' 122 | }); 123 | }); 124 | 125 | it('../hello/world :: http://example.com', () => { 126 | const [local, native] = compare('URL', '../hello/world', 'http://example.com'); 127 | expect(local).toStrictEqual(native); 128 | // direct copy from Node.js – insurance 129 | expect(local).toStrictEqual({ 130 | href: 'http://example.com/hello/world', 131 | origin: 'http://example.com', 132 | protocol: 'http:', 133 | username: '', 134 | password: '', 135 | host: 'example.com', 136 | hostname: 'example.com', 137 | port: '', 138 | pathname: '/hello/world', 139 | search: '', 140 | hash: '' 141 | }); 142 | }); 143 | 144 | it('/hello/world :: http://example.com', () => { 145 | const [local, native] = compare('URL', '/hello/world', 'http://example.com'); 146 | expect(local).toStrictEqual(native); 147 | // direct copy from Node.js – insurance 148 | expect(local).toStrictEqual({ 149 | href: 'http://example.com/hello/world', 150 | origin: 'http://example.com', 151 | protocol: 'http:', 152 | username: '', 153 | password: '', 154 | host: 'example.com', 155 | hostname: 'example.com', 156 | port: '', 157 | pathname: '/hello/world', 158 | search: '', 159 | hash: '' 160 | }); 161 | }); 162 | 163 | it('./hello/world :: http://example.com/foo/bar', () => { 164 | const [local, native] = compare('URL', './hello/world', 'http://example.com/foo/bar'); 165 | expect(local).toStrictEqual(native); 166 | // direct copy from Node.js – insurance 167 | expect(local).toStrictEqual({ 168 | href: 'http://example.com/foo/hello/world', 169 | origin: 'http://example.com', 170 | protocol: 'http:', 171 | username: '', 172 | password: '', 173 | host: 'example.com', 174 | hostname: 'example.com', 175 | port: '', 176 | pathname: '/foo/hello/world', 177 | search: '', 178 | hash: '' 179 | }); 180 | }); 181 | 182 | it('../hello/world :: http://example.com/foo/bar', () => { 183 | const [local, native] = compare('URL', '../hello/world', 'http://example.com/foo/bar'); 184 | expect(local).toStrictEqual(native); 185 | // direct copy from Node.js – insurance 186 | expect(local).toStrictEqual({ 187 | href: 'http://example.com/hello/world', 188 | origin: 'http://example.com', 189 | protocol: 'http:', 190 | username: '', 191 | password: '', 192 | host: 'example.com', 193 | hostname: 'example.com', 194 | port: '', 195 | pathname: '/hello/world', 196 | search: '', 197 | hash: '' 198 | }); 199 | }); 200 | 201 | it('/hello/world :: http://example.com/foo/bar', () => { 202 | const [local, native] = compare('URL', '/hello/world', 'http://example.com/foo/bar'); 203 | expect(local).toStrictEqual(native); 204 | // direct copy from Node.js – insurance 205 | expect(local).toStrictEqual({ 206 | href: 'http://example.com/hello/world', 207 | origin: 'http://example.com', 208 | protocol: 'http:', 209 | username: '', 210 | password: '', 211 | host: 'example.com', 212 | hostname: 'example.com', 213 | port: '', 214 | pathname: '/hello/world', 215 | search: '', 216 | hash: '' 217 | }); 218 | }); 219 | 220 | it('/ :: http://example.com/foo/bar', () => { 221 | const [local, native] = compare('URL', '/', 'http://example.com/foo/bar'); 222 | expect(local).toStrictEqual(native); 223 | // direct copy from Node.js – insurance 224 | expect(local).toStrictEqual({ 225 | href: 'http://example.com/', 226 | origin: 'http://example.com', 227 | protocol: 'http:', 228 | username: '', 229 | password: '', 230 | host: 'example.com', 231 | hostname: 'example.com', 232 | port: '', 233 | pathname: '/', 234 | search: '', 235 | hash: '' 236 | }); 237 | }); 238 | 239 | it('/ :: http://example.com/', () => { 240 | const [local, native] = compare('URL', '/', 'http://example.com/'); 241 | expect(local).toStrictEqual(native); 242 | // direct copy from Node.js – insurance 243 | expect(local).toStrictEqual({ 244 | href: 'http://example.com/', 245 | origin: 'http://example.com', 246 | protocol: 'http:', 247 | username: '', 248 | password: '', 249 | host: 'example.com', 250 | hostname: 'example.com', 251 | port: '', 252 | pathname: '/', 253 | search: '', 254 | hash: '' 255 | }); 256 | }); 257 | 258 | it('/ :: http://example.com', () => { 259 | const [local, native] = compare('URL', '/', 'http://example.com'); 260 | expect(local).toStrictEqual(native); 261 | // direct copy from Node.js – insurance 262 | expect(local).toStrictEqual({ 263 | href: 'http://example.com/', 264 | origin: 'http://example.com', 265 | protocol: 'http:', 266 | username: '', 267 | password: '', 268 | host: 'example.com', 269 | hostname: 'example.com', 270 | port: '', 271 | pathname: '/', 272 | search: '', 273 | hash: '' 274 | }); 275 | }); 276 | 277 | it('"" :: http://example.com/foo/bar', () => { 278 | const [local, native] = compare('URL', '', 'http://example.com/foo/bar'); 279 | expect(local).toStrictEqual(native); 280 | // direct copy from Node.js – insurance 281 | expect(local).toStrictEqual({ 282 | href: 'http://example.com/foo/bar', 283 | origin: 'http://example.com', 284 | protocol: 'http:', 285 | username: '', 286 | password: '', 287 | host: 'example.com', 288 | hostname: 'example.com', 289 | port: '', 290 | pathname: '/foo/bar', 291 | search: '', 292 | hash: '' 293 | }); 294 | }); 295 | 296 | it('"" :: http://example.com/', () => { 297 | const [local, native] = compare('URL', '', 'http://example.com/'); 298 | expect(local).toStrictEqual(native); 299 | // direct copy from Node.js – insurance 300 | expect(local).toStrictEqual({ 301 | href: 'http://example.com/', 302 | origin: 'http://example.com', 303 | protocol: 'http:', 304 | username: '', 305 | password: '', 306 | host: 'example.com', 307 | hostname: 'example.com', 308 | port: '', 309 | pathname: '/', 310 | search: '', 311 | hash: '' 312 | }); 313 | }); 314 | 315 | it('"" :: http://example.com', () => { 316 | const [local, native] = compare('URL', '', 'http://example.com'); 317 | expect(local).toStrictEqual(native); 318 | // direct copy from Node.js – insurance 319 | expect(local).toStrictEqual({ 320 | href: 'http://example.com/', 321 | origin: 'http://example.com', 322 | protocol: 'http:', 323 | username: '', 324 | password: '', 325 | host: 'example.com', 326 | hostname: 'example.com', 327 | port: '', 328 | pathname: '/', 329 | search: '', 330 | hash: '' 331 | }); 332 | }); 333 | 334 | it('https://abc:xyz@example.com', () => { 335 | const local = new lib.URL('https://abc:xyz@example.com'); 336 | const native = new URL('https://abc:xyz@example.com'); 337 | 338 | const local_foo = parse(local); 339 | const native_foo = parse(native); 340 | expect(local_foo).toStrictEqual(native_foo); 341 | 342 | // direct copy from Node.js – insurance 343 | expect(local_foo).toStrictEqual({ 344 | href: 'https://abc:xyz@example.com/', 345 | origin: 'https://example.com', 346 | protocol: 'https:', 347 | username: 'abc', 348 | password: 'xyz', 349 | host: 'example.com', 350 | hostname: 'example.com', 351 | port: '', 352 | pathname: '/', 353 | search: '', 354 | hash: '' 355 | }); 356 | 357 | local.password = '123'; 358 | native.password = '123'; 359 | 360 | const local_bar = parse(local); 361 | const native_bar = parse(native); 362 | expect(local_bar).toStrictEqual(native_bar); 363 | 364 | // direct copy from Node.js – insurance 365 | expect(local_bar).toStrictEqual({ 366 | href: 'https://abc:123@example.com/', 367 | origin: 'https://example.com', 368 | protocol: 'https:', 369 | username: 'abc', 370 | password: '123', 371 | host: 'example.com', 372 | hostname: 'example.com', 373 | port: '', 374 | pathname: '/', 375 | search: '', 376 | hash: '' 377 | }); 378 | }); 379 | 380 | it('/foo/bar :: https://測試', () => { 381 | const [local, native] = compare('URL', '/foo/bar', 'https://測試'); 382 | expect(local).toStrictEqual(native); 383 | // direct copy from Node.js – insurance 384 | expect(local).toStrictEqual({ 385 | href: 'https://xn--g6w251d/foo/bar', 386 | origin: 'https://xn--g6w251d', 387 | protocol: 'https:', 388 | username: '', 389 | password: '', 390 | host: 'xn--g6w251d', 391 | hostname: 'xn--g6w251d', 392 | port: '', 393 | pathname: '/foo/bar', 394 | search: '', 395 | hash: '' 396 | }); 397 | }); 398 | 399 | it('https://測試', () => { 400 | const [local, native] = compare('URL', 'https://測試'); 401 | expect(local).toStrictEqual(native); 402 | // direct copy from Node.js – insurance 403 | expect(local).toStrictEqual({ 404 | href: 'https://xn--g6w251d/', 405 | origin: 'https://xn--g6w251d', 406 | protocol: 'https:', 407 | username: '', 408 | password: '', 409 | host: 'xn--g6w251d', 410 | hostname: 'xn--g6w251d', 411 | port: '', 412 | pathname: '/', 413 | search: '', 414 | hash: '' 415 | }); 416 | }); 417 | }); 418 | }); 419 | 420 | describe('URLSearchParams', () => { 421 | describe('Matches `URLSearchParams` from Node.js', () => { 422 | it('?foo=bar', () => { 423 | const [local, native] = compare('URLSearchParams', '?foo=bar'); 424 | expect(local).toStrictEqual(native); 425 | }); 426 | 427 | it('foo=bar', () => { 428 | const [local, native] = compare('URLSearchParams', 'foo=bar'); 429 | expect(local).toStrictEqual(native); 430 | }); 431 | 432 | it('?foo=bar&bar&baz#123', () => { 433 | const [local, native] = compare('URLSearchParams', '?foo=bar&bar&baz#123'); 434 | expect(local).toStrictEqual(native); 435 | }); 436 | 437 | it('[[foo,1]]', () => { 438 | const [local, native] = compare('URLSearchParams', [['foo', 1]]); 439 | expect(local).toStrictEqual(native); 440 | }); 441 | 442 | it('[[foo, null]]', () => { 443 | const [local, native] = compare('URLSearchParams', [['foo', null]]); 444 | expect(local).toStrictEqual(native); 445 | }); 446 | 447 | it('[[foo, undefined]]', () => { 448 | const [local, native] = compare('URLSearchParams', [['foo', undefined]]); 449 | expect(local).toStrictEqual(native); 450 | }); 451 | 452 | it('[[foo,1],[foo,2]]', () => { 453 | const [local, native] = compare('URLSearchParams', [['foo', 1], ['foo', '2']]); 454 | expect(local).toStrictEqual(native); 455 | }); 456 | 457 | it('[[foo,1],[bar,abc],[foo,2]]', () => { 458 | const [local, native] = compare('URLSearchParams', [ 459 | ['foo', 1], ['bar', 'abc'], ['foo', '2'] 460 | ]); 461 | expect(local).toStrictEqual(native); 462 | }); 463 | 464 | it('{ foo: 1 }', () => { 465 | const [local, native] = compare('URLSearchParams', { 466 | foo: 1 467 | }); 468 | expect(local).toStrictEqual(native); 469 | }); 470 | 471 | it('{ foo: [1,2] }', () => { 472 | const [local, native] = compare('URLSearchParams', { 473 | foo: [1, 2] 474 | }); 475 | expect(local).toStrictEqual(native); 476 | }); 477 | 478 | it('{ foo: 1, bar: 2 }', () => { 479 | const [local, native] = compare('URLSearchParams', { 480 | foo: 1, 481 | bar: 2, 482 | }); 483 | expect(local).toStrictEqual(native); 484 | }); 485 | 486 | it('{ foo: null }', () => { 487 | const [local, native] = compare('URLSearchParams', { 488 | foo: null 489 | }); 490 | expect(local).toStrictEqual(native); 491 | }); 492 | 493 | it('{ foo: undefined }', () => { 494 | const [local, native] = compare('URLSearchParams', { 495 | foo: undefined 496 | }); 497 | expect(local).toStrictEqual(native); 498 | }); 499 | 500 | it('{ foo: 1, bar: }', () => { 501 | const [local, native] = compare('URLSearchParams', { 502 | foo: 1, 503 | bar: { 504 | a: 1, 505 | c: { 506 | d: 999 507 | }, 508 | b: 2 509 | } 510 | }); 511 | expect(local).toStrictEqual(native); 512 | }); 513 | }); 514 | 515 | describe('$.append', () => { 516 | it('should throw TypeError if no params', () => { 517 | const foo = new lib.URLSearchParams(); 518 | 519 | try { 520 | foo.append(); 521 | } catch (err) { 522 | expect(err instanceof TypeError).toBe(true); 523 | expect(err.code).toBe('ERR_MISSING_ARGS'); 524 | expect(err.message).toBe('The "name" and "value" arguments must be specified'); 525 | } 526 | }); 527 | 528 | it('should throw TypeError if no value param', () => { 529 | const foo = new lib.URLSearchParams(); 530 | 531 | try { 532 | foo.append('foo'); 533 | } catch (err) { 534 | expect(err instanceof TypeError).toBe(true); 535 | expect(err.code).toBe('ERR_MISSING_ARGS'); 536 | expect(err.message).toBe('The "name" and "value" arguments must be specified'); 537 | } 538 | }); 539 | 540 | it('should respect order; values', () => { 541 | const local = new lib.URLSearchParams(); 542 | const native = new URLSearchParams(); 543 | 544 | local.append('a', 1); local.append('a', 2); 545 | native.append('a', 1); native.append('a', 2); 546 | 547 | expect(String(local)).toStrictEqual(String(native)); 548 | }); 549 | 550 | it('should respect order; keys', () => { 551 | const local = new lib.URLSearchParams(); 552 | const native = new URLSearchParams(); 553 | 554 | local.append('a', 1); local.append('b', 9); local.append('a', 2); 555 | native.append('a', 1); native.append('b', 9); native.append('a', 2); 556 | 557 | expect(String(local)).toStrictEqual(String(native)); 558 | }); 559 | 560 | it('should propagate to bound URL instance', () => { 561 | const foo = new lib.URL('http://foo.com'); 562 | const bar = new lib.URLSearchParams('?hello=world', foo); 563 | 564 | expect(foo.href).toBe('http://foo.com/?hello=world'); 565 | 566 | bar.append('foo', 1); 567 | bar.append('bar', 2); 568 | bar.append('foo', 3); 569 | 570 | expect(foo.href).toBe('http://foo.com/?hello=world&foo=1&bar=2&foo=3'); 571 | }); 572 | }); 573 | 574 | describe('$.delete', () => { 575 | it('should throw TypeError if no params', () => { 576 | const foo = new lib.URLSearchParams(); 577 | 578 | try { 579 | foo.delete(); 580 | } catch (err) { 581 | expect(err instanceof TypeError).toBe(true); 582 | expect(err.code).toBe('ERR_MISSING_ARGS'); 583 | expect(err.message).toBe('The "name" argument must be specified'); 584 | } 585 | }); 586 | 587 | it('should delete the key', () => { 588 | const local = new lib.URLSearchParams('a=1'); 589 | const native = new URLSearchParams('a=1'); 590 | 591 | local.delete('a'); 592 | native.delete('a'); 593 | 594 | expect(String(local)).toStrictEqual(String(native)); 595 | }); 596 | 597 | it('should delete all values for key', () => { 598 | const local = new lib.URLSearchParams('a=1&a=2&a=3'); 599 | const native = new URLSearchParams('a=1&a=2&a=3'); 600 | 601 | local.delete('a'); 602 | native.delete('a'); 603 | 604 | expect(String(local)).toStrictEqual(String(native)); 605 | }); 606 | 607 | it('should respect order; keys', () => { 608 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 609 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 610 | 611 | local.delete('a'); 612 | native.delete('a'); 613 | 614 | expect(String(local)).toStrictEqual(String(native)); 615 | }); 616 | 617 | it('should propagate to bound URL instance', () => { 618 | const foo = new lib.URL('http://foo.com'); 619 | const bar = new lib.URLSearchParams('?a=1&b=2&c=3', foo); 620 | 621 | expect(foo.href).toBe('http://foo.com/?a=1&b=2&c=3'); 622 | 623 | bar.delete('b'); 624 | 625 | expect(foo.href).toBe('http://foo.com/?a=1&c=3'); 626 | }); 627 | }); 628 | 629 | describe('$.entries', () => { 630 | it('should return an iterator', () => { 631 | const local = new lib.URLSearchParams('a=1'); 632 | expect(typeof local.entries().next).toBe('function'); 633 | }); 634 | 635 | it('should return all value pairs', () => { 636 | const local = new lib.URLSearchParams('a=1&a=2&a=3'); 637 | const native = new URLSearchParams('a=1&a=2&a=3'); 638 | 639 | const foo = JSON.stringify([...local.entries()]); 640 | const bar = JSON.stringify([...native.entries()]); 641 | 642 | expect(foo).toBe(bar); 643 | }); 644 | 645 | it('should equivalent to iterating the instance', () => { 646 | const local = new lib.URLSearchParams('a=1&a=2&a=3'); 647 | const native = new URLSearchParams('a=1&a=2&a=3'); 648 | 649 | const foo = JSON.stringify([...local]); 650 | const bar = JSON.stringify([...native]); 651 | 652 | expect(foo).toBe(bar); 653 | }); 654 | 655 | it('should respect order; keys', () => { 656 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 657 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 658 | 659 | const foo = JSON.stringify([...local.entries()]); 660 | const bar = JSON.stringify([...native.entries()]); 661 | 662 | expect(foo).toBe(bar); 663 | }); 664 | }); 665 | 666 | describe('$.forEach', () => { 667 | it('should throw TypeError if no params', () => { 668 | const foo = new lib.URLSearchParams(); 669 | 670 | try { 671 | foo.forEach(); 672 | } catch (err) { 673 | expect(err instanceof TypeError).toBe(true); 674 | expect(err.code).toBe('ERR_INVALID_CALLBACK'); 675 | expect(err.message).toBe('Callback must be a function'); 676 | } 677 | }); 678 | 679 | it('should throw TypeError if param not a function', () => { 680 | const foo = new lib.URLSearchParams(); 681 | 682 | try { 683 | foo.forEach(123); 684 | } catch (err) { 685 | expect(err instanceof TypeError).toBe(true); 686 | expect(err.code).toBe('ERR_INVALID_CALLBACK'); 687 | expect(err.message).toBe('Callback must be a function'); 688 | } 689 | }); 690 | 691 | it('should loop value pairs in order', () => { 692 | const local_k = [], local_v = []; 693 | const native_k = [], native_v = []; 694 | 695 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 696 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 697 | 698 | local.forEach((v, k) => { 699 | local_k.push(k); 700 | local_v.push(v); 701 | }); 702 | 703 | native.forEach((v, k) => { 704 | native_k.push(k); 705 | native_v.push(v); 706 | }); 707 | 708 | expect(local_k).toStrictEqual(['a', 'b', 'c', 'a', 'd', 'a', 'e']); 709 | expect(local_v).toStrictEqual(['1', '2', '3', '4', '5', '6', '7']); 710 | 711 | expect(local_k).toStrictEqual(native_k); 712 | expect(local_v).toStrictEqual(native_v); 713 | }); 714 | }); 715 | 716 | describe('$.get', () => { 717 | it('should throw TypeError if no params', () => { 718 | const foo = new lib.URLSearchParams(); 719 | 720 | try { 721 | foo.get(); 722 | } catch (err) { 723 | expect(err instanceof TypeError).toBe(true); 724 | expect(err.code).toBe('ERR_MISSING_ARGS'); 725 | expect(err.message).toBe('The "name" argument must be specified'); 726 | } 727 | }); 728 | 729 | it('should get the first value for a key', () => { 730 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 731 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 732 | 733 | const local_foo = local.get('a'); 734 | const native_foo = native.get('a'); 735 | 736 | const local_bar = local.get('e'); 737 | const native_bar = native.get('e'); 738 | 739 | expect(local_foo).toBe('1'); 740 | expect(local_foo).toBe(native_foo); 741 | 742 | expect(local_bar).toBe('7'); 743 | expect(local_bar).toBe(native_bar); 744 | }); 745 | 746 | it('should return `null` if not found', () => { 747 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 748 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 749 | 750 | const local_foo = local.get('foobar'); 751 | const native_foo = native.get('foobar'); 752 | 753 | expect(local_foo).toBe(null); 754 | expect(local_foo).toBe(native_foo); 755 | }); 756 | }); 757 | 758 | describe('$.getAll', () => { 759 | it('should throw TypeError if no params', () => { 760 | const foo = new lib.URLSearchParams(); 761 | 762 | try { 763 | foo.getAll(); 764 | } catch (err) { 765 | expect(err instanceof TypeError).toBe(true); 766 | expect(err.code).toBe('ERR_MISSING_ARGS'); 767 | expect(err.message).toBe('The "name" argument must be specified'); 768 | } 769 | }); 770 | 771 | it('should get the all values for a key', () => { 772 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 773 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 774 | 775 | const local_foo = JSON.stringify(local.getAll('a')); 776 | const native_foo = JSON.stringify(native.getAll('a')); 777 | const expected_foo = JSON.stringify(['1', '4', '6']); 778 | 779 | const local_bar = JSON.stringify(local.getAll('e')); 780 | const native_bar = JSON.stringify(native.getAll('e')); 781 | const expected_bar = JSON.stringify(['7']); 782 | 783 | expect(local_foo).toBe(expected_foo); 784 | expect(local_foo).toBe(native_foo); 785 | 786 | expect(local_bar).toBe(expected_bar); 787 | expect(local_bar).toBe(native_bar); 788 | }); 789 | 790 | it('should return empty array if not found', () => { 791 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 792 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 793 | 794 | const local_foo = JSON.stringify(local.getAll('foobar')); 795 | const native_foo = JSON.stringify(native.getAll('foobar')); 796 | 797 | expect(local_foo).toStrictEqual('[]'); 798 | expect(native_foo).toStrictEqual('[]'); 799 | }); 800 | 801 | it('should return Array; not iterator', () => { 802 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 803 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 804 | 805 | const local_foo = local.getAll('a'); 806 | const native_foo = native.getAll('a'); 807 | 808 | expect(local_foo.next).toBeUndefined(); 809 | expect(native_foo.next).toBeUndefined(); 810 | 811 | expect(Array.isArray(local_foo)).toBe(true); 812 | expect(Array.isArray(native_foo)).toBe(true); 813 | }); 814 | }); 815 | 816 | describe('$.has', () => { 817 | it('should throw TypeError if no params', () => { 818 | const foo = new lib.URLSearchParams(); 819 | 820 | try { 821 | foo.get(); 822 | } catch (err) { 823 | expect(err instanceof TypeError).toBe(true); 824 | expect(err.code).toBe('ERR_MISSING_ARGS'); 825 | expect(err.message).toBe('The "name" argument must be specified'); 826 | } 827 | }); 828 | 829 | it('should return `true` if found', () => { 830 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 831 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 832 | 833 | const local_foo = local.has('a'); 834 | const native_foo = native.has('a'); 835 | 836 | expect(local_foo).toBe(true); 837 | expect(local_foo).toBe(native_foo); 838 | }); 839 | 840 | it('should return `false` if not found', () => { 841 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 842 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 843 | 844 | const local_foo = local.has('foobar'); 845 | const native_foo = native.has('foobar'); 846 | 847 | expect(local_foo).toBe(false); 848 | expect(local_foo).toBe(native_foo); 849 | }); 850 | }); 851 | 852 | describe('$.keys', () => { 853 | it('should return an iterator', () => { 854 | const local = new lib.URLSearchParams('a=1'); 855 | expect(typeof local.keys().next).toBe('function'); 856 | }); 857 | 858 | it('should return all keys', () => { 859 | const local = new lib.URLSearchParams('a=1&a=2&a=3'); 860 | const native = new URLSearchParams('a=1&a=2&a=3'); 861 | 862 | const foo = JSON.stringify([...local.keys()]); 863 | const bar = JSON.stringify([...native.keys()]); 864 | 865 | expect(foo).toBe(bar); 866 | }); 867 | 868 | it('should respect key order', () => { 869 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 870 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 871 | 872 | const foo = JSON.stringify([...local.keys()]); 873 | const bar = JSON.stringify([...native.keys()]); 874 | 875 | expect(foo).toBe(bar); 876 | }); 877 | }); 878 | 879 | describe('$.values', () => { 880 | it('should return an iterator', () => { 881 | const local = new lib.URLSearchParams('a=1'); 882 | expect(typeof local.values().next).toBe('function'); 883 | }); 884 | 885 | it('should return all values', () => { 886 | const local = new lib.URLSearchParams('a=1&a=2&a=3'); 887 | const native = new URLSearchParams('a=1&a=2&a=3'); 888 | 889 | const foo = JSON.stringify([...local.values()]); 890 | const bar = JSON.stringify([...native.values()]); 891 | 892 | expect(foo).toBe(bar); 893 | }); 894 | 895 | it('should respect key order', () => { 896 | const local = new lib.URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 897 | const native = new URLSearchParams('a=1&b=2&c=3&a=4&d=5&a=6&e=7'); 898 | 899 | const foo = JSON.stringify([...local.values()]); 900 | const bar = JSON.stringify([...native.values()]); 901 | 902 | expect(foo).toBe(bar); 903 | }); 904 | }); 905 | 906 | describe('$.sort', () => { 907 | it('should sort key order', () => { 908 | const local = new lib.URLSearchParams('a=4&d=5&a=6&e=7&a=1&b=2&c=3'); 909 | const native = new URLSearchParams('a=4&d=5&a=6&e=7&a=1&b=2&c=3'); 910 | 911 | const local_old = JSON.stringify([...local.keys()]); 912 | const native_old = JSON.stringify([...native.keys()]); 913 | const expected_old = JSON.stringify(['a', 'd', 'a', 'e', 'a', 'b', 'c']); 914 | 915 | expect(local_old).toBe(expected_old); 916 | expect(local_old).toBe(native_old); 917 | 918 | local.sort(); 919 | native.sort(); 920 | 921 | const local_nxt_k = JSON.stringify([...local.keys()]); 922 | const native_nxt_k = JSON.stringify([...native.keys()]); 923 | const expected_nxt_k = JSON.stringify(['a', 'a', 'a', 'b', 'c', 'd', 'e']); 924 | expect(local_nxt_k).toBe(expected_nxt_k); 925 | expect(local_nxt_k).toBe(native_nxt_k); 926 | 927 | const local_nxt_v = JSON.stringify([...local.values()]); 928 | const native_nxt_v = JSON.stringify([...native.values()]); 929 | const expected_nxt_v = JSON.stringify(['4', '6', '1', '2', '3', '5', '7']); 930 | expect(local_nxt_v).toBe(expected_nxt_v); 931 | expect(local_nxt_v).toBe(native_nxt_v); 932 | }); 933 | }); 934 | 935 | describe('$.set', () => { 936 | it('should throw TypeError if no params', () => { 937 | const foo = new lib.URLSearchParams(); 938 | 939 | try { 940 | foo.set(); 941 | } catch (err) { 942 | expect(err instanceof TypeError).toBe(true); 943 | expect(err.code).toBe('ERR_MISSING_ARGS'); 944 | expect(err.message).toBe('The "name" and "value" arguments must be specified'); 945 | } 946 | }); 947 | 948 | it('should throw TypeError if no value param', () => { 949 | const foo = new lib.URLSearchParams(); 950 | 951 | try { 952 | foo.set('foo'); 953 | } catch (err) { 954 | expect(err instanceof TypeError).toBe(true); 955 | expect(err.code).toBe('ERR_MISSING_ARGS'); 956 | expect(err.message).toBe('The "name" and "value" arguments must be specified'); 957 | } 958 | }); 959 | 960 | it('should overwrite existing keys', () => { 961 | const local = new lib.URLSearchParams(); 962 | const native = new URLSearchParams(); 963 | 964 | local.set('a', 1); local.set('a', 2); 965 | native.set('a', 1); native.set('a', 2); 966 | 967 | expect(String(local)).toStrictEqual(String(native)); 968 | }); 969 | 970 | it('should respect order; keys', () => { 971 | const local = new lib.URLSearchParams(); 972 | const native = new URLSearchParams(); 973 | 974 | local.set('a', 1); local.set('b', 9); local.set('a', 2); 975 | native.set('a', 1); native.set('b', 9); native.set('a', 2); 976 | 977 | expect(String(local)).toStrictEqual(String(native)); 978 | }); 979 | 980 | it('should propagate to bound URL instance', () => { 981 | const foo = new lib.URL('http://foo.com'); 982 | const bar = new lib.URLSearchParams('?hello=world', foo); 983 | 984 | expect(foo.href).toBe('http://foo.com/?hello=world'); 985 | 986 | bar.set('foo', 1); 987 | bar.set('bar', 2); 988 | bar.set('foo', 3); 989 | 990 | expect(foo.href).toBe('http://foo.com/?hello=world&foo=3&bar=2'); 991 | }); 992 | }); 993 | }); 994 | --------------------------------------------------------------------------------