mod common; use common::run; #[test] fn parses_resolves_and_updates_url_components() { let output = run( r#" const u = new URL('../child?x=a b#f', 'https://u:p@BÜCHER.example:443/dir/file'); console.log(JSON.stringify([u.href, u.origin, u.protocol, u.username, u.password, u.host, u.hostname, u.port, u.pathname, u.search, u.hash])); u.protocol = 'http:'; u.host = '[2001:db8::1]:8080'; u.port = '443junk'; u.username = 'a b'; u.password = '#p'; u.pathname = '/old/../a b'; u.hash = 'two words'; console.log(u.href, u.origin); const before = u.href; u.hostname = 'invalid host'; u.port = '70000'; u.protocol = '1bad:'; if (u.href !== before) throw new Error('Invalid setter changed the URL'); try { u.href = '/relative'; throw new Error('Invalid href accepted'); } catch (e) { if (!(e instanceof TypeError)) throw e; } if (u.href !== before) throw new Error('Failed href assignment changed the URL'); console.log(URL.canParse('../ok', u), URL.canParse('/no-base'), URL.parse('/no-base')); console.log(URL.parse('../ok', u).href, JSON.stringify(u)); console.log(new URL('http://0x7f.1/').hostname, new URL('data:text/plain,hello').origin, new URL('blob:https://example.com/id').origin, new URL('blob:ftp://host/id').origin, new URL('blob:blob:https://example.com/id').origin); console.log(Object.prototype.toString.call(u), Object.prototype.toString.call(u.searchParams)); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "[\"https://u:p@xn--bcher-kva.example/child?x=a%20b#f\",\"https://xn--bcher-kva.example\",\"https:\",\"u\",\"p\",\"xn--bcher-kva.example\",\"xn--bcher-kva.example\",\"\",\"/child\",\"?x=a%20b\",\"#f\"]\n", "http://a%20b:%23p@[2001:db8::1]:443/a%20b?x=a%20b#two%20words http://[2001:db8::1]:443\n", "true false null\n", "http://a%20b:%23p@[2001:db8::1]:443/ok \"http://a%20b:%23p@[2001:db8::1]:443/a%20b?x=a%20b#two%20words\"\n", "127.0.0.1 null https://example.com null null\n", "[object URL] [object URLSearchParams]\n", ) ); } #[test] fn search_params_stay_live_in_both_directions() { let output = run( r#" const u = new URL('https://example.com/?a=b%20~&a=second'); const p = u.searchParams; console.log(p.size, p.get('a'), JSON.stringify(p.getAll('a'))); p.sort(); console.log(u.href); u.search = '?z=1&z=2'; if (u.searchParams !== p) throw new Error('Replaced searchParams'); console.log(p.toString(), p.has('z', '2'), p.has('z', '3')); p.delete('z', '1'); p.append('space', 'a b'); console.log(u.href); u.href = 'https://other.example/?new=value#hash'; console.log(p.toString()); p.set('new', 'updated'); p.append('new', 'duplicate'); p.set('new', 'one'); console.log(p.size, u.href); p.delete('new', undefined); console.log(u.href); const empty = new URL('https://example.com/?#'); empty.searchParams.delete('absent'); console.log(empty.href); const names = new URLSearchParams([['\uE000', 'bmp'], ['👽', 'astral'], ['a', 'first'], ['a', 'second']]); names.sort(); console.log(names.toString()); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "2 b ~ [\"b ~\",\"second\"]\n", "https://example.com/?a=b+%7E&a=second\n", "z=1&z=2 true false\n", "https://example.com/?z=2&space=a+b\n", "new=value\n", "1 https://other.example/?new=one#hash\n", "https://other.example/#hash\n", "https://example.com/#\n", "a=first&a=second&%F0%9F%91%BD=astral&%EE%80%80=bmp\n", ) ); } #[test] fn search_params_support_webidl_inputs_and_live_iteration() { let output = run( r#" console.log(new URLSearchParams('?a=1&&a=2&x=%FF+%2B&empty').toString()); console.log(new URLSearchParams({ number: 42, empty: null, missing: undefined }).toString()); console.log(new URLSearchParams(null).toString(), new URLSearchParams().toString()); const collapsed = { ['\uD800']: 'first', ['\uDC00']: 'second' }; console.log(new URLSearchParams(collapsed).toString()); let outer = 0, inner = 0; const pair = { get [Symbol.iterator]() { inner++; return function*() { yield 'a'; yield 'b'; }; } }; const input = { get [Symbol.iterator]() { outer++; return function*() { yield pair; }; } }; console.log(new URLSearchParams(input).toString(), outer, inner); const p = new URLSearchParams('a=1&b=2'); const iterator = p.entries(); console.log(Object.prototype.toString.call(iterator), iterator[Symbol.iterator]() === iterator); console.log(JSON.stringify(iterator.next())); p.delete('a'); p.append('c', '3'); console.log(JSON.stringify(iterator.next()), JSON.stringify(iterator.next())); p.append('d', '4'); console.log(JSON.stringify(iterator.next())); const owner = new URL('https://example.com/?one=1&two=2'); const keys = owner.searchParams.keys(); console.log(keys.next().value); owner.search = '?three=3&four=4'; console.log(keys.next().value); const each = new URLSearchParams('x=1'); const context = {}; const seen = []; each.forEach(function(value, name, params) { if (this !== context || params !== each) throw new Error('Incorrect forEach arguments'); seen.push([name, value]); if (name === 'x') each.append('y', '2'); }, context); console.log(JSON.stringify(seen), JSON.stringify([...each.values()])); let closed = false; const bad = { *[Symbol.iterator]() { try { yield ['one']; } finally { closed = true; } } }; try { new URLSearchParams(bad); } catch (error) { if (!(error instanceof TypeError)) throw error; } console.log(closed); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "a=1&a=2&x=%EF%BF%BD+%2B&empty=\n", "number=42&empty=null&missing=undefined\n", "null= \n", "%EF%BF%BD=second\n", "a=b 1 1\n", "[object URLSearchParams Iterator] true\n", "{\"value\":[\"a\",\"1\"],\"done\":false}\n", "{\"value\":[\"c\",\"3\"],\"done\":false} {\"done\":true}\n", "{\"value\":[\"d\",\"4\"],\"done\":false}\n", "one\nfour\n", "[[\"x\",\"1\"],[\"y\",\"2\"]] [\"1\",\"2\"]\n", "true\n", ) ); } #[test] fn url_interfaces_validate_receivers_arguments_and_usv_strings() { let output = run( r#" const typeError = fn => { try { fn(); } catch (e) { if (e instanceof TypeError) return; throw e; } throw new Error('Expected TypeError'); }; for (const fn of [() => URL('https://example.com'), () => URLSearchParams(), () => new URL(), () => URL.canParse(), () => URL.parse(), () => new URL(Symbol()), () => new URL('https://example.com', 'invalid'), () => new URLSearchParams(Symbol()), () => new URLSearchParams([['a', 'b', 'c']]), () => new URLSearchParams(['ab']), () => new URLSearchParams({ [Symbol()]: 'value' }), () => URL.prototype.toString.call({}), () => URL.prototype.toJSON.call({}), () => Object.getOwnPropertyDescriptor(URL.prototype, 'href').get.call({}), () => URLSearchParams.prototype.get.call({}), () => new URLSearchParams().append('a'), () => new URLSearchParams().get(), () => new URLSearchParams().set('a'), () => new URLSearchParams().has(), () => new URLSearchParams().delete(), () => new URLSearchParams().getAll(), () => new URLSearchParams().forEach(null), () => new URLSearchParams().append('a', Symbol()), ]) typeError(fn); console.log(new URL('/\uD800', 'https://example.com').href); console.log(new URLSearchParams([['\uD800', '\uDC00']]).toString()); class Child extends URL {} const child = new Child('https://example.com'); console.log(child.href, child instanceof Child, URL.parse('https://example.com') instanceof URL); console.log(Object.getOwnPropertyDescriptor(URL.prototype, 'href').get.name, Object.getOwnPropertyDescriptor(URL.prototype, 'href').set.name); const u = new URL('https://example.com/?a=1'); u.searchParams.append({ toString() { u.search = '?b=2'; return 'c'; } }, '3'); console.log(u.href); console.log('ok'); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "https://example.com/%EF%BF%BD\n", "%EF%BF%BD=%EF%BF%BD\n", "https://example.com/ true true\n", "get href set href\n", "https://example.com/?b=2&c=3\n", "ok\n", ) ); } #[test] fn url_operations_capture_intrinsics_and_do_not_depend_on_public_overrides() { let output = run( r#" const u = new URL('https://example.com/?b=2&a=1'); const p = u.searchParams; const weak = WeakMap.prototype, map = Map.prototype, strings = String.prototype; const poison = () => { throw new Error('Poisoned intrinsic'); }; globalThis.String = poison; globalThis.WeakMap = poison; globalThis.Map = poison; Array.prototype.push = poison; Array.prototype.splice = poison; Array.prototype.sort = poison; strings.toWellFormed = poison; weak.get = poison; weak.set = poison; map.get = poison; map.set = poison; map.has = poison; Array.prototype[Symbol.iterator] = poison; Object.defineProperty(u, 'href', { value: 'overridden' }); p.delete('b'); p.append('x', 'a b'); p.sort(); console.log(u.toString(), u.toJSON(), p.toString(), p.get('x')); console.log(new URL('/next', u).href, new URLSearchParams('x=a+b').toString()); console.log(new URLSearchParams({ a: 'b' }).toString()); "#, &[], ); assert_eq!(output.stdout, b"https://example.com/?a=1&x=a+b https://example.com/?a=1&x=a+b a=1&x=a+b a b\nhttps://example.com/next x=a+b\na=b\n"); } #[test] fn query_sequence_conversion_keeps_iterator_closing_and_error_precedence() { let output = run( r#" const failure = Error('item conversion'), log = []; const pair = { *[Symbol.iterator]() { try { yield 'key'; yield {toString() {throw failure;}}; } finally { log.push('inner'); } }, }; const input = { *[Symbol.iterator]() { try { yield ['short']; yield pair; } finally { log.push('outer'); } }, }; try { new URLSearchParams(input); throw Error('Accepted invalid input'); } catch (error) { if (error !== failure) throw error; } if (log.join() !== 'inner,outer') throw Error('IteratorClose order'); let closed = false; const invalid = { *[Symbol.iterator]() { try { yield {[Symbol.iterator]() {return 1;}}; } finally { closed = true; } }, }; try { new URLSearchParams(invalid); throw Error('Accepted primitive iterator'); } catch (error) { if (!(error instanceof TypeError)) throw error; } if (!closed) throw Error('Outer iterator was not closed'); console.log('query conversion'); "#, &[], ); assert_eq!(output.stdout, b"query conversion\n"); } #[test] fn url_coercions_reenter_before_native_updates_and_preserve_callable_shape() { let output = run( r#" for (const parse of [ (input, base) => new URL(input, base), (input, base) => URL.parse(input, base), (input, base) => URL.canParse(input, base), ]) { const log = []; parse( {toString() {log.push('input'); return './child';}}, {toString() {log.push('base'); return 'https://example.com/';}}, ); if (log.join() !== 'input,base') throw Error('Argument conversion order'); const failure = Error('base conversion'); try { parse('http://[', {toString() {throw failure;}}); throw Error('Skipped base conversion'); } catch (error) { if (error !== failure) throw error; } } const url = new URL('https://first.example/?a=1'), live = url.searchParams; url.search = {toString() { url.href = 'https://second.example/path?old=1#fragment'; return '?new=2'; }}; if (url.href !== 'https://second.example/path?new=2#fragment' || url.searchParams !== live || live.toString() !== 'new=2') throw Error('Reentrant setter'); if (URL.length !== 1 || URL.parse.length !== 1 || URL.canParse.length !== 1 || URLSearchParams.length !== 0) throw Error('Constructor/static arity'); for (const name of ['href', 'origin', 'protocol', 'username', 'password', 'host', 'hostname', 'port', 'pathname', 'search', 'hash']) { const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, name); if (descriptor.get.length !== 0 || Object.hasOwn(url, name)) throw Error('Component getter'); if (name === 'origin') { if (descriptor.set !== undefined) throw Error('Writable origin'); } else if (descriptor.set.length !== 1) throw Error('Component setter arity'); } for (const name of ['append', 'set']) { if (URLSearchParams.prototype[name].length !== 2) throw Error('Mutation arity'); } for (const name of ['delete', 'get', 'getAll', 'has', 'forEach']) { if (URLSearchParams.prototype[name].length !== 1) throw Error('Query arity'); } console.log('URL coercion'); "#, &[], ); assert_eq!(output.stdout, b"URL coercion\n"); } #[test] fn url_objects_work_as_module_specifiers_with_fetch_available() { let dir = tempfile::tempdir().unwrap(); std::fs::write( dir.path().join("main.js"), r#" const target = new URL('./dep%20space%23%F0%9F%98%80.ts', import.meta.url); const { answer } = await import(target); console.log(import.meta.main, URL.canParse(import.meta.url), answer, typeof fetch); "#, ) .unwrap(); std::fs::write( dir.path().join("dep space#😀.ts"), "export const answer: number = 42;", ) .unwrap(); let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.js") .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"true true 42 function\n"); } #[test] fn preserves_literal_query_question_marks_and_hostless_path_serialization() { let output = run( r#" const u = new URL('https://example.com/??a=1'); const parsed = URL.parse(u.href); console.log(u.searchParams.toString(), parsed.searchParams.toString()); u.searchParams.sort(); console.log(u.href); u.href = 'https://example.com/??b=2'; console.log(u.searchParams.toString()); u.search = '??c=3'; console.log(u.searchParams.toString()); const hostless = new URL('web+demo:/x'); hostless.pathname = '//p'; console.log(hostless.href, hostless.host, hostless.pathname); hostless.hash = 'h'; console.log(hostless.href, hostless.host, hostless.pathname); hostless.hostname = 'example.com'; console.log(hostless.href, hostless.pathname); hostless.hostname = ''; console.log(hostless.href, hostless.pathname); let converted = false; try { new URLSearchParams([['bad'], { *[Symbol.iterator]() { converted = true; yield 'k'; yield 'v'; } }]); } catch (e) { if (!(e instanceof TypeError)) throw e; } console.log(converted); const setter = Object.getOwnPropertyDescriptor(URL.prototype, 'href').set; console.log(Object.hasOwn(setter, 'prototype')); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "%3Fa=1 %3Fa=1\n", "https://example.com/?%3Fa=1\n", "%3Fb=2\n%3Fc=3\n", "web+demo:/.//p //p\n", "web+demo:/.//p#h //p\n", "web+demo://example.com//p#h //p\n", "web+demo:/.//p#h //p\n", "true\nfalse\n", ) ); } #[test] fn private_query_storage_cannot_be_intercepted_by_array_prototype_hooks() { let output = run( r#" const u = new URL('https://example.com/?a=1'); const p = u.searchParams; let intercepted; Object.defineProperty(Array, Symbol.species, { get() { throw new Error('Species consulted'); }, configurable: true }); Object.defineProperty(Array.prototype, '1', { set() { intercepted = this; }, configurable: true }); p.append('a', '2'); p.set('a', '3'); p.append('b', '4'); p.delete('b'); const all = p.getAll('a'); u.search = '?x=1&x=2'; p.set('x', '5'); p.append('x', '6'); const fresh = new URLSearchParams([['x', 'y']]); fresh.append('z', 'v'); delete Array.prototype[1]; console.log(intercepted === undefined, all instanceof Array, JSON.stringify(all), p.toString(), u.href, fresh.toString()); "#, &[], ); assert_eq!( output.stdout, b"true true [\"3\"] x=5&x=6 https://example.com/?x=5&x=6 x=y&z=v\n" ); } #[test] fn retained_query_iterators_follow_url_replacements_without_linking_copies() { let output = run( r#" const url = URL.parse('https://example.com/?a=1&b=2'); const live = url.searchParams; const copy = new URLSearchParams(live); const iterator = live.entries(); if (JSON.stringify(iterator.next().value) !== '["a","1"]') throw Error('Initial iterator'); url.href = 'https://other.example/?x=3&y=4#fragment'; if (url.searchParams !== live) throw Error('Lost live identity'); if (JSON.stringify(iterator.next().value) !== '["y","4"]') throw Error('Iterator retained stale query'); live.set('x', 'five six'); if (url.href !== 'https://other.example/?x=five+six&y=4#fragment') throw Error('Lost URL link'); url.search = '?z=7'; live.append('z', '8'); if (url.search !== '?z=7&z=8') throw Error('Lost replacement link'); copy.append('independent', 'yes'); if (copy.toString() !== 'a=1&b=2&independent=yes' || live.has('independent')) throw Error('Copy linked to URL'); if (!iterator.next().done) throw Error('Iterator did not finish on current query'); console.log('live'); "#, &[], ); assert_eq!(output.stdout, b"live\n"); } #[test] fn url_interface_descriptors_and_iterator_alias_are_preserved() { let output = run( r#" const iterator = Object.getOwnPropertyDescriptor(URLSearchParams.prototype, Symbol.iterator); if (iterator.value !== URLSearchParams.prototype.entries || !iterator.writable || iterator.enumerable || !iterator.configurable) throw Error('Invalid iterator descriptor'); for (const name of ['parse', 'canParse']) { const descriptor = Object.getOwnPropertyDescriptor(URL, name); if (!descriptor.enumerable || !descriptor.writable || !descriptor.configurable) throw Error('Invalid static descriptor'); } for (const ctor of [URL, URLSearchParams]) { for (const name of Object.getOwnPropertyNames(ctor.prototype)) { if (name === 'constructor') continue; const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name); if (!descriptor.enumerable || !descriptor.configurable) throw Error('Invalid prototype descriptor'); } const tag = Object.getOwnPropertyDescriptor(ctor.prototype, Symbol.toStringTag); if (tag.value !== ctor.name || tag.enumerable || tag.writable || !tag.configurable) throw Error('Invalid interface tag'); } console.log('interfaces'); "#, &[], ); assert_eq!(output.stdout, b"interfaces\n"); }