mod common;
#[test]
fn navigator_exposes_all_html_worker_mixins_with_web_idl_brands() {
let output = common::run(
r#"
const attributes = ['appCodeName','appName','appVersion','platform','product','userAgent',
'language','languages','onLine','hardwareConcurrency'];
for (const name of attributes) {
const descriptor = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype,name);
if (!descriptor.get || !!descriptor.set !== (name === 'userAgent') || !descriptor.enumerable || !descriptor.configurable) throw Error('Descriptor '+name);
for (const receiver of [{},WorkerNavigator.prototype,new EventTarget()]) {
try {descriptor.get.call(receiver); throw Error('Brand accepted '+name);} catch (error) {if (!(error instanceof TypeError)) throw error;}
}
if (Object.hasOwn(navigator,name)) throw Error('Own attribute '+name);
}
const languages = navigator.languages;
if (languages !== navigator.languages || !Object.isFrozen(languages) || languages[0] !== navigator.language) throw Error('Languages');
if (typeof navigator.appVersion !== 'string' || navigator.appVersion.length === 0 ||
typeof navigator.userAgent !== 'string' || navigator.userAgent.length === 0 ||
navigator.hardwareConcurrency < 1 || !Number.isInteger(navigator.hardwareConcurrency) ||
navigator.onLine !== true) throw Error('Metadata');
const channel = new BroadcastChannel('navigator');
for (const clone of [() => structuredClone(navigator), () => postMessage(navigator), () => channel.postMessage(navigator)]) {
try {clone(); throw Error('Navigator cloned');} catch (error) {if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error;}
}
channel.close();
WeakMap.prototype.get = () => {throw Error('Guest getter');};
globalThis.WeakMap = () => {throw Error('Guest WeakMap');};
if (navigator.languages !== languages || navigator.userAgent.length === 0) throw Error('Changed metadata');
console.log('navigator');
"#,
&[],
);
assert_eq!(output.stdout, b"navigator\n");
}
#[test]
fn user_agent_is_writable_with_string_conversion_and_brand_checks() {
let output = common::run(
r#"
const original = navigator.userAgent;
const identity = JSON.stringify(navigator.userAgentData);
navigator.userAgent = 'custom/1.0';
if (navigator.userAgent !== 'custom/1.0') throw Error('Assignment');
if (JSON.stringify(navigator.userAgentData) !== identity) throw Error('Identity changed');
for (const value of [42, null, undefined, '', {toString() {return 'object/1.0';}}]) {
navigator.userAgent = value;
if (navigator.userAgent !== String(value)) throw Error('String conversion');
}
const setter = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgent').set;
for (const receiver of [{}, WorkerNavigator.prototype, new EventTarget()]) {
try { setter.call(receiver, 'bad'); throw Error('Brand accepted'); }
catch (error) { if (!(error instanceof TypeError)) throw error; }
}
navigator.userAgent = original;
try { navigator.userAgent = Symbol(); throw Error('Symbol accepted'); }
catch (error) { if (!(error instanceof TypeError)) throw error; }
const failure = new Error('conversion failed');
try { navigator.userAgent = {toString() {throw failure;}}; throw Error('Conversion accepted'); }
catch (error) { if (error !== failure) throw error; }
if (navigator.userAgent !== original) throw Error('Failed assignment mutated UA');
WeakMap.prototype.get = () => {throw Error('Guest getter');};
globalThis.String = () => {throw Error('Guest conversion');};
navigator.userAgent = 123;
if (navigator.userAgent !== '123') throw Error('Primordials');
console.log('writable UA');
"#,
&[],
);
assert_eq!(output.stdout, b"writable UA\n");
}
#[test]
fn navigator_user_agent_data_matches_spec_and_deno() {
let output = common::run(
r#"
const data = navigator.userAgentData;
if (!(data instanceof NavigatorUAData) || typeof NavigatorUAData !== 'function' || typeof Navigator !== 'undefined') throw Error('Interface');
if (Object.prototype.toString.call(data) !== '[object NavigatorUAData]') throw Error('Tag');
if ('vendor' in navigator || 'productSub' in navigator || 'vendorSub' in navigator) throw Error('Window-only');
try { new NavigatorUAData(); throw Error('Constructor accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
for (const name of ['brands','mobile','platform']) {
const descriptor = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, name);
if (!descriptor.get || descriptor.set || !descriptor.enumerable || !descriptor.configurable) throw Error('Descriptor '+name);
for (const receiver of [{}, NavigatorUAData.prototype, navigator, WorkerNavigator.prototype]) {
try { descriptor.get.call(receiver); throw Error('Brand accepted '+name); } catch (error) { if (!(error instanceof TypeError)) throw error; }
}
if (Object.hasOwn(data, name)) throw Error('Own attribute '+name);
}
const userAgentData = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgentData');
if (!userAgentData.get || userAgentData.set || !userAgentData.enumerable || !userAgentData.configurable) throw Error('Descriptor userAgentData');
for (const receiver of [{}, WorkerNavigator.prototype, new EventTarget(), data]) {
try { userAgentData.get.call(receiver); throw Error('Brand accepted userAgentData'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
}
if (Object.hasOwn(navigator, 'userAgentData')) throw Error('Own userAgentData');
const toJSON = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, 'toJSON');
if (typeof toJSON.value !== 'function' || !toJSON.enumerable || !toJSON.configurable) throw Error('Method toJSON');
for (const receiver of [{}, NavigatorUAData.prototype, navigator]) {
try { toJSON.value.call(receiver); throw Error('Brand accepted toJSON'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
}
const ghev = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, 'getHighEntropyValues');
if (typeof ghev.value !== 'function' || !ghev.enumerable || !ghev.configurable) throw Error('Method getHighEntropyValues');
for (const receiver of [{}, NavigatorUAData.prototype, navigator]) {
let value;
try { value = ghev.value.call(receiver, []); } catch (error) { throw Error('Sync throw brand'); }
if (!(value instanceof Promise)) throw Error('Brand not a promise');
try { await value; throw Error('Brand accepted getHighEntropyValues'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
}
if (data !== navigator.userAgentData) throw Error('Identity');
if (data.brands !== data.brands) throw Error('SameObject');
const brands = data.brands;
if (!Array.isArray(brands) || !Object.isFrozen(brands) || brands.length !== 1) throw Error('Brands array');
const entry = brands[0];
if (!Object.isFrozen(entry) || Object.keys(entry).join() !== 'brand,version' || entry.brand !== 'avril') throw Error('Brand entry');
if (data.mobile !== false || typeof data.platform !== 'string') throw Error('Low entropy');
if (data.platform === navigator.platform) throw Error('Platform vocabulary');
if (!['Linux','macOS','Windows'].includes(data.platform)) throw Error('Platform value');
if (NavigatorUAData.prototype.getHighEntropyValues.length !== 1) throw Error('Arity');
for (const hints of [undefined, 'architecture', 42, null, {}, {length: 1}, [Symbol()]]) {
let value;
try { value = hints === undefined ? data.getHighEntropyValues() : data.getHighEntropyValues(hints); } catch (error) { throw Error('Sync throw hints'); }
if (!(value instanceof Promise)) throw Error('Hints not a promise');
try { await value; throw Error('Hints accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
}
const empty = await data.getHighEntropyValues([]);
if (Object.getPrototypeOf(empty) !== Object.prototype || Object.isFrozen(empty) || Object.keys(empty).join() !== 'brands,mobile,platform') throw Error('Empty hints');
if (!(empty instanceof Object) || empty.brands === brands || Object.isFrozen(empty.brands)) throw Error('Empty brands copy');
if (JSON.stringify(empty.brands) !== JSON.stringify(brands) || empty.mobile !== false || empty.platform !== data.platform) throw Error('Empty values');
if (!(data.getHighEntropyValues(['bogus', '42', 42]) instanceof Promise)) throw Error('Promise');
const ignored = await data.getHighEntropyValues(['bogus', '42', 42]);
if (Object.keys(ignored).join() !== 'brands,mobile,platform') throw Error('Unknown hints');
const full = await data.getHighEntropyValues(['architecture','bitness','model','platformVersion','uaFullVersion','fullVersionList','wow64']);
if (Object.keys(full).sort().join() !== 'architecture,bitness,brands,fullVersionList,mobile,model,platform,platformVersion,uaFullVersion,wow64') throw Error('Full keys');
if (!['x86','arm'].includes(full.architecture) || !['32','64'].includes(full.bitness)) throw Error('High values');
if (full.model !== '' || full.platformVersion !== '' || full.wow64 !== false) throw Error('Empty high values');
if (typeof full.uaFullVersion !== 'string' || full.uaFullVersion.split('.').length !== 3) throw Error('Full version');
if (navigator.userAgent !== 'avril/'+full.uaFullVersion || brands[0].version !== full.uaFullVersion.split('.')[0]) throw Error('Version split');
if (full.fullVersionList.length !== 1 || full.fullVersionList[0].brand !== 'avril' || full.fullVersionList[0].version !== full.uaFullVersion) throw Error('Version list');
if (Object.getPrototypeOf(full) !== Object.prototype || Object.isFrozen(full)) throw Error('Full plain');
if (JSON.stringify(structuredClone(full)) !== JSON.stringify(full)) throw Error('Full cloneable');
const json = data.toJSON();
if (Object.getPrototypeOf(json) !== Object.prototype || Object.keys(json).join() !== 'brands,mobile,platform') throw Error('JSON keys');
if (json.brands === brands || !Array.isArray(json.brands) || Object.isFrozen(json.brands)) throw Error('JSON brands copy');
if (JSON.stringify(json.brands) !== JSON.stringify(brands) || json.mobile !== false || json.platform !== data.platform) throw Error('JSON values');
if (JSON.stringify(data) !== JSON.stringify(json)) throw Error('Stringify');
const brandsClone = structuredClone(brands);
if (brandsClone === brands || JSON.stringify(brandsClone) !== JSON.stringify(brands)) throw Error('Brands cloneable');
const entryClone = structuredClone(entry);
if (entryClone === entry || JSON.stringify(entryClone) !== JSON.stringify(entry)) throw Error('Entry cloneable');
const channel = new BroadcastChannel('userAgentData');
for (const clone of [() => structuredClone(data), () => postMessage(data), () => channel.postMessage(data)]) {
try { clone(); throw Error('UA data cloned'); } catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; }
}
channel.close();
WeakMap.prototype.get = () => { throw Error('Guest getter'); };
globalThis.WeakMap = () => { throw Error('Guest WeakMap'); };
Promise.resolve = () => { throw Error('Guest resolve'); };
globalThis.Promise = class extends Promise {};
if (data.brands !== brands || data.platform.length === 0 || (await data.getHighEntropyValues([])).platform !== data.platform) throw Error('Changed data');
console.log('userAgentData');
"#,
&[],
);
assert_eq!(output.stdout, b"userAgentData\n");
}
#[test]
fn worker_navigators_have_the_same_runtime_identity() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("worker.js"),r#"
if (!(navigator instanceof WorkerNavigator) || typeof Navigator !== 'undefined') throw Error('Worker interface');
postMessage([navigator.userAgent,navigator.platform,navigator.languages,navigator.hardwareConcurrency]); close();
"#).unwrap();
std::fs::write(dir.path().join("main.js"),r#"
const worker = new Worker(new URL('./worker.js',import.meta.url),{type:'module'});
const attributes = await new Promise((resolve,reject) => {worker.onmessage = e => resolve(e.data); worker.onerror = reject;});
if (attributes[0] !== navigator.userAgent || attributes[1] !== navigator.platform ||
attributes[2].join() !== navigator.languages.join() || attributes[3] !== navigator.hardwareConcurrency) throw Error('Identity');
console.log('worker navigator');
"#).unwrap();
let output = common::wait(
std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
.arg("run")
.arg(dir.path().join("main.js"))
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap(),
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(output.stdout, b"worker navigator\n");
}