use browser_oxide::Page;
fn html(body: &str) -> String {
format!(
"<!DOCTYPE html><html><head></head><body>{}</body></html>",
body
)
}
async fn check(js: &str) -> String {
let mut page = Page::from_html(&html(""), None::<browser_oxide::stealth::StealthProfile>)
.await
.unwrap();
page.evaluate(js).unwrap_or_else(|e| format!("ERROR: {e}"))
}
async fn check_secure(js: &str) -> String {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
page.evaluate(js).unwrap_or_else(|e| format!("ERROR: {e}"))
}
#[tokio::test]
async fn window_self() {
assert_eq!(check("window === self").await, "true");
}
#[tokio::test]
async fn window_document() {
assert_eq!(check("typeof document").await, "object");
}
#[tokio::test]
async fn window_location() {
assert_eq!(check("typeof location").await, "object");
}
#[tokio::test]
async fn window_location_href() {
assert_eq!(check("typeof location.href").await, "string");
}
#[tokio::test]
async fn window_location_protocol() {
assert_eq!(check("typeof location.protocol").await, "string");
}
#[tokio::test]
async fn window_navigator() {
assert_eq!(check("typeof navigator").await, "object");
}
#[tokio::test]
async fn window_screen() {
assert_eq!(check("typeof screen").await, "object");
}
#[tokio::test]
async fn window_history() {
assert_eq!(check("typeof history").await, "object");
}
#[tokio::test]
async fn window_chrome() {
assert_eq!(check("typeof chrome").await, "object");
}
#[tokio::test]
async fn window_crypto() {
assert_eq!(check("typeof crypto").await, "object");
}
#[tokio::test]
async fn window_performance() {
assert_eq!(check("typeof performance").await, "object");
}
#[tokio::test]
async fn window_console() {
assert_eq!(check("typeof console").await, "object");
}
#[tokio::test]
async fn window_local_storage() {
assert_eq!(check("typeof localStorage").await, "object");
}
#[tokio::test]
async fn window_session_storage() {
assert_eq!(check("typeof sessionStorage").await, "object");
}
#[tokio::test]
async fn window_is_secure_context() {
assert_eq!(check("isSecureContext").await, "false");
}
#[tokio::test]
async fn window_inner_width() {
assert_eq!(check("typeof innerWidth").await, "number");
}
#[tokio::test]
async fn window_inner_height() {
assert_eq!(check("typeof innerHeight").await, "number");
}
#[tokio::test]
async fn window_outer_width() {
assert_eq!(check("typeof outerWidth").await, "number");
}
#[tokio::test]
async fn window_outer_height() {
assert_eq!(check("typeof outerHeight").await, "number");
}
#[tokio::test]
async fn window_device_pixel_ratio() {
assert_eq!(check("typeof devicePixelRatio").await, "number");
}
#[tokio::test]
async fn window_scroll_x() {
assert_eq!(check("typeof scrollX").await, "number");
}
#[tokio::test]
async fn window_scroll_y() {
assert_eq!(check("typeof scrollY").await, "number");
}
#[tokio::test]
async fn fn_set_timeout() {
assert_eq!(check("typeof setTimeout").await, "function");
}
#[tokio::test]
async fn fn_set_interval() {
assert_eq!(check("typeof setInterval").await, "function");
}
#[tokio::test]
async fn fn_clear_timeout() {
assert_eq!(check("typeof clearTimeout").await, "function");
}
#[tokio::test]
async fn fn_clear_interval() {
assert_eq!(check("typeof clearInterval").await, "function");
}
#[tokio::test]
async fn fn_request_animation_frame() {
assert_eq!(check("typeof requestAnimationFrame").await, "function");
}
#[tokio::test]
async fn fn_request_idle_callback() {
assert_eq!(check("typeof requestIdleCallback").await, "function");
}
#[tokio::test]
async fn fn_fetch() {
assert_eq!(check("typeof fetch").await, "function");
}
#[tokio::test]
async fn fn_atob() {
assert_eq!(check("typeof atob").await, "function");
}
#[tokio::test]
async fn fn_btoa() {
assert_eq!(check("typeof btoa").await, "function");
}
#[tokio::test]
async fn fn_alert() {
assert_eq!(check("typeof alert").await, "function");
}
#[tokio::test]
async fn fn_confirm() {
assert_eq!(check("typeof confirm").await, "function");
}
#[tokio::test]
async fn fn_prompt() {
assert_eq!(check("typeof prompt").await, "function");
}
#[tokio::test]
async fn fn_scroll_to() {
assert_eq!(check("typeof scrollTo").await, "function");
}
#[tokio::test]
async fn fn_scroll_by() {
assert_eq!(check("typeof scrollBy").await, "function");
}
#[tokio::test]
async fn fn_get_computed_style() {
assert_eq!(check("typeof getComputedStyle").await, "function");
}
#[tokio::test]
async fn fn_match_media() {
assert_eq!(check("typeof matchMedia").await, "function");
}
#[tokio::test]
async fn fn_get_selection() {
assert_eq!(check("typeof getSelection").await, "function");
}
#[tokio::test]
async fn fn_open() {
assert_eq!(check("typeof open").await, "function");
}
#[tokio::test]
async fn fn_close() {
assert_eq!(check("typeof close").await, "function");
}
#[tokio::test]
async fn fn_post_message() {
assert_eq!(check("typeof postMessage").await, "function");
}
#[tokio::test]
async fn nav_user_agent() {
assert_eq!(check("typeof navigator.userAgent").await, "string");
}
#[tokio::test]
async fn nav_platform() {
assert_eq!(check("typeof navigator.platform").await, "string");
}
#[tokio::test]
async fn nav_language() {
assert_eq!(check("typeof navigator.language").await, "string");
}
#[tokio::test]
async fn nav_languages() {
assert_eq!(check("Array.isArray(navigator.languages)").await, "true");
}
#[tokio::test]
async fn nav_vendor() {
assert_eq!(check("navigator.vendor").await, "Google Inc.");
}
#[tokio::test]
async fn nav_hardware_concurrency() {
assert_eq!(check("navigator.hardwareConcurrency > 0").await, "true");
}
#[tokio::test]
async fn nav_device_memory() {
assert_eq!(check_secure("navigator.deviceMemory > 0").await, "true");
}
#[tokio::test]
async fn nav_max_touch_points() {
assert_eq!(check("typeof navigator.maxTouchPoints").await, "number");
}
#[tokio::test]
async fn nav_cookie_enabled() {
assert_eq!(check("navigator.cookieEnabled").await, "true");
}
#[tokio::test]
async fn nav_on_line() {
assert_eq!(check("navigator.onLine").await, "true");
}
#[tokio::test]
async fn nav_pdf_viewer_enabled() {
assert_eq!(check("navigator.pdfViewerEnabled").await, "true");
}
#[tokio::test]
async fn nav_webdriver() {
assert_eq!(check("navigator.webdriver").await, "false");
assert_eq!(check("typeof navigator.webdriver").await, "boolean");
}
#[tokio::test]
async fn nav_plugins_length() {
assert_eq!(check("navigator.plugins.length > 0").await, "true");
}
#[tokio::test]
async fn nav_connection() {
assert_eq!(check("typeof navigator.connection").await, "object");
}
#[tokio::test]
async fn nav_java_enabled() {
assert_eq!(check("typeof navigator.javaEnabled").await, "function");
}
#[tokio::test]
async fn nav_send_beacon() {
assert_eq!(check("typeof navigator.sendBeacon").await, "function");
}
#[tokio::test]
async fn nav_get_battery() {
assert_eq!(
check_secure("typeof navigator.getBattery").await,
"function"
);
}
#[tokio::test]
async fn nav_user_agent_data() {
assert_eq!(
check_secure("typeof navigator.userAgentData").await,
"object"
);
}
#[tokio::test]
async fn nav_ua_data_brands() {
assert_eq!(
check_secure("navigator.userAgentData.brands.length > 0").await,
"true"
);
}
#[tokio::test]
async fn nav_ua_data_mobile() {
assert_eq!(
check_secure("typeof navigator.userAgentData.mobile").await,
"boolean"
);
}
#[tokio::test]
async fn nav_ua_data_get_high_entropy_is_function() {
assert_eq!(
check_secure("typeof navigator.userAgentData.getHighEntropyValues").await,
"function"
);
}
#[tokio::test]
async fn nav_ua_data_get_high_entropy_returns_promise() {
assert_eq!(
check_secure("navigator.userAgentData.getHighEntropyValues([]) instanceof Promise").await,
"true"
);
}
#[tokio::test]
async fn nav_ua_data_high_entropy_full_version_list() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
page.evaluate(
r#"window.__r = null;
navigator.userAgentData.getHighEntropyValues(['fullVersionList']).then(r => { window.__r = r; });"#
).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
let result = page.evaluate(
r#"(() => {
const r = window.__r;
if (!r) return 'null';
return Array.isArray(r.fullVersionList)
&& r.fullVersionList.length >= 3
&& r.fullVersionList.every(b => typeof b.brand === 'string' && typeof b.version === 'string');
})()"#
).unwrap();
assert_eq!(result, "true");
}
#[tokio::test]
async fn nav_ua_data_high_entropy_architecture_and_bitness() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
page.evaluate(
r#"window.__r = null;
navigator.userAgentData.getHighEntropyValues(['architecture', 'bitness']).then(r => { window.__r = r; });"#
).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
let result = page
.evaluate(
r#"(() => {
const r = window.__r;
if (!r) return 'null';
return (r.architecture === 'x86' || r.architecture === 'arm') && r.bitness === '64';
})()"#,
)
.unwrap();
assert_eq!(result, "true");
}
#[tokio::test]
async fn nav_ua_data_high_entropy_only_returns_requested_plus_low_entropy() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
page.evaluate(
r#"window.__r = null;
navigator.userAgentData.getHighEntropyValues(['architecture']).then(r => { window.__r = r; });"#
).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
let result = page
.evaluate(
r#"(() => {
const r = window.__r;
if (!r) return 'null';
return 'brands' in r && 'mobile' in r && 'platform' in r
&& 'architecture' in r
&& !('wow64' in r)
&& !('model' in r)
&& !('platformVersion' in r);
})()"#,
)
.unwrap();
assert_eq!(result, "true");
}
#[tokio::test]
async fn nav_ua_data_to_json_returns_low_entropy_only() {
assert_eq!(
check_secure(
r#"(() => {
const j = navigator.userAgentData.toJSON();
return Array.isArray(j.brands) && typeof j.mobile === 'boolean' && typeof j.platform === 'string'
&& !('fullVersionList' in j) && !('architecture' in j);
})()"#
).await,
"true"
);
}
#[tokio::test]
async fn nav_ua_data_brands_match_user_agent_version() {
assert_eq!(
check_secure(
r#"(() => {
const uaMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
if (!uaMatch) return 'no-ua-match';
const major = uaMatch[1];
return navigator.userAgentData.brands.some(b => b.brand === 'Google Chrome' && b.version === major);
})()"#
).await,
"true"
);
}
#[tokio::test]
async fn nav_media_devices() {
assert_eq!(
check_secure("typeof navigator.mediaDevices").await,
"object"
);
}
#[tokio::test]
async fn nav_permissions() {
assert_eq!(check("typeof navigator.permissions").await, "object");
}
#[tokio::test]
async fn nav_clipboard() {
assert_eq!(check_secure("typeof navigator.clipboard").await, "object");
}
#[tokio::test]
async fn nav_storage() {
assert_eq!(check_secure("typeof navigator.storage").await, "object");
}
#[tokio::test]
async fn nav_service_worker() {
assert_eq!(
check_secure("typeof navigator.serviceWorker").await,
"object"
);
}
#[tokio::test]
async fn doc_document_element() {
assert_eq!(check("document.documentElement.tagName").await, "HTML");
}
#[tokio::test]
async fn doc_head() {
assert_eq!(check("document.head.tagName").await, "HEAD");
}
#[tokio::test]
async fn doc_body() {
assert_eq!(check("document.body.tagName").await, "BODY");
}
#[tokio::test]
async fn doc_ready_state() {
assert_eq!(check("document.readyState").await, "complete");
}
#[tokio::test]
async fn doc_visibility_state() {
assert_eq!(check("document.visibilityState").await, "visible");
}
#[tokio::test]
async fn doc_hidden() {
assert_eq!(check("document.hidden").await, "false");
}
#[tokio::test]
async fn doc_has_focus() {
assert_eq!(check("document.hasFocus()").await, "true");
}
#[tokio::test]
async fn doc_character_set() {
assert_eq!(check("document.characterSet").await, "windows-1252");
}
#[tokio::test]
async fn doc_content_type() {
assert_eq!(check("document.contentType").await, "text/html");
}
#[tokio::test]
async fn doc_compat_mode() {
assert_eq!(check("document.compatMode").await, "CSS1Compat");
}
#[tokio::test]
async fn doc_default_view() {
assert_eq!(check("document.defaultView === window").await, "true");
}
#[tokio::test]
async fn doc_create_element() {
assert_eq!(check("typeof document.createElement").await, "function");
}
#[tokio::test]
async fn doc_create_text_node() {
assert_eq!(check("typeof document.createTextNode").await, "function");
}
#[tokio::test]
async fn doc_create_document_fragment() {
assert_eq!(
check("typeof document.createDocumentFragment").await,
"function"
);
}
#[tokio::test]
async fn doc_create_event() {
assert_eq!(check("typeof document.createEvent").await, "function");
}
#[tokio::test]
async fn doc_create_range() {
assert_eq!(check("typeof document.createRange").await, "function");
}
#[tokio::test]
async fn doc_get_element_by_id() {
assert_eq!(check("typeof document.getElementById").await, "function");
}
#[tokio::test]
async fn doc_query_selector() {
assert_eq!(check("typeof document.querySelector").await, "function");
}
#[tokio::test]
async fn doc_query_selector_all() {
assert_eq!(check("typeof document.querySelectorAll").await, "function");
}
#[tokio::test]
async fn doc_get_elements_by_tag_name() {
assert_eq!(
check("typeof document.getElementsByTagName").await,
"function"
);
}
#[tokio::test]
async fn doc_get_elements_by_class_name() {
assert_eq!(
check("typeof document.getElementsByClassName").await,
"function"
);
}
#[tokio::test]
async fn doc_exec_command() {
assert_eq!(check("typeof document.execCommand").await, "function");
}
#[tokio::test]
async fn doc_element_from_point() {
assert_eq!(check("typeof document.elementFromPoint").await, "function");
}
#[tokio::test]
async fn doc_element_from_point_out_of_viewport_is_null() {
let r = check(
r#"(function(){
var oob = document.elementFromPoint(99999, 99999);
var neg = document.elementFromPoint(-1, -1);
var oobList = document.elementsFromPoint(99999, 99999);
return JSON.stringify({
oob: oob === null,
neg: neg === null,
oobListEmpty: Array.isArray(oobList) && oobList.length === 0,
});
})()"#,
)
.await;
assert!(
r.contains("\"oob\":true"),
"OOB elementFromPoint must be null: {r}"
);
assert!(
r.contains("\"neg\":true"),
"negative elementFromPoint must be null: {r}"
);
assert!(
r.contains("\"oobListEmpty\":true"),
"OOB elementsFromPoint must be []: {r}"
);
}
#[tokio::test]
async fn doc_write() {
assert_eq!(check("typeof document.write").await, "function");
}
#[tokio::test]
async fn doc_writeln() {
assert_eq!(check("typeof document.writeln").await, "function");
}
#[tokio::test]
async fn doc_import_node() {
assert_eq!(check("typeof document.importNode").await, "function");
}
#[tokio::test]
async fn doc_adopt_node() {
assert_eq!(check("typeof document.adoptNode").await, "function");
}
#[tokio::test]
async fn doc_scripts() {
assert_eq!(check("typeof document.scripts").await, "object");
}
#[tokio::test]
async fn doc_forms() {
assert_eq!(check("typeof document.forms").await, "object");
}
#[tokio::test]
async fn doc_active_element() {
assert_eq!(
check("document.activeElement === document.body").await,
"true"
);
}
#[tokio::test]
async fn node_element_node() {
assert_eq!(check("Node.ELEMENT_NODE").await, "1");
}
#[tokio::test]
async fn node_text_node() {
assert_eq!(check("Node.TEXT_NODE").await, "3");
}
#[tokio::test]
async fn node_document_node() {
assert_eq!(check("Node.DOCUMENT_NODE").await, "9");
}
#[tokio::test]
async fn el_append_child() {
assert_eq!(check("typeof document.body.appendChild").await, "function");
}
#[tokio::test]
async fn el_remove_child() {
assert_eq!(check("typeof document.body.removeChild").await, "function");
}
#[tokio::test]
async fn el_insert_before() {
assert_eq!(check("typeof document.body.insertBefore").await, "function");
}
#[tokio::test]
async fn el_replace_child() {
assert_eq!(check("typeof document.body.replaceChild").await, "function");
}
#[tokio::test]
async fn el_clone_node() {
assert_eq!(check("typeof document.body.cloneNode").await, "function");
}
#[tokio::test]
async fn el_contains() {
assert_eq!(check("typeof document.body.contains").await, "function");
}
#[tokio::test]
async fn el_remove() {
assert_eq!(
check("typeof document.createElement('div').remove").await,
"function"
);
}
#[tokio::test]
async fn el_append() {
assert_eq!(check("typeof document.body.append").await, "function");
}
#[tokio::test]
async fn el_prepend() {
assert_eq!(check("typeof document.body.prepend").await, "function");
}
#[tokio::test]
async fn el_after() {
assert_eq!(
check("typeof document.createElement('div').after").await,
"function"
);
}
#[tokio::test]
async fn el_before() {
assert_eq!(
check("typeof document.createElement('div').before").await,
"function"
);
}
#[tokio::test]
async fn el_replace_with() {
assert_eq!(
check("typeof document.createElement('div').replaceWith").await,
"function"
);
}
#[tokio::test]
async fn el_replace_children() {
assert_eq!(
check("typeof document.body.replaceChildren").await,
"function"
);
}
#[tokio::test]
async fn el_get_attribute() {
assert_eq!(check("typeof document.body.getAttribute").await, "function");
}
#[tokio::test]
async fn el_set_attribute() {
assert_eq!(check("typeof document.body.setAttribute").await, "function");
}
#[tokio::test]
async fn el_remove_attribute() {
assert_eq!(
check("typeof document.body.removeAttribute").await,
"function"
);
}
#[tokio::test]
async fn el_has_attribute() {
assert_eq!(check("typeof document.body.hasAttribute").await, "function");
}
#[tokio::test]
async fn el_toggle_attribute() {
assert_eq!(
check("typeof document.body.toggleAttribute").await,
"function"
);
}
#[tokio::test]
async fn el_insert_adjacent_html() {
assert_eq!(
check("typeof document.body.insertAdjacentHTML").await,
"function"
);
}
#[tokio::test]
async fn el_insert_adjacent_element() {
assert_eq!(
check("typeof document.body.insertAdjacentElement").await,
"function"
);
}
#[tokio::test]
async fn el_matches() {
assert_eq!(check("typeof document.body.matches").await, "function");
}
#[tokio::test]
async fn el_closest() {
assert_eq!(check("typeof document.body.closest").await, "function");
}
#[tokio::test]
async fn el_get_bounding_client_rect() {
assert_eq!(
check("typeof document.body.getBoundingClientRect").await,
"function"
);
}
#[tokio::test]
async fn el_class_list() {
assert_eq!(check("typeof document.body.classList").await, "object");
}
#[tokio::test]
async fn el_style() {
assert_eq!(check("typeof document.body.style").await, "object");
}
#[tokio::test]
async fn el_dataset() {
assert_eq!(check("typeof document.body.dataset").await, "object");
}
#[tokio::test]
async fn el_inner_html() {
assert_eq!(check("typeof document.body.innerHTML").await, "string");
}
#[tokio::test]
async fn el_outer_html() {
assert_eq!(check("typeof document.body.outerHTML").await, "string");
}
#[tokio::test]
async fn el_text_content() {
assert_eq!(check("typeof document.body.textContent").await, "string");
}
#[tokio::test]
async fn el_owner_document() {
assert_eq!(
check("document.body.ownerDocument === document").await,
"true"
);
}
#[tokio::test]
async fn el_is_connected() {
assert_eq!(check("document.body.isConnected").await, "true");
}
#[tokio::test]
async fn el_offset_width() {
assert_eq!(check("typeof document.body.offsetWidth").await, "number");
}
#[tokio::test]
async fn el_offset_height() {
assert_eq!(check("typeof document.body.offsetHeight").await, "number");
}
#[tokio::test]
async fn el_check_visibility() {
assert_eq!(
check("typeof document.body.checkVisibility").await,
"function"
);
}
#[tokio::test]
async fn el_add_event_listener() {
assert_eq!(
check("typeof document.body.addEventListener").await,
"function"
);
}
#[tokio::test]
async fn el_dispatch_event() {
assert_eq!(
check("typeof document.body.dispatchEvent").await,
"function"
);
}
#[tokio::test]
async fn el_click() {
assert_eq!(check("typeof document.body.click").await, "function");
}
#[tokio::test]
async fn el_focus() {
assert_eq!(check("typeof document.body.focus").await, "function");
}
#[tokio::test]
async fn el_blur() {
assert_eq!(check("typeof document.body.blur").await, "function");
}
#[tokio::test]
async fn el_next_element_sibling() {
assert_eq!(check("'nextElementSibling' in document.body").await, "true");
}
#[tokio::test]
async fn el_previous_element_sibling() {
assert_eq!(
check("'previousElementSibling' in document.body").await,
"true"
);
}
#[tokio::test]
async fn el_child_element_count() {
assert_eq!(
check("typeof document.body.childElementCount").await,
"number"
);
}
#[tokio::test]
async fn el_animate() {
assert_eq!(check("typeof document.body.animate").await, "function");
}
#[tokio::test]
async fn cls_event() {
assert_eq!(check("typeof Event").await, "function");
}
#[tokio::test]
async fn cls_custom_event() {
assert_eq!(check("typeof CustomEvent").await, "function");
}
#[tokio::test]
async fn cls_mouse_event() {
assert_eq!(check("typeof MouseEvent").await, "function");
}
#[tokio::test]
async fn cls_keyboard_event() {
assert_eq!(check("typeof KeyboardEvent").await, "function");
}
#[tokio::test]
async fn cls_input_event() {
assert_eq!(check("typeof InputEvent").await, "function");
}
#[tokio::test]
async fn cls_focus_event() {
assert_eq!(check("typeof FocusEvent").await, "function");
}
#[tokio::test]
async fn cls_pointer_event() {
assert_eq!(check("typeof PointerEvent").await, "function");
}
#[tokio::test]
async fn cls_wheel_event() {
assert_eq!(check("typeof WheelEvent").await, "function");
}
#[tokio::test]
async fn cls_touch_event() {
assert_eq!(check("typeof TouchEvent").await, "function");
}
#[tokio::test]
async fn cls_message_event() {
assert_eq!(check("typeof MessageEvent").await, "function");
}
#[tokio::test]
async fn cls_error_event() {
assert_eq!(check("typeof ErrorEvent").await, "function");
}
#[tokio::test]
async fn cls_event_target() {
assert_eq!(check("typeof EventTarget").await, "function");
}
#[tokio::test]
async fn cls_event_source() {
assert_eq!(check("typeof EventSource").await, "function");
assert_eq!(check("EventSource.CONNECTING").await, "0");
assert_eq!(check("EventSource.OPEN").await, "1");
assert_eq!(check("EventSource.CLOSED").await, "2");
}
#[tokio::test]
async fn cls_rtc_peer_connection() {
assert_eq!(check("typeof RTCPeerConnection").await, "function");
assert_eq!(check("typeof webkitRTCPeerConnection").await, "function");
assert_eq!(check("typeof RTCSessionDescription").await, "function");
assert_eq!(check("typeof RTCIceCandidate").await, "function");
}
#[tokio::test]
async fn api_document_fonts() {
assert_eq!(check("typeof document.fonts").await, "object");
assert_eq!(check("typeof document.fonts.check").await, "function");
assert_eq!(check("document.fonts.check('12px Arial')").await, "true");
}
#[tokio::test]
async fn cls_media_source() {
assert_eq!(check("typeof MediaSource").await, "function");
assert_eq!(
check("typeof MediaSource.isTypeSupported").await,
"function"
);
assert_eq!(
check("MediaSource.isTypeSupported('video/mp4')").await,
"true"
);
assert_eq!(
check("MediaSource.isTypeSupported('video/webm; codecs=\"vp9\"')").await,
"true"
);
}
#[tokio::test]
async fn api_speech_synthesis_voices() {
assert_eq!(
check("speechSynthesis.getVoices().length > 0").await,
"true"
);
}
#[tokio::test]
async fn api_permissions_query() {
assert_eq!(
check("typeof navigator.permissions.query").await,
"function"
);
}
#[tokio::test]
async fn api_battery() {
assert_eq!(
check_secure("typeof navigator.getBattery").await,
"function"
);
}
#[tokio::test]
async fn api_dynamic_script_src_property() {
assert_eq!(
check(
r#"
const s = document.createElement('script');
s.src = 'https://example.com/test.js';
s.getAttribute('src')
"#
)
.await,
"https://example.com/test.js"
);
}
#[tokio::test]
async fn api_document_cookie_readwrite() {
assert_eq!(
check(
r#"
document.cookie = "cookietest=abc123; path=/; SameSite=Lax";
document.cookie.includes("cookietest=abc123")
"#
)
.await,
"true"
);
}
#[tokio::test]
async fn cls_url() {
assert_eq!(check("typeof URL").await, "function");
}
#[tokio::test]
async fn cls_url_search_params() {
assert_eq!(check("typeof URLSearchParams").await, "function");
}
#[tokio::test]
async fn cls_abort_controller() {
assert_eq!(check("typeof AbortController").await, "function");
}
#[tokio::test]
async fn cls_abort_signal() {
assert_eq!(check("typeof AbortSignal").await, "function");
}
#[tokio::test]
async fn cls_headers() {
assert_eq!(check("typeof Headers").await, "function");
}
#[tokio::test]
async fn cls_request() {
assert_eq!(check("typeof Request").await, "function");
}
#[tokio::test]
async fn cls_response() {
assert_eq!(check("typeof Response").await, "function");
}
#[tokio::test]
async fn cls_form_data() {
assert_eq!(check("typeof FormData").await, "function");
}
#[tokio::test]
async fn cls_blob() {
assert_eq!(check("typeof Blob").await, "function");
}
#[tokio::test]
async fn cls_file() {
assert_eq!(check("typeof File").await, "function");
}
#[tokio::test]
async fn cls_image() {
assert_eq!(check("typeof Image").await, "function");
}
#[tokio::test]
async fn cls_dom_parser() {
assert_eq!(check("typeof DOMParser").await, "function");
}
#[tokio::test]
async fn cls_dom_rect() {
assert_eq!(check("typeof DOMRect").await, "function");
}
#[tokio::test]
async fn cls_text_encoder() {
assert_eq!(check("typeof TextEncoder").await, "function");
}
#[tokio::test]
async fn cls_text_decoder() {
assert_eq!(check("typeof TextDecoder").await, "function");
}
#[tokio::test]
async fn cls_mutation_observer() {
assert_eq!(check("typeof MutationObserver").await, "function");
}
#[tokio::test]
async fn cls_intersection_observer() {
assert_eq!(check("typeof IntersectionObserver").await, "function");
}
#[tokio::test]
async fn cls_resize_observer() {
assert_eq!(check("typeof ResizeObserver").await, "function");
}
#[tokio::test]
async fn cls_xml_http_request() {
assert_eq!(check("typeof XMLHttpRequest").await, "function");
}
#[tokio::test]
async fn cls_websocket() {
assert_eq!(check("typeof WebSocket").await, "function");
}
#[tokio::test]
async fn cls_notification() {
assert_eq!(check("typeof Notification").await, "function");
}
#[tokio::test]
async fn cls_dom_exception() {
assert_eq!(check("typeof DOMException").await, "function");
}
#[tokio::test]
async fn cls_audio_context() {
assert_eq!(check("typeof AudioContext").await, "function");
}
#[tokio::test]
async fn cls_offline_audio_context() {
assert_eq!(check("typeof OfflineAudioContext").await, "function");
}
#[tokio::test]
async fn cls_custom_elements() {
assert_eq!(check("typeof customElements").await, "object");
}
#[tokio::test]
async fn cls_html_element() {
assert_eq!(check("typeof HTMLElement").await, "function");
}
#[tokio::test]
async fn cls_html_div_element() {
assert_eq!(check("typeof HTMLDivElement").await, "function");
}
#[tokio::test]
async fn cls_html_input_element() {
assert_eq!(check("typeof HTMLInputElement").await, "function");
}
#[tokio::test]
async fn cls_html_anchor_element() {
assert_eq!(check("typeof HTMLAnchorElement").await, "function");
}
#[tokio::test]
async fn cls_html_image_element() {
assert_eq!(check("typeof HTMLImageElement").await, "function");
}
#[tokio::test]
async fn cls_html_canvas_element() {
assert_eq!(check("typeof HTMLCanvasElement").await, "function");
}
#[tokio::test]
async fn cls_html_form_element() {
assert_eq!(check("typeof HTMLFormElement").await, "function");
}
#[tokio::test]
async fn cls_html_video_element() {
assert_eq!(check("typeof HTMLVideoElement").await, "function");
}
#[tokio::test]
async fn cls_svg_element() {
assert_eq!(check("typeof SVGElement").await, "function");
}
#[tokio::test]
async fn canvas_2d_context() {
assert_eq!(
check("typeof document.createElement('canvas').getContext('2d')").await,
"object"
);
}
#[tokio::test]
async fn canvas_webgl_context() {
assert_eq!(
check("typeof document.createElement('canvas').getContext('webgl')").await,
"object"
);
}
#[tokio::test]
async fn offscreen_canvas_webgl_context() {
assert_eq!(
check("typeof new OffscreenCanvas(1,1).getContext('webgl')").await,
"object"
);
assert_eq!(
check("typeof new OffscreenCanvas(1,1).getContext('webgl2')").await,
"object"
);
let vendor = check(
"(function(){var gl=new OffscreenCanvas(1,1).getContext('webgl');var e=gl.getExtension('WEBGL_debug_renderer_info');return String(gl.getParameter(e.UNMASKED_VENDOR_WEBGL));})()",
)
.await;
assert!(
vendor.contains("Google")
|| vendor.contains("Apple")
|| vendor.contains("Intel")
|| vendor.contains("Mozilla"),
"OffscreenCanvas WebGL unmasked vendor looks empty/wrong: {vendor}"
);
}
#[tokio::test]
async fn webgpu_adapter_has_real_limits_and_info() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
let setup = r#"
globalThis.__gpu = "(pending)";
(async () => {
const a = await navigator.gpu.requestAdapter();
const d = await a.requestDevice();
globalThis.__gpu = JSON.stringify({
ownKeys: Object.keys(a.limits).length,
maxTex: a.limits.maxTextureDimension2D,
maxBind: a.limits.maxBindGroups,
vendor: a.info && a.info.vendor,
arch: a.info && a.info.architecture,
features: a.features.size,
deviceOk: !!(d && d.limits && d.limits.maxBindGroups === 4),
});
})();
"#;
let _ = page
.evaluate_async(setup, std::time::Duration::from_secs(5))
.await;
let out = page.evaluate("globalThis.__gpu").unwrap_or_default();
assert!(
out.contains("\"ownKeys\":0"),
"limits own-keys must be [] like Chrome: {out}"
);
assert!(
out.contains("\"maxTex\":16384"),
"maxTextureDimension2D missing/wrong: {out}"
);
assert!(
out.contains("\"vendor\":\"apple\""),
"adapter.info.vendor wrong: {out}"
);
assert!(
out.contains("\"arch\":\"metal-3\""),
"adapter.info.architecture wrong: {out}"
);
assert!(
out.contains("\"deviceOk\":true"),
"requestDevice must resolve to a device: {out}"
);
assert!(
!out.contains("\"features\":0"),
"adapter must expose features: {out}"
);
}
#[tokio::test]
async fn canvas_to_data_url() {
assert_eq!(
check("document.createElement('canvas').toDataURL().startsWith('data:image/png')").await,
"true"
);
}
#[tokio::test]
async fn canvas_drawing_produces_nonblank_data_url() {
let result = check(r#"(function() {
var c = document.createElement('canvas');
c.width = 200; c.height = 50;
var ctx = c.getContext('2d');
ctx.font = '18px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('WBAAS fingerprint test Cwm fjordbank glyphs 😺', 2, 15);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.fillText('WBAAS fingerprint test Cwm fjordbank glyphs 😺', 4, 45);
var data = c.toDataURL('image/png');
// Check it's non-empty (not a blank canvas)
return data.length + ':' + (data !== 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAAyCAYAAAAZUl3oAAAABmJLR0QA/wD/AP+gvaeTAAAADklEQVRoge3BMQEAAADCoPVP7WsIoAAAeAMBxAACqwAAAABJRU5ErkJggg==');
})()"#).await;
eprintln!("Canvas drawing probe: {}", &result[..result.len().min(100)]);
let parts: Vec<&str> = result.splitn(2, ':').collect();
let len: usize = parts[0].parse().unwrap_or(0);
assert!(
len > 1000,
"canvas toDataURL after drawing should produce >1KB data, got {}b",
len
);
assert_eq!(
parts.get(1),
Some(&"true"),
"canvas toDataURL should differ from blank canvas"
);
}
async fn webgl_eval(profile: browser_oxide::stealth::StealthProfile, js: &str) -> String {
let mut page = Page::with_profile(
"<!DOCTYPE html><html><head></head><body></body></html>",
"https://example.com/",
profile,
)
.await
.unwrap();
page.evaluate(js).unwrap_or_else(|e| format!("ERR: {e}"))
}
#[tokio::test]
async fn webgl_extensions_has_25_plus_entries() {
let profile = browser_oxide::stealth::chrome_148_windows();
let r = webgl_eval(
profile,
r#"(() => {
const gl = document.createElement('canvas').getContext('webgl');
const exts = gl.getSupportedExtensions();
return exts.length >= 25 ? 'ok:' + exts.length : 'fail:' + exts.length;
})()"#,
)
.await;
assert!(r.starts_with("ok:"), "expected >=25 extensions, got {r}");
}
#[tokio::test]
async fn webgl_unmasked_vendor_matches_windows_profile() {
let profile = browser_oxide::stealth::chrome_148_windows();
let r = webgl_eval(
profile,
r#"(() => {
const gl = document.createElement('canvas').getContext('webgl');
return gl.getParameter(0x9245); // UNMASKED_VENDOR_WEBGL
})()"#,
)
.await;
assert!(
r.contains("NVIDIA"),
"Windows profile should report NVIDIA, got: {r}"
);
}
#[tokio::test]
async fn webgl_unmasked_renderer_matches_macos_profile() {
let profile = browser_oxide::stealth::chrome_148_macos();
let r = webgl_eval(
profile,
r#"(() => {
const gl = document.createElement('canvas').getContext('webgl');
return gl.getParameter(0x9246); // UNMASKED_RENDERER_WEBGL
})()"#,
)
.await;
assert!(
r.contains("Apple M3"),
"macOS profile should report Apple M3, got: {r}"
);
}
#[tokio::test]
async fn webgl_unmasked_renderer_matches_linux_profile() {
let profile = browser_oxide::stealth::chrome_148_linux();
let r = webgl_eval(
profile,
r#"(() => {
const gl = document.createElement('canvas').getContext('webgl');
return gl.getParameter(0x9246);
})()"#,
)
.await;
assert!(
r.contains("Intel"),
"Linux profile should report Intel, got: {r}"
);
}
#[tokio::test]
async fn webgl_extensions_differ_across_profiles() {
let apple = browser_oxide::stealth::chrome_148_macos();
let intel = browser_oxide::stealth::chrome_148_linux();
let apple_has_astc = webgl_eval(
apple,
r#"document.createElement('canvas').getContext('webgl').getSupportedExtensions().includes('WEBGL_compressed_texture_astc')"#,
).await;
let intel_has_astc = webgl_eval(
intel,
r#"document.createElement('canvas').getContext('webgl').getSupportedExtensions().includes('WEBGL_compressed_texture_astc')"#,
).await;
assert_eq!(
apple_has_astc, "true",
"Apple should expose WEBGL_compressed_texture_astc"
);
assert_eq!(
intel_has_astc, "false",
"Intel Linux should NOT expose WEBGL_compressed_texture_astc"
);
}
#[tokio::test]
async fn webgl_shader_precision_int_differs_from_float() {
let profile = browser_oxide::stealth::chrome_148_windows();
let r = webgl_eval(
profile,
r#"(() => {
const gl = document.createElement('canvas').getContext('webgl');
const VERTEX_SHADER = 0x8B31;
const HIGH_FLOAT = 0x8DF2;
const HIGH_INT = 0x8DF5;
const f = gl.getShaderPrecisionFormat(VERTEX_SHADER, HIGH_FLOAT);
const i = gl.getShaderPrecisionFormat(VERTEX_SHADER, HIGH_INT);
return f.rangeMin + ',' + f.rangeMax + ',' + f.precision + ' | ' + i.rangeMin + ',' + i.rangeMax + ',' + i.precision;
})()"#,
)
.await;
assert!(
r.contains("127,127,23 | 31,30,0"),
"shader precision wrong, got: {r}"
);
}
#[tokio::test]
async fn webgl_max_texture_size_is_16384() {
let profile = browser_oxide::stealth::chrome_148_windows();
let r = webgl_eval(
profile,
r#"document.createElement('canvas').getContext('webgl').getParameter(0x0D33)"#,
)
.await;
assert_eq!(r, "16384", "MAX_TEXTURE_SIZE should be 16384");
}
#[tokio::test]
async fn perf_get_entries_by_type_navigation() {
assert_eq!(
check("performance.getEntriesByType('navigation').length >= 1").await,
"true"
);
}
#[tokio::test]
async fn perf_navigation_entry_has_timing_fields() {
assert_eq!(
check(
r#"(() => {
const e = performance.getEntriesByType('navigation')[0];
return typeof e.domContentLoadedEventStart === 'number'
&& typeof e.loadEventEnd === 'number'
&& typeof e.transferSize === 'number'
&& e.entryType === 'navigation';
})()"#
)
.await,
"true"
);
}
#[tokio::test]
async fn perf_get_entries_by_type_resource() {
assert_eq!(
check("performance.getEntriesByType('resource').length >= 3").await,
"true"
);
}
#[tokio::test]
async fn perf_resource_entry_shape() {
assert_eq!(
check(
r#"(() => {
const r = performance.getEntriesByType('resource')[0];
return r.entryType === 'resource'
&& typeof r.initiatorType === 'string'
&& typeof r.nextHopProtocol === 'string'
&& r.nextHopProtocol === 'h2';
})()"#
)
.await,
"true"
);
}
#[tokio::test]
async fn perf_timing_is_present() {
assert_eq!(
check("typeof performance.timing.navigationStart === 'number'").await,
"true"
);
}
#[tokio::test]
async fn perf_time_origin_is_present() {
assert_eq!(
check("typeof performance.timeOrigin === 'number'").await,
"true"
);
}
#[tokio::test]
async fn intl_timezone_matches_moscow_profile() {
let profile = browser_oxide::stealth::presets::chrome_148_ru();
let mut page = Page::with_profile(
"<!DOCTYPE html><html><body></body></html>",
"https://example.com/",
profile,
)
.await
.unwrap();
let r = page
.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone")
.unwrap();
assert_eq!(r, "Europe/Moscow", "RU profile should report Europe/Moscow");
}
#[tokio::test]
async fn intl_timezone_matches_tokyo_profile() {
let profile = browser_oxide::stealth::presets::chrome_148_jp();
let mut page = Page::with_profile(
"<!DOCTYPE html><html><body></body></html>",
"https://example.com/",
profile,
)
.await
.unwrap();
let r = page
.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone")
.unwrap();
assert_eq!(r, "Asia/Tokyo", "JP profile should report Asia/Tokyo");
}
#[tokio::test]
async fn date_timezone_offset_is_numeric_from_profile() {
let profile = browser_oxide::stealth::presets::chrome_148_ru();
let mut page = Page::with_profile(
"<!DOCTYPE html><html><body></body></html>",
"https://example.com/",
profile,
)
.await
.unwrap();
let r = page
.evaluate("typeof new Date().getTimezoneOffset()")
.unwrap();
assert_eq!(r, "number");
let r2 = page.evaluate("new Date().getTimezoneOffset()").unwrap();
assert_eq!(
r2, "-180",
"Moscow profile should report -180 min offset, got {r2}"
);
}
#[tokio::test]
async fn perf_paint_entries_present() {
assert_eq!(
check(
r#"(() => {
const p = performance.getEntriesByType('paint');
return p.length === 2
&& p.some(e => e.name === 'first-paint')
&& p.some(e => e.name === 'first-contentful-paint');
})()"#
)
.await,
"true"
);
}
#[tokio::test]
async fn webgl_get_extension_returns_non_null_for_supported() {
let profile = browser_oxide::stealth::chrome_148_windows();
let r = webgl_eval(
profile,
r#"(() => {
const gl = document.createElement('canvas').getContext('webgl');
const ext = gl.getExtension('EXT_texture_filter_anisotropic');
return typeof ext;
})()"#,
)
.await;
assert_eq!(
r, "object",
"getExtension should return non-null for supported extensions"
);
}
#[tokio::test]
async fn chrome_app() {
assert_eq!(check("typeof chrome.app").await, "object");
}
#[tokio::test]
async fn chrome_runtime() {
assert_eq!(check("typeof chrome.runtime").await, "undefined");
}
#[tokio::test]
async fn chrome_csi() {
assert_eq!(check("typeof chrome.csi").await, "function");
}
#[tokio::test]
async fn chrome_load_times() {
assert_eq!(check("typeof chrome.loadTimes").await, "function");
}
#[tokio::test]
async fn performance_memory() {
assert_eq!(
check("performance.memory.jsHeapSizeLimit > 0").await,
"true"
);
}
#[tokio::test]
async fn speech_synthesis() {
assert_eq!(
check("speechSynthesis.getVoices().length > 0").await,
"true"
);
}
#[tokio::test]
async fn iframe_content_window() {
assert_eq!(
check("typeof document.createElement('iframe').contentWindow").await,
"object"
);
}
#[tokio::test]
async fn iframe_cross_realm_nav_stealth() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = browser_oxide::Page::with_profile(
"<!DOCTYPE html><html><head></head><body></body></html>",
"https://example.com/",
profile,
)
.await
.unwrap();
let result = page
.evaluate(r#"
(function() {
var iframe = document.createElement('iframe');
iframe.setAttribute('srcdoc', '<head></head><body></body>');
document.body.appendChild(iframe);
var cw = iframe.contentWindow;
if (!cw) return 'no_cw';
var navType = typeof cw.navigator;
var wd = 'threw';
try { wd = cw.navigator ? cw.navigator.webdriver : 'null_nav'; } catch(e) { wd = 'ERR:' + e.message; }
var aw = 'threw';
try { aw = cw.screen ? cw.screen.availWidth : 'null_scr'; } catch(e) { aw = 'ERR:' + e.message; }
var names = Object.getOwnPropertyNames(cw);
var hasNav = names.indexOf('navigator') >= 0;
var hasScr = names.indexOf('screen') >= 0;
return JSON.stringify({navType, wd, aw, hasNav, hasScr, total: names.length});
})()
"#)
.unwrap_or_else(|e| format!("ERROR: {e}"));
println!("iframe_cross_realm_nav_stealth: {}", result);
assert!(
result.contains("\"navType\":\"object\""),
"cw.navigator must be object with stealth profile, got: {result}"
);
assert!(
!result.contains("\"wd\":true"),
"cw.navigator.webdriver must not be true, got: {result}"
);
}
#[tokio::test]
async fn iframe_inner_realm_nav_access() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = browser_oxide::Page::with_profile(
"<!DOCTYPE html><html><body></body></html>",
"https://example.com/",
profile,
)
.await
.unwrap();
let result = page
.evaluate(
r#"
(function() {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var cw = iframe.contentWindow;
if (!cw) return 'no_cw';
if (!cw.Function) return 'no_fn:' + typeof cw.Function;
var innerNav = 'threw';
try { innerNav = new cw.Function('return typeof navigator')(); }
catch(e) { innerNav = 'err:' + e.message; }
var innerDpr = 'threw';
try { innerDpr = new cw.Function('return devicePixelRatio')(); }
catch(e) { innerDpr = 'err:' + e.message; }
var innerScreen = 'threw';
try { innerScreen = new cw.Function('return typeof screen')(); }
catch(e) { innerScreen = 'err:' + e.message; }
return JSON.stringify({innerNav, innerDpr, innerScreen});
})()
"#,
)
.unwrap_or_else(|e| format!("ERROR: {e}"));
println!("iframe_inner_realm_nav_access: {}", result);
assert!(
result.contains("\"innerNav\":\"object\""),
"navigator must be object inside realm, got: {result}"
);
assert!(
result.contains("\"innerScreen\":\"object\""),
"screen must be object inside realm, got: {result}"
);
}
#[tokio::test]
async fn iframe_child_realm_dpr_is_accessor() {
let result = check(r#"
(function() {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var cw = iframe.contentWindow;
if (!cw) return 'no_cw';
if (!cw.Function) return 'no_fn';
// Check descriptor from inside the child realm
var desc = new cw.Function(
'return JSON.stringify((function(d){return d?{hasGet:!!d.get,hasValue:"value" in d}:{missing:true};})(Object.getOwnPropertyDescriptor(globalThis,"devicePixelRatio")))'
)();
return desc || 'null';
})()
"#).await;
println!("iframe_child_realm_dpr_is_accessor: {}", result);
assert!(
result.contains("\"hasGet\":true"),
"child realm dpr must be accessor with getter, got: {result}"
);
}
#[tokio::test]
async fn iframe_cross_realm_write_read() {
let result = check(
r#"
(function() {
var f = document.createElement('iframe');
document.body.appendChild(f);
var cw = f.contentWindow;
if (!cw) return 'no_cw';
// Write from parent context to child realm
cw.__testSentinel = 'hello';
// Read back from parent context
var readBack = cw.__testSentinel;
// Also write a function
var fn1 = function sentinel() { return 42; };
cw.__sentinelFn = fn1;
var fn1Back = cw.__sentinelFn;
return JSON.stringify({
writeRead: readBack,
fnType: typeof fn1Back,
fnCall: fn1Back ? fn1Back() : 'no_fn',
navType: typeof cw.navigator,
wd: cw.navigator ? cw.navigator.webdriver : 'null_nav'
});
})()
"#,
)
.await;
println!("iframe_cross_realm_write_read: {}", result);
assert!(
result.contains("\"writeRead\":\"hello\""),
"cross-realm write-read must work, got: {result}"
);
assert!(
result.contains("\"fnType\":\"function\""),
"cross-realm fn write-read must work, got: {result}"
);
}
#[tokio::test]
async fn iframe_content_document() {
assert_eq!(
check("typeof document.createElement('iframe').contentDocument").await,
"object"
);
}
#[tokio::test]
async fn k_no_script_id_navigator_is_instance() {
assert_eq!(check("navigator instanceof Navigator").await, "true");
}
#[tokio::test]
async fn k_no_script_id_navigator_proto_is_navigator_prototype() {
assert_eq!(
check("Object.getPrototypeOf(navigator) === Navigator.prototype").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_user_agent_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(navigator, 'userAgent') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_user_agent_on_prototype() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent') !== undefined")
.await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_user_agent_prototype_is_accessor() {
assert_eq!(
check(
"typeof Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent').get === 'function'"
)
.await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_webdriver_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(navigator, 'webdriver') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_webdriver_on_prototype_returns_false() {
assert_eq!(check("navigator.webdriver === false").await, "true");
}
#[tokio::test]
async fn k_no_script_id_plugins_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(navigator, 'plugins') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_mime_types_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(navigator, 'mimeTypes') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_user_agent_data_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(navigator, 'userAgentData') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_gpu_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(navigator, 'gpu') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_scheduling_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(navigator, 'scheduling') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_get_user_media_on_prototype() {
assert_eq!(
check("Navigator.prototype.hasOwnProperty('getUserMedia')").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_navigator_has_no_own_properties() {
assert_eq!(
check("Object.getOwnPropertyNames(navigator).length").await,
"0"
);
}
#[tokio::test]
async fn k_no_script_id_user_agent_getter_tostring_masked() {
assert_eq!(
check(
"Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent').get.toString().includes('[native code]')"
)
.await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_screen_is_instance_of_screen() {
assert_eq!(check("screen instanceof Screen").await, "true");
}
#[tokio::test]
async fn k_no_script_id_screen_width_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(screen, 'width') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_screen_has_no_own_properties() {
assert_eq!(
check("Object.getOwnPropertyNames(screen).length").await,
"0"
);
}
#[tokio::test]
async fn k_no_script_id_history_is_instance_of_history() {
assert_eq!(check("history instanceof History").await, "true");
}
#[tokio::test]
async fn k_no_script_id_history_length_not_on_instance() {
assert_eq!(
check("Object.getOwnPropertyDescriptor(history, 'length') === undefined").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_history_push_state_on_prototype() {
assert_eq!(
check("History.prototype.hasOwnProperty('pushState')").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_speech_synthesis_is_instance() {
assert_eq!(
check("speechSynthesis instanceof SpeechSynthesis").await,
"true"
);
}
#[tokio::test]
async fn k_no_script_id_custom_elements_is_instance() {
assert_eq!(
check("customElements instanceof CustomElementRegistry").await,
"true"
);
}
#[tokio::test]
async fn text_encoder_encoding_is_utf8() {
assert_eq!(check("new TextEncoder().encoding").await, "utf-8");
}
#[tokio::test]
async fn text_encoder_encode_into_exists() {
assert_eq!(
check("typeof TextEncoder.prototype.encodeInto").await,
"function"
);
}
#[tokio::test]
async fn text_encoder_tostring_masked_as_native() {
assert_eq!(
check("TextEncoder.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn text_encoder_encode_tostring_masked_as_native() {
assert_eq!(
check("TextEncoder.prototype.encode.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn text_encoder_encoding_is_accessor_on_prototype() {
assert_eq!(
check(
"typeof Object.getOwnPropertyDescriptor(TextEncoder.prototype, 'encoding').get === 'function'"
)
.await,
"true"
);
}
#[tokio::test]
async fn text_encoder_prototype_has_expected_props() {
assert_eq!(
check("JSON.stringify(Object.getOwnPropertyNames(TextEncoder.prototype).sort())").await,
"[\"constructor\",\"encode\",\"encodeInto\",\"encoding\"]"
);
}
#[tokio::test]
async fn text_encoder_encode_roundtrip() {
assert_eq!(
check("JSON.stringify(Array.from(new TextEncoder().encode('ABC')))").await,
"[65,66,67]"
);
}
#[tokio::test]
async fn text_encoder_encode_into_writes_bytes() {
assert_eq!(
check(
"(() => { const b = new Uint8Array(4); const r = new TextEncoder().encodeInto('Hi', b); return JSON.stringify({r, b: Array.from(b)}); })()"
)
.await,
"{\"r\":{\"read\":2,\"written\":2},\"b\":[72,105,0,0]}"
);
}
#[tokio::test]
async fn text_decoder_encoding_is_utf8() {
assert_eq!(check("new TextDecoder().encoding").await, "utf-8");
}
#[tokio::test]
async fn text_decoder_decodes_utf8() {
assert_eq!(
check("new TextDecoder().decode(new Uint8Array([72, 105]))").await,
"Hi"
);
}
#[tokio::test]
async fn chrome_load_times_is_native() {
assert_eq!(
check("chrome.loadTimes.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn chrome_csi_is_native() {
assert_eq!(
check("chrome.csi.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_nav_getbattery_native() {
assert_eq!(
check_secure(
"Function.prototype.toString.call(navigator.getBattery).includes('[native code]')"
)
.await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_speech_getvoices_native() {
assert_eq!(
check(
"Function.prototype.toString.call(speechSynthesis.getVoices).includes('[native code]')"
)
.await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_permissions_query_native() {
assert_eq!(
check("Function.prototype.toString.call(navigator.permissions.query).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_media_devices_enumerate_native() {
assert_eq!(
check_secure("Function.prototype.toString.call(navigator.mediaDevices.enumerateDevices).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_ua_getter_native() {
assert_eq!(
check("Function.prototype.toString.call(Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent').get).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_screen_width_getter_native() {
assert_eq!(
check("Function.prototype.toString.call(Object.getOwnPropertyDescriptor(Screen.prototype, 'width').get).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_chrome_loadtimes_native() {
assert_eq!(
check("Function.prototype.toString.call(chrome.loadTimes).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_rtc_create_offer_native() {
assert_eq!(
check("Function.prototype.toString.call(RTCPeerConnection.prototype.createOffer).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn fn_proto_tostring_itself_is_native() {
assert_eq!(
check("Function.prototype.toString.call(Function.prototype.toString).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn to_string_tag_document() {
assert_eq!(
check("Object.prototype.toString.call(document)").await,
"[object HTMLDocument]"
);
}
#[tokio::test]
async fn to_string_tag_html_body_element() {
let s = check(
"(() => { const b = document.createElement('body'); return Object.prototype.toString.call(b); })()",
)
.await;
assert_eq!(s, "[object HTMLBodyElement]");
}
#[tokio::test]
async fn to_string_tag_html_div_element() {
let s = check(
"(() => { const d = document.createElement('div'); return Object.prototype.toString.call(d); })()",
)
.await;
assert_eq!(s, "[object HTMLDivElement]");
}
#[tokio::test]
async fn to_string_tag_html_canvas_element() {
let s = check(
"(() => { const c = document.createElement('canvas'); return Object.prototype.toString.call(c); })()",
)
.await;
assert_eq!(s, "[object HTMLCanvasElement]");
}
#[tokio::test]
async fn to_string_tag_canvas_rendering_context_2d() {
let s = check(
"(() => { const ctx = document.createElement('canvas').getContext('2d'); return Object.prototype.toString.call(ctx); })()",
)
.await;
assert_eq!(s, "[object CanvasRenderingContext2D]");
}
#[tokio::test]
async fn to_string_tag_webgl_rendering_context() {
let s = check(
"(() => { const ctx = document.createElement('canvas').getContext('webgl'); return Object.prototype.toString.call(ctx); })()",
)
.await;
assert_eq!(s, "[object WebGLRenderingContext]");
}
#[tokio::test]
async fn to_string_tag_element_prototype() {
assert_eq!(
check("Object.prototype.toString.call(Element.prototype)").await,
"[object Element]"
);
}
#[tokio::test]
async fn to_string_tag_node_prototype() {
assert_eq!(
check("Object.prototype.toString.call(Node.prototype)").await,
"[object Node]"
);
}
#[tokio::test]
async fn to_string_tag_event_target_prototype() {
assert_eq!(
check("Object.prototype.toString.call(EventTarget.prototype)").await,
"[object EventTarget]"
);
}
#[tokio::test]
async fn worker_ua_matches_window() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_windows();
let win_ua = profile.user_agent.to_string();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
page.evaluate(
r#"window.__wua = null;
const src = 'self.postMessage(navigator.userAgent);';
const w = new Worker(URL.createObjectURL(new Blob([src],{type:'text/javascript'})));
w.onmessage = e => { window.__wua = e.data; w.terminate(); };"#,
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(500))
.await
.ok();
let worker_ua = page.evaluate("window.__wua").unwrap();
assert_eq!(worker_ua, win_ua, "Worker UA should match window UA");
}
#[tokio::test]
async fn worker_platform_matches_window() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_windows();
let expected = profile.platform.clone();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
page.evaluate(
r#"window.__wplat = null;
const src = 'self.postMessage(navigator.platform);';
const w = new Worker(URL.createObjectURL(new Blob([src],{type:'text/javascript'})));
w.onmessage = e => { window.__wplat = e.data; w.terminate(); };"#,
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(500))
.await
.ok();
assert_eq!(page.evaluate("window.__wplat").unwrap(), expected);
}
#[tokio::test]
async fn worker_hardware_concurrency_matches_window() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_windows();
let expected = profile.cpu_cores.to_string();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
page.evaluate(
r#"window.__whc = null;
const src = 'self.postMessage(String(navigator.hardwareConcurrency));';
const w = new Worker(URL.createObjectURL(new Blob([src],{type:'text/javascript'})));
w.onmessage = e => { window.__whc = e.data; w.terminate(); };"#,
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(500))
.await
.ok();
assert_eq!(page.evaluate("window.__whc").unwrap(), expected);
}
#[tokio::test]
async fn screen_avail_top_macos_is_33() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_macos();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
assert_eq!(page.evaluate("screen.availTop").unwrap(), "33");
}
#[tokio::test]
async fn screen_avail_top_windows_is_0() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_windows();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
assert_eq!(page.evaluate("screen.availTop").unwrap(), "0");
}
#[tokio::test]
async fn screen_avail_top_linux_is_0() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_linux();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
assert_eq!(page.evaluate("screen.availTop").unwrap(), "0");
}
#[tokio::test]
async fn nav_webdriver_typeof_boolean() {
assert_eq!(check("typeof navigator.webdriver").await, "boolean");
}
#[tokio::test]
async fn nav_webdriver_in_operator_true() {
assert_eq!(check("'webdriver' in navigator").await, "true");
}
#[tokio::test]
async fn nav_languages_is_frozen() {
assert_eq!(check("Object.isFrozen(navigator.languages)").await, "true");
}
#[tokio::test]
async fn nav_constructor_name_is_navigator() {
assert_eq!(check("navigator.constructor.name").await, "Navigator");
}
#[tokio::test]
async fn nav_permissions_query_is_native() {
assert_eq!(
check("navigator.permissions.query.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn nav_keyboard_exists() {
assert_eq!(check_secure("typeof navigator.keyboard").await, "object");
}
#[tokio::test]
async fn nav_keyboard_getlayoutmap_is_function() {
assert_eq!(
check_secure("typeof navigator.keyboard.getLayoutMap").await,
"function"
);
}
#[tokio::test]
async fn nav_keyboard_getlayoutmap_returns_promise() {
assert_eq!(
check_secure("navigator.keyboard.getLayoutMap() instanceof Promise").await,
"true"
);
}
#[tokio::test]
async fn nav_keyboard_getlayoutmap_resolves_to_map() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
page.evaluate(
"window.__r = null; navigator.keyboard.getLayoutMap().then(m => { window.__r = m; });",
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
assert_eq!(page.evaluate("window.__r !== null").unwrap(), "true");
}
#[tokio::test]
async fn nav_keyboard_getlayoutmap_has_entries() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
page.evaluate(
"window.__r = 0; navigator.keyboard.getLayoutMap().then(m => { window.__r = m.size; });",
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
let size: usize = page.evaluate("window.__r").unwrap().parse().unwrap_or(0);
assert!(
size > 0,
"KeyboardLayoutMap should have entries, got {size}"
);
}
#[tokio::test]
async fn nav_keyboard_getlayoutmap_has_keya() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
page.evaluate("window.__r = false; navigator.keyboard.getLayoutMap().then(m => { window.__r = m.has('KeyA'); });").unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
assert_eq!(page.evaluate("window.__r").unwrap(), "true");
}
#[tokio::test]
async fn nav_keyboard_getlayoutmap_is_native() {
assert_eq!(
check_secure("Function.prototype.toString.call(navigator.keyboard.getLayoutMap).includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn media_key_widevine_resolves_on_windows() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_windows();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
page.evaluate(
"window.__r = null; navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{initDataTypes:['cenc'],videoCapabilities:[{contentType:'video/mp4;codecs=\"avc1.42E01E\"'}]}]).then(a => { window.__r = 'ok'; }).catch(e => { window.__r = 'err:' + e.name; });"
).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
assert_eq!(page.evaluate("window.__r").unwrap(), "ok");
}
#[tokio::test]
async fn media_key_widevine_resolves_on_macos() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_macos();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
page.evaluate(
"window.__r = null; navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{initDataTypes:['cenc'],videoCapabilities:[{contentType:'video/mp4;codecs=\"avc1.42E01E\"'}]}]).then(a => { window.__r = 'ok'; }).catch(e => { window.__r = 'err:' + e.name; });"
).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
assert_eq!(page.evaluate("window.__r").unwrap(), "ok");
}
#[tokio::test]
async fn media_key_clearkey_always_resolves() {
let mut page = Page::from_html(&html(""), None::<browser_oxide::stealth::StealthProfile>)
.await
.unwrap();
page.evaluate(
"window.__r = null; navigator.requestMediaKeySystemAccess('org.w3.clearkey', [{initDataTypes:['keyids'],videoCapabilities:[{contentType:'video/webm;codecs=\"vp8\"'}]}]).then(a => { window.__r = 'ok'; }).catch(e => { window.__r = 'err:' + e.name; });"
).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
assert_eq!(page.evaluate("window.__r").unwrap(), "ok");
}
#[tokio::test]
async fn media_key_access_key_system_is_string() {
use browser_oxide::stealth::presets;
let profile = presets::chrome_148_windows();
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
page.evaluate(
"window.__r = null; navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{initDataTypes:['cenc'],videoCapabilities:[{contentType:'video/mp4;codecs=\"avc1.42E01E\"'}]}]).then(a => { window.__r = typeof a.keySystem; }).catch(() => { window.__r = 'err'; });"
).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
assert_eq!(page.evaluate("window.__r").unwrap(), "string");
}
#[tokio::test]
async fn crypto_instanceof_crypto() {
assert_eq!(check("crypto instanceof Crypto").await, "true");
}
#[tokio::test]
async fn crypto_subtle_instanceof_subtle_crypto() {
assert_eq!(
check_secure("crypto.subtle instanceof SubtleCrypto").await,
"true"
);
}
#[tokio::test]
async fn crypto_has_no_own_properties() {
assert_eq!(
check("Object.getOwnPropertyNames(crypto).length").await,
"0"
);
}
#[tokio::test]
async fn crypto_get_random_values_native() {
assert_eq!(
check("crypto.getRandomValues.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn crypto_random_uuid_native() {
assert_eq!(
check_secure("crypto.randomUUID.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn crypto_random_uuid_format() {
assert_eq!(
check_secure("/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(crypto.randomUUID())").await,
"true"
);
}
#[tokio::test]
async fn crypto_get_random_values_returns_non_zero() {
assert_eq!(
check(
"(() => { const a = new Uint32Array(4); crypto.getRandomValues(a); return a.some(v => v !== 0); })()"
)
.await,
"true"
);
}
#[tokio::test]
async fn crypto_subtle_digest_exists() {
assert_eq!(
check_secure("typeof crypto.subtle.digest").await,
"function"
);
}
#[tokio::test]
async fn crypto_subtle_digest_is_native() {
assert_eq!(
check_secure("crypto.subtle.digest.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn crypto_subtle_digest_actually_works() {
use browser_oxide::Page;
use std::time::Duration;
let mut page = Page::with_profile(
"<!DOCTYPE html><html><head></head><body></body></html>",
"https://example.com/",
browser_oxide::stealth::presets::chrome_148_windows(),
)
.await
.unwrap();
let _ = page.event_loop().execute_and_run(r#"
(async function() {
try {
var data = new TextEncoder().encode('hello world');
var hash = await crypto.subtle.digest('SHA-256', data);
var arr = new Uint8Array(hash);
globalThis.__cryptoTestResult = Array.from(arr).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
} catch(e) { globalThis.__cryptoTestResult = 'err:' + e.message; }
})();
"#, Duration::from_secs(5)).await;
let result = page
.event_loop()
.execute_script("globalThis.__cryptoTestResult || 'not-set'")
.unwrap_or_default();
assert_eq!(
result.len(),
64,
"crypto.subtle.digest should return 64-char SHA-256 hex hash, got: {}",
result
);
assert!(
!result.starts_with("err:"),
"crypto.subtle.digest failed: {}",
result
);
assert!(
!result.starts_with("not-set"),
"crypto.subtle.digest never ran"
);
}
#[tokio::test]
async fn performance_instanceof_performance() {
assert_eq!(check("performance instanceof Performance").await, "true");
}
#[tokio::test]
async fn performance_has_no_own_properties() {
assert_eq!(
check("Object.getOwnPropertyNames(performance).length").await,
"0"
);
}
#[tokio::test]
async fn performance_memory_on_prototype() {
assert_eq!(
check("Performance.prototype.hasOwnProperty('memory')").await,
"true"
);
}
#[tokio::test]
async fn performance_get_entries_is_native() {
assert_eq!(
check("performance.getEntries.toString().includes('[native code]')").await,
"true"
);
}
async fn await_promise_secure(js: &str, timeout_ms: u64) -> String {
await_promise_inner(js, timeout_ms, None, true).await
}
async fn await_promise_with_profile_secure(
js: &str,
timeout_ms: u64,
profile: browser_oxide::stealth::StealthProfile,
) -> String {
await_promise_inner(js, timeout_ms, Some(profile), true).await
}
async fn await_promise_inner(
js: &str,
timeout_ms: u64,
profile: Option<browser_oxide::stealth::StealthProfile>,
secure: bool,
) -> String {
let mut page = if secure {
Page::from_html_with_url(&html(""), "https://example.com/", profile)
.await
.unwrap()
} else {
Page::from_html(&html(""), profile).await.unwrap()
};
let stash = format!(
"window.__r = null; window.__rej = null; \
({js}).then(v => {{ window.__r = String(v); }}, \
e => {{ window.__rej = (e && e.name) ? e.name : (e && e.constructor ? e.constructor.name : String(e)); }});"
);
page.evaluate(&stash).unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(timeout_ms))
.await
.ok();
page.evaluate(
"(window.__rej !== null) ? ('rej:' + window.__rej) \
: (window.__r !== null) ? ('ok:' + window.__r) \
: 'pending'",
)
.unwrap()
}
#[tokio::test]
async fn webauthn_public_key_credential_exists() {
assert_eq!(check_secure("typeof PublicKeyCredential").await, "function");
}
#[tokio::test]
async fn webauthn_public_key_credential_constructor_throws() {
assert_eq!(
check_secure("(() => { try { new PublicKeyCredential(); return 'no-throw'; } catch (e) { return e.message; } })()")
.await,
"Illegal constructor"
);
}
#[tokio::test]
async fn webauthn_authenticator_response_classes_exist() {
assert_eq!(
check_secure("typeof AuthenticatorResponse").await,
"function"
);
assert_eq!(
check_secure("typeof AuthenticatorAttestationResponse").await,
"function"
);
assert_eq!(
check_secure("typeof AuthenticatorAssertionResponse").await,
"function"
);
}
#[tokio::test]
async fn webauthn_attestation_extends_response() {
assert_eq!(
check_secure("AuthenticatorAttestationResponse.prototype instanceof AuthenticatorResponse")
.await,
"true"
);
}
#[tokio::test]
async fn webauthn_isuvpa_returns_promise() {
assert_eq!(
check_secure("PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() instanceof Promise").await,
"true"
);
}
#[tokio::test]
async fn webauthn_iscma_returns_promise() {
assert_eq!(
check_secure("PublicKeyCredential.isConditionalMediationAvailable() instanceof Promise")
.await,
"true"
);
}
#[tokio::test]
async fn webauthn_isuvpa_true_on_windows_profile() {
let profile = browser_oxide::stealth::presets::chrome_148_windows();
assert_eq!(
await_promise_with_profile_secure(
"PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()",
50,
profile,
)
.await,
"ok:true"
);
}
#[tokio::test]
async fn webauthn_isuvpa_true_on_macos_profile() {
let profile = browser_oxide::stealth::presets::chrome_148_macos();
assert_eq!(
await_promise_with_profile_secure(
"PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()",
50,
profile,
)
.await,
"ok:true"
);
}
#[tokio::test]
async fn webauthn_isuvpa_false_on_linux_profile() {
let profile = browser_oxide::stealth::presets::chrome_148_linux();
assert_eq!(
await_promise_with_profile_secure(
"PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()",
50,
profile,
)
.await,
"ok:false"
);
}
#[tokio::test]
async fn webauthn_get_client_capabilities_shape() {
assert_eq!(
await_promise_secure(
"PublicKeyCredential.getClientCapabilities().then(c => \
typeof c.userVerifyingPlatformAuthenticator === 'boolean' && \
typeof c.conditionalGet === 'boolean' && \
typeof c.hybridTransport === 'boolean')",
50,
)
.await,
"ok:true"
);
}
#[tokio::test]
async fn webauthn_isuvpa_is_native() {
assert_eq!(
check_secure("PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn webauthn_credentials_create_is_native() {
assert_eq!(
check_secure("navigator.credentials.create.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn webauthn_credentials_create_rejects_with_not_allowed() {
assert_eq!(
await_promise_secure("navigator.credentials.create({publicKey:{}})", 250,).await,
"rej:NotAllowedError"
);
}
#[tokio::test]
async fn webauthn_credentials_get_publickey_rejects() {
assert_eq!(
await_promise_secure("navigator.credentials.get({publicKey:{}})", 250,).await,
"rej:NotAllowedError"
);
}
#[tokio::test]
async fn webauthn_credentials_create_no_args_rejects_typeerror() {
assert_eq!(
await_promise_secure("navigator.credentials.create()", 50,).await,
"rej:TypeError"
);
}
#[tokio::test]
async fn webauthn_navigator_credentials_is_credentials_container() {
assert_eq!(
check_secure("navigator.credentials instanceof CredentialsContainer").await,
"true"
);
}
#[tokio::test]
async fn fedcm_identity_credential_exists() {
assert_eq!(check_secure("typeof IdentityCredential").await, "function");
}
#[tokio::test]
async fn fedcm_identity_provider_exists() {
assert_eq!(check_secure("typeof IdentityProvider").await, "function");
}
#[tokio::test]
async fn fedcm_identity_credential_constructor_throws() {
assert_eq!(
check_secure("(() => { try { new IdentityCredential(); return 'no-throw'; } catch (e) { return e.message; } })()")
.await,
"Illegal constructor"
);
}
#[tokio::test]
async fn fedcm_identity_provider_get_user_info_rejects() {
assert_eq!(
await_promise_secure(
"IdentityProvider.getUserInfo({configURL:'https://x/cfg.json',clientId:'a'})",
50,
)
.await,
"rej:NotAllowedError"
);
}
async fn webgl_check(js: &str) -> String {
let wrapped = format!(
"(() => {{ \
const c = document.createElement('canvas'); \
c.width = 256; c.height = 256; \
const gl = c.getContext('webgl'); \
if (!gl) return 'no-context'; \
return ({js}); \
}})()"
);
check(&wrapped).await
}
async fn webgl_check_with_profile(
js: &str,
profile: browser_oxide::stealth::StealthProfile,
) -> String {
let wrapped = format!(
"(() => {{ \
const c = document.createElement('canvas'); \
c.width = 256; c.height = 256; \
const gl = c.getContext('webgl'); \
if (!gl) return 'no-context'; \
return ({js}); \
}})()"
);
let mut page = Page::from_html(&html(""), Some(profile)).await.unwrap();
page.evaluate(&wrapped)
.unwrap_or_else(|e| format!("ERROR: {e}"))
}
#[tokio::test]
async fn webgl_rendering_context_class_exists() {
assert_eq!(check("typeof WebGLRenderingContext").await, "function");
assert_eq!(check("typeof WebGL2RenderingContext").await, "function");
}
#[tokio::test]
async fn webgl_get_context_returns_object() {
assert_eq!(webgl_check("typeof gl").await, "object");
assert_eq!(webgl_check("gl !== null").await, "true");
}
#[tokio::test]
async fn webgl_vendor_renderer_strings_non_empty() {
assert_eq!(
webgl_check(
"typeof gl.getParameter(0x1F00) === 'string' && gl.getParameter(0x1F00).length > 0"
)
.await,
"true"
);
assert_eq!(
webgl_check(
"typeof gl.getParameter(0x1F01) === 'string' && gl.getParameter(0x1F01).length > 0"
)
.await,
"true"
);
}
#[tokio::test]
async fn webgl_unmasked_vendor_renderer_per_profile() {
let win_renderer = webgl_check_with_profile(
"gl.getParameter(0x9246)", browser_oxide::stealth::presets::chrome_148_windows(),
)
.await;
assert!(
win_renderer.contains("NVIDIA"),
"Win UNMASKED_RENDERER should mention NVIDIA, got {win_renderer}"
);
let mac_renderer = webgl_check_with_profile(
"gl.getParameter(0x9246)",
browser_oxide::stealth::presets::chrome_148_macos(),
)
.await;
assert!(
mac_renderer.contains("Apple"),
"Mac UNMASKED_RENDERER should mention Apple, got {mac_renderer}"
);
let linux_renderer = webgl_check_with_profile(
"gl.getParameter(0x9246)",
browser_oxide::stealth::presets::chrome_148_linux(),
)
.await;
assert!(
linux_renderer.contains("Intel"),
"Linux UNMASKED_RENDERER should mention Intel, got {linux_renderer}"
);
}
#[tokio::test]
async fn webgl_max_texture_size_at_least_chrome_minimum() {
assert_eq!(
webgl_check("gl.getParameter(0x0D33) >= 8192").await, "true"
);
}
#[tokio::test]
async fn webgl_get_supported_extensions_non_empty() {
assert_eq!(
webgl_check(
"Array.isArray(gl.getSupportedExtensions()) && gl.getSupportedExtensions().length > 5"
)
.await,
"true"
);
}
#[tokio::test]
async fn webgl_supported_extensions_contain_debug_renderer_info() {
assert_eq!(
webgl_check("gl.getSupportedExtensions().includes('WEBGL_debug_renderer_info')").await,
"true"
);
}
#[tokio::test]
async fn webgl_get_extension_debug_renderer_info_returns_constants() {
assert_eq!(
webgl_check(
"(() => { \
const ext = gl.getExtension('WEBGL_debug_renderer_info'); \
return ext !== null \
&& ext.UNMASKED_VENDOR_WEBGL === 0x9245 \
&& ext.UNMASKED_RENDERER_WEBGL === 0x9246; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn webgl_get_shader_precision_format_high_float() {
assert_eq!(
webgl_check(
"(() => { \
const p = gl.getShaderPrecisionFormat(0x8B30, 0x8DF2); \
return p !== null \
&& p.rangeMin === 127 \
&& p.rangeMax === 127 \
&& p.precision === 23; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn webgl_get_context_attributes_chrome_defaults() {
assert_eq!(
webgl_check(
"(() => { \
const a = gl.getContextAttributes(); \
return a.alpha === true \
&& a.antialias === true \
&& a.depth === true \
&& a.failIfMajorPerformanceCaveat === false \
&& a.premultipliedAlpha === true \
&& a.preserveDrawingBuffer === false \
&& a.stencil === false; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn webgl_is_context_lost_returns_false() {
assert_eq!(webgl_check("gl.isContextLost()").await, "false");
}
#[tokio::test]
async fn webgl_get_error_returns_zero_initially() {
assert_eq!(webgl_check("gl.getError()").await, "0");
}
#[tokio::test]
async fn webgl_max_viewport_dims_returns_array_of_two() {
assert_eq!(
webgl_check(
"(() => { \
const v = gl.getParameter(0x0D3A); \
return Array.isArray(v) && v.length === 2 && v[0] > 0 && v[1] > 0; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn webgl2_context_returns_object() {
assert_eq!(
check(
"(() => { \
const c = document.createElement('canvas'); \
const gl = c.getContext('webgl2'); \
return typeof gl; \
})()"
)
.await,
"object"
);
}
#[tokio::test]
async fn webgl_extensions_differ_per_profile_apple() {
let exts = webgl_check_with_profile(
"JSON.stringify(gl.getSupportedExtensions())",
browser_oxide::stealth::presets::chrome_148_macos(),
)
.await;
assert!(
exts.contains("WEBGL_compressed_texture_astc"),
"Apple profile should expose ASTC; got {exts}"
);
}
#[tokio::test]
async fn audio_context_exists() {
assert_eq!(check("typeof AudioContext").await, "function");
assert_eq!(check("typeof OfflineAudioContext").await, "function");
}
#[tokio::test]
async fn analyser_has_chrome_default_props() {
assert_eq!(
check(
"(() => { \
const ctx = new AudioContext(); \
const a = ctx.createAnalyser(); \
return a.fftSize === 2048 \
&& a.frequencyBinCount === 1024 \
&& a.smoothingTimeConstant === 0.8 \
&& a.minDecibels === -100 \
&& a.maxDecibels === -30; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn analyser_get_float_frequency_data_returns_min_db_for_silence() {
assert_eq!(
check(
"(() => { \
const ctx = new AudioContext(); \
const a = ctx.createAnalyser(); \
const f = new Float32Array(a.frequencyBinCount); \
a.getFloatFrequencyData(f); \
return f.every(v => v === a.minDecibels); \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn biquad_get_frequency_response_unity_at_dc_for_lowpass() {
assert_eq!(
check(
"(() => { \
const ctx = new AudioContext(); \
const f = ctx.createBiquadFilter(); \
f.type = 'lowpass'; f.frequency.value = 1000; f.Q.value = 0.7071; \
const freqs = new Float32Array([0]); \
const mag = new Float32Array(1); \
const phase = new Float32Array(1); \
f.getFrequencyResponse(freqs, mag, phase); \
return Math.abs(mag[0] - 1) < 0.01; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn biquad_get_frequency_response_blocks_dc_for_highpass() {
assert_eq!(
check(
"(() => { \
const ctx = new AudioContext(); \
const f = ctx.createBiquadFilter(); \
f.type = 'highpass'; f.frequency.value = 1000; f.Q.value = 0.7071; \
const freqs = new Float32Array([0]); \
const mag = new Float32Array(1); \
const phase = new Float32Array(1); \
f.getFrequencyResponse(freqs, mag, phase); \
return mag[0] < 0.01; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn biquad_get_frequency_response_writes_n_values() {
assert_eq!(
check(
"(() => { \
const ctx = new AudioContext(); \
const f = ctx.createBiquadFilter(); \
const freqs = new Float32Array([100, 500, 1000, 5000, 20000]); \
const mag = new Float32Array(5); \
const phase = new Float32Array(5); \
f.getFrequencyResponse(freqs, mag, phase); \
return mag.every(v => Number.isFinite(v) && v >= 0) \
&& phase.every(v => Number.isFinite(v)); \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn perf_now_returns_number() {
assert_eq!(check("typeof performance.now()").await, "number");
}
#[tokio::test]
async fn perf_now_is_native() {
assert_eq!(
check("performance.now.toString().includes('[native code]')").await,
"true"
);
}
#[tokio::test]
async fn perf_now_is_finite_non_negative() {
assert_eq!(
check("(() => { const t = performance.now(); return Number.isFinite(t) && t >= 0; })()")
.await,
"true"
);
}
#[tokio::test]
async fn perf_now_hot_loop_produces_distinct_values() {
assert_eq!(
check(
"(() => { \
const xs = []; \
for (let i = 0; i < 500; i++) xs.push(performance.now()); \
return new Set(xs).size > 10; \
})()"
)
.await,
"true"
);
}
#[tokio::test]
async fn perf_now_is_strictly_monotonic() {
assert_eq!(
check(
"(() => { \
const xs = []; \
for (let i = 0; i < 1000; i++) xs.push(performance.now()); \
for (let i = 1; i < xs.length; i++) if (xs[i] < xs[i-1]) return false; \
return true; \
})()"
)
.await,
"true"
);
}
fn coi_check(cross_origin_isolated: bool, js: &str) -> String {
let dom = browser_oxide::html_parser::parse_html("<html><body></body></html>");
let mut rt = browser_oxide::js_runtime::BrowserJsRuntime::with_options(
dom,
browser_oxide::js_runtime::runtime::BrowserRuntimeOptions {
cross_origin_isolated,
..Default::default()
},
);
rt.execute_script(js, None)
.unwrap_or_else(|e| format!("ERROR: {e}"))
}
#[tokio::test]
async fn coi_default_is_false_via_page() {
assert_eq!(check("typeof crossOriginIsolated").await, "boolean");
assert_eq!(check("crossOriginIsolated").await, "false");
}
#[tokio::test]
async fn coi_true_when_runtime_constructed_isolated() {
assert_eq!(coi_check(true, "crossOriginIsolated"), "true");
}
#[tokio::test]
async fn coi_false_when_runtime_constructed_non_isolated() {
assert_eq!(coi_check(false, "crossOriginIsolated"), "false");
}
#[tokio::test]
async fn coi_property_descriptor_is_configurable_getter() {
assert_eq!(
coi_check(false, "Object.getOwnPropertyDescriptor(globalThis, 'crossOriginIsolated').get instanceof Function"),
"true"
);
}
#[tokio::test]
async fn sab_constructor_exists() {
assert_eq!(check("typeof SharedArrayBuffer").await, "undefined");
}
#[tokio::test]
#[ignore = "SAB only available with cross-origin isolation (COOP+COEP headers)"]
async fn sab_constructible_with_byte_length() {
assert_eq!(coi_check(true, "new SharedArrayBuffer(8).byteLength"), "8");
}
#[tokio::test]
#[ignore = "SAB only available with cross-origin isolation (COOP+COEP headers)"]
async fn sab_instance_is_shared_array_buffer() {
assert_eq!(
coi_check(
true,
"new SharedArrayBuffer(4) instanceof SharedArrayBuffer"
),
"true"
);
}
#[tokio::test]
async fn atomics_object_exists() {
assert_eq!(check("typeof Atomics").await, "object");
}
#[tokio::test]
async fn atomics_wait_and_notify_exist() {
assert_eq!(check("typeof Atomics.wait").await, "function");
assert_eq!(check("typeof Atomics.notify").await, "function");
}
#[tokio::test]
#[ignore = "SAB only available with cross-origin isolation (COOP+COEP headers)"]
async fn atomics_wait_returns_timed_out_synchronously() {
assert_eq!(
coi_check(
true,
"Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1)"
),
"timed-out"
);
}
#[tokio::test]
async fn fedcm_credentials_get_identity_rejects() {
assert_eq!(
await_promise_secure(
"navigator.credentials.get({ identity: { providers: [{ configURL: 'https://x/cfg.json', clientId: 'a' }] } })",
350,
)
.await,
"rej:NotAllowedError"
);
}
#[tokio::test]
async fn shim_recursion_proto_walk_no_access() {
let result = check(
r#"
(function() {
try {
let p = globalThis;
const seen = [];
let depth = 0;
while (p !== null && p !== undefined && depth < 30) {
// Guard against circular prototype (shouldn't happen, but just in case)
if (seen.indexOf(p) !== -1) return 'cycle_at_' + depth;
seen.push(p);
Reflect.ownKeys(p); // enumerate — don't access values
p = Object.getPrototypeOf(p);
depth++;
}
return 'walk_done_' + depth;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("walk_done_") || result.starts_with("cycle_at_"),
"proto walk should complete without crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_proto_walk_with_getters() {
let result = check(
r#"
(function() {
try {
let p = globalThis;
let depth = 0;
const seen = [];
while (p !== null && p !== undefined && depth < 30) {
if (seen.indexOf(p) !== -1) return 'cycle_at_' + depth;
seen.push(p);
for (const key of Reflect.ownKeys(p)) {
try {
const desc = Object.getOwnPropertyDescriptor(p, key);
if (!desc) continue;
// Invoke getter if present
if (typeof desc.get === 'function') {
try { desc.get.call(p); } catch(_) {}
}
// Call .toString() on any function value/getter
if (typeof desc.get === 'function') {
desc.get.toString();
}
if (typeof desc.value === 'function') {
desc.value.toString();
}
} catch(_) {}
}
p = Object.getPrototypeOf(p);
depth++;
}
return 'walk_done_' + depth;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("walk_done_") || result.starts_with("cycle_at_"),
"proto walk+getters should complete without crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_fn_proto_tostring_on_all() {
let result = check(
r#"
(function() {
try {
const seen = new Set();
let checked = 0;
let p = globalThis;
while (p) {
for (const key of Reflect.ownKeys(p)) {
try {
const desc = Object.getOwnPropertyDescriptor(p, key);
if (!desc) continue;
for (const fn of [desc.value, desc.get, desc.set]) {
if (typeof fn !== 'function' || seen.has(fn)) continue;
seen.add(fn);
Function.prototype.toString.call(fn);
checked++;
}
} catch(_) {}
}
p = Object.getPrototypeOf(p);
if (!p || p === Object.prototype) break;
}
return 'toString_ok_' + checked;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("toString_ok_"),
"Function.prototype.toString on all fns should not recurse, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_diag_weakset_globalthis() {
let result = check(
r#"
(function() {
try {
const ws = new WeakSet();
// Add globalThis and check if it's found
ws.add(globalThis);
const afterAdd = ws.has(globalThis);
// Add window (which === globalThis) and check
ws.add(window);
const afterAddWindow = ws.has(globalThis);
const afterAddGT = ws.has(window);
// Check inner global (prototype of globalThis)
const innerGlobal = Object.getPrototypeOf(globalThis);
const innerInSet = innerGlobal ? ws.has(innerGlobal) : null;
const innerIsGT = innerGlobal === globalThis;
// WeakMap test
const wm = new WeakMap();
wm.set(globalThis, 42);
const wmGet = wm.get(globalThis);
const wmGetWindow = wm.get(window);
return JSON.stringify({
afterAdd, afterAddWindow, afterAddGT,
innerInSet, innerIsGT,
wmGet, wmGetWindow,
windowIsGT: window === globalThis,
});
} catch(e) { return 'error: ' + e.message; }
})()
"#,
)
.await;
println!("weakset globalThis test: {result}");
assert!(
!result.starts_with("error:"),
"weakset test failed: {result}"
);
assert!(
result.contains("\"afterAdd\":true"),
"WeakSet.has(globalThis) after add must be true, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_diag_callsite() {
let result = check(
r#"
(function() {
try {
let frameInfo = null;
const origPrep = Error.prepareStackTrace;
Error.prepareStackTrace = function(err, frames) {
if (frames.length > 0 && frameInfo === null) {
const f = frames[0];
frameInfo = {
type: typeof f,
isFunc: typeof f === 'function',
hasToString: typeof f.toString === 'function',
toStringSrc: (() => {
try {
// Get toString without calling it (to avoid recursion)
const ts = Object.getOwnPropertyDescriptor(f, 'toString');
if (ts) return 'own:' + typeof ts.value;
// inherited?
let p = Object.getPrototypeOf(f);
while (p) {
const d = Object.getOwnPropertyDescriptor(p, 'toString');
if (d) {
const fn = d.value;
const isFnProtoTs = fn === Function.prototype.toString;
const isObjProtoTs = fn === Object.prototype.toString;
return 'inherited:fn=' + typeof fn +
',isFuncProtoToStr=' + isFnProtoTs +
',isObjProtoToStr=' + isObjProtoTs;
}
p = Object.getPrototypeOf(p);
}
return 'none';
} catch(e) { return 'err:' + e.message; }
})(),
protoChainLen: (() => {
let n = 0;
let p = f;
while (p && n < 20) { p = Object.getPrototypeOf(p); n++; }
return n;
})(),
isFuncProtoInChain: (() => {
let p = Object.getPrototypeOf(f);
while (p) {
if (p === Function.prototype) return true;
p = Object.getPrototypeOf(p);
}
return false;
})(),
};
}
// Call original (if any) or use default
if (origPrep) return origPrep(err, frames);
return undefined;
};
// Trigger a stack trace capture
try { throw new Error('test'); } catch(e) { void e.stack; }
Error.prepareStackTrace = origPrep;
return JSON.stringify(frameInfo);
} catch(e) { return 'error: ' + e.message; }
})()
"#,
)
.await;
println!("callsite frame info: {result}");
assert!(
!result.starts_with("error:"),
"callsite diag failed: {result}"
);
}
#[tokio::test]
async fn shim_recursion_diag_global_proto() {
let result = check(
r#"
(function() {
try {
const gt = globalThis;
const p1 = Object.getPrototypeOf(gt);
const info = {
p1_null: p1 === null,
p1_is_gt: p1 === gt,
p1_is_objproto: p1 === Object.prototype,
p1_type: typeof p1,
p1_keys: p1 ? Object.getOwnPropertyNames(p1).length : -1,
};
if (p1 && p1 !== null) {
const p2 = Object.getPrototypeOf(p1);
info.p2_null = p2 === null;
info.p2_is_gt = p2 === gt;
info.p2_is_objproto = p2 === Object.prototype;
info.p2_is_p1 = p2 === p1;
}
if (p1) {
info.p1_own_keys = Object.getOwnPropertyNames(p1);
info.p1_own_symbols = Object.getOwnPropertySymbols(p1).map(s => s.toString());
// Check the constructor property
const ctor = p1.constructor;
info.p1_ctor_is_gt = ctor === gt;
info.p1_ctor_is_func = typeof ctor === 'function';
info.p1_ctor_is_object_ctor = ctor === Object;
info.p1_ctor_type = typeof ctor;
if (ctor && typeof ctor === 'function') {
info.p1_ctor_name = ctor.name;
}
// Check the descriptor for constructor
const desc = Object.getOwnPropertyDescriptor(p1, 'constructor');
info.p1_ctor_desc_type = desc ? (desc.get ? 'getter' : 'value') : 'none';
// Important: does accessing constructor cause recursion?
// Check Object.getPrototypeOf(p1.constructor) if it's a function
if (ctor && typeof ctor === 'function') {
info.p1_ctor_proto_is_gt = Object.getPrototypeOf(ctor) === gt;
const ctorProto = Object.getPrototypeOf(ctor);
info.p1_ctor_proto_is_func_proto = ctorProto === Function.prototype;
// Does Function.prototype.toString work on it?
try {
const ts = Function.prototype.toString.call(ctor);
info.p1_ctor_tostring = ts.slice(0, 50);
} catch(e) {
info.p1_ctor_tostring_err = e.message;
}
// Can we access ctor.prototype?
info.p1_ctor_prototype_is_p1 = ctor.prototype === p1;
}
}
return JSON.stringify(info);
} catch(e) { return 'error: ' + e.message; }
})()
"#,
)
.await;
println!("global proto chain: {result}");
assert!(!result.starts_with("error:"), "proto diag failed: {result}");
}
#[tokio::test]
async fn shim_recursion_iframe_proto_walk() {
let result = check(
r#"
(function() {
try {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const win = iframe.contentWindow;
if (!win) return 'no_contentWindow';
// Walk contentWindow's own properties
const ownKeys = Object.getOwnPropertyNames(win);
// Check if 'window' in win (has trap)
const hasWindow = 'window' in win;
// Walk prototype chain of contentWindow
const chain = [];
let proto = win;
let depth = 0;
while (proto !== null && proto !== undefined && depth < 20) {
if (chain.indexOf(proto) !== -1) return 'cycle_at_' + depth;
chain.push(proto);
proto = Object.getPrototypeOf(proto);
depth++;
}
// Access each own key of contentWindow
for (const key of ownKeys) {
try { win[key]; } catch(_) {}
}
// Check 'in' for common window properties
const props = ['window','self','top','parent','location','document',
'navigator','screen','fetch','setTimeout','Array',
'Object','Function','localStorage'];
for (const p of props) {
try { const r = p in win; } catch(_) {}
}
return 'iframe_walk_ok_' + chain.length + '_own_' + ownKeys.length;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("iframe_walk_ok_") || result.starts_with("cycle_at_"),
"iframe contentWindow walk should not crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_creepjs_realm_check() {
let result = check(
r#"
(function() {
try {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iwin = iframe.contentWindow;
if (!iwin) return 'no_contentWindow';
// creepjs: compare constructors across realms
const tests = {
arrayMatch: iwin.Array === Array,
objectMatch: iwin.Object === Object,
functionMatch: iwin.Function === Function,
selfIsWindow: iwin.self === iwin,
toStringNative: Function.prototype.toString.call(iwin.Array),
};
// creepjs: walk ALL own props of main window, check if in iframe window
let diffCount = 0;
const mainKeys = Object.getOwnPropertyNames(window);
const iwinKeys = Object.getOwnPropertyNames(iwin);
for (const k of mainKeys) {
if (!iwinKeys.includes(k)) diffCount++;
}
// creepjs: getPrototypeOf chain on iwin
let protoDepth = 0;
let p = iwin;
while (p !== null && p !== undefined && protoDepth < 10) {
p = Object.getPrototypeOf(p);
protoDepth++;
}
return JSON.stringify({
ok: true,
diffCount,
protoDepth,
selfIsWindow: tests.selfIsWindow,
});
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.contains("\"ok\":true"),
"creepjs realm check should not crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_creepjs_lies_detection() {
let result = check(
r#"
(function() {
try {
const toString = Function.prototype.toString;
let checked = 0;
let errors = 0;
// Access every own property of window and call toString if function
const keys = Object.getOwnPropertyNames(window);
for (const key of keys) {
try {
const val = window[key];
if (typeof val === 'function') {
toString.call(val);
checked++;
}
// Also check getters
const desc = Object.getOwnPropertyDescriptor(window, key);
if (desc && typeof desc.get === 'function') {
toString.call(desc.get);
}
} catch(e) {
errors++;
}
}
// Check via prototype chain too
let proto = Object.getPrototypeOf(window);
while (proto && proto !== Object.prototype) {
for (const key of Object.getOwnPropertyNames(proto)) {
try {
const desc = Object.getOwnPropertyDescriptor(proto, key);
if (desc && typeof desc.get === 'function') toString.call(desc.get);
if (desc && typeof desc.value === 'function') toString.call(desc.value);
checked++;
} catch(_) { errors++; }
}
proto = Object.getPrototypeOf(proto);
}
return 'lies_check_ok_checked=' + checked + '_errors=' + errors;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("lies_check_ok_"),
"creepjs lies detection should not crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_ym_iife_pattern() {
let result = check(
r#"
(function() {
try {
// Simulate Yandex Metrika initialization IIFE.
// Uses a data: URL so no real network call is made.
var insertCount = 0;
(function(m, e, t, r, i) {
m[i] = m[i] || function() { (m[i].a = m[i].a || []).push(arguments); };
m[i].l = 1 * new Date();
for (var j = 0; j < document.scripts.length; j++) {
if (document.scripts[j].src === r) { return; }
}
var k = e.createElement(t);
k.src = r;
insertCount++;
var a = e.getElementsByTagName(t)[0];
if (a && a.parentNode) { a.parentNode.insertBefore(k, a); }
else { e.head.appendChild(k); }
})(window, document, 'script', 'data:text/javascript,void+0', 'ym');
return 'ym_ok_inserts_' + insertCount;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("ym_ok_"),
"Yandex Metrika IIFE pattern should not recurse, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_ym_iframe_navigator_probe() {
let result = check(
r#"
(function() {
try {
// Create iframe + read its window (our iframeWindow Proxy)
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var iw = iframe.contentWindow;
if (!iw) return 'error: no contentWindow';
// YM accesses iframe.contentWindow.navigator for cross-frame check
var nav = iw.navigator;
var ua = nav ? nav.userAgent : 'missing';
// YM probes navigator properties via for...in
var navKeys = [];
try {
for (var k in navigator) { navKeys.push(k); if (navKeys.length > 100) break; }
} catch(e) {}
// YM accesses plugins and mimeTypes
var pluginsLen = navigator.plugins.length;
var mimesLen = navigator.mimeTypes.length;
// YM calls Function.prototype.toString on iframe globals
var iToStr = Function.prototype.toString.call(iw.Function || Function);
return 'ym_iframe_ok_nav_keys_' + navKeys.length +
'_plugins_' + pluginsLen +
'_mimes_' + mimesLen +
'_tostr_' + (iToStr.includes('[native code]') ? 'native' : 'src');
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("ym_iframe_ok_"),
"YM iframe+navigator probe should not crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_ym_cookie_scripts_probe() {
let result = check(
r#"
(function() {
try {
// Cookie read/write (YM stores its state in _ym_uid cookie)
document.cookie = '_ym_uid=1234567890; path=/';
var cookie = document.cookie;
// YM iterates document.scripts to check if already loaded
var scriptSrcs = [];
for (var j = 0; j < document.scripts.length; j++) {
scriptSrcs.push(document.scripts[j].src || 'inline');
}
// YM probes performance.timing for load time calculation
var timing = window.performance && window.performance.timing;
var navStart = timing ? timing.navigationStart : -1;
// YM uses screen properties
var screenInfo = screen.width + 'x' + screen.height + 'x' + screen.colorDepth;
return 'ym_cookie_ok_cookie_' + (cookie.includes('_ym_uid') ? 'found' : 'missing') +
'_scripts_' + scriptSrcs.length +
'_screen_' + screenInfo;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("ym_cookie_ok_"),
"YM cookie+scripts probe should not crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_ym_window_enum_probe() {
let result = check(
r#"
(function() {
try {
// YM checks if specific globals exist via in-operator
var checks = {
ym: 'ym' in window,
Ya: 'Ya' in window,
yandex: 'yandex' in window,
_ym_: '_ym_' in window,
};
// YM enumerates window to find conflicting libraries
var keyCount = 0;
try {
for (var k in window) {
keyCount++;
if (keyCount > 500) break; // Safety cap
}
} catch(e) {}
// YM checks window.top === window (not in a cross-origin frame)
var isTop = window.top === window;
var isSelf = window.self === window;
var isParent = window.parent === window;
// YM checks typeof various globals
var typeChecks = [
typeof window.JSON,
typeof window.Promise,
typeof window.fetch,
typeof window.XMLHttpRequest,
typeof window.Worker,
].join(',');
return 'ym_enum_ok_keys_' + keyCount + '_top_' + isTop +
'_types_' + typeChecks;
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("ym_enum_ok_"),
"YM window enumeration should not crash, got: {result}"
);
}
#[tokio::test]
async fn shim_recursion_ym_module_loader_pattern() {
let result = check(
r#"
(function() {
try {
// Simulate YM's webpack-style module loader
var modules = {};
var cache = {};
function require(id) {
if (cache[id]) return cache[id].exports;
var mod = { exports: {} };
cache[id] = mod;
if (modules[id]) modules[id](mod, mod.exports, require);
return mod.exports;
}
// Register some fake modules
modules[0] = function(m, e, r) {
e.init = function() { return r(1).run(); };
};
modules[1] = function(m, e, r) {
e.run = function() {
// Simulate YM environment probe
var hasYa = typeof window.Ya !== 'undefined';
var userAgent = navigator.userAgent;
var lang = navigator.language;
return { hasYa: hasYa, ua: userAgent.slice(0, 20), lang: lang };
};
};
// Run entry point (module 0)
var result = require(0).init();
return 'ym_loader_ok_ua_' + (result.ua ? 'present' : 'missing') +
'_lang_' + (result.lang ? 'present' : 'missing');
} catch(e) {
return 'error: ' + e.message;
}
})()
"#,
)
.await;
assert!(
result.starts_with("ym_loader_ok_"),
"YM module loader pattern should not crash, got: {result}"
);
}
#[tokio::test]
#[ignore = "network: dump our headers via httpbin to diff vs real Chrome"]
async fn dump_our_headers_httpbin() {
let profile = browser_oxide::stealth::presets::chrome_148_macos();
let client = browser_oxide::net::HttpClient::new(&profile).unwrap();
let url = "https://httpbin.org/headers";
println!("\n=== Our request headers per httpbin.org/headers ===");
let hdrs = browser_oxide::net::headers::chrome_headers(&profile);
println!(" Headers we'll attach (in order, LOW-ENTROPY = production navs):");
for (k, v) in &hdrs {
println!(" {}: {}", k, v);
}
match client.get_with_headers(url, &hdrs).await {
Ok(resp) => {
let body = resp.text();
println!(" --- httpbin echo (what server saw) ---");
println!("{}", body);
}
Err(e) => println!(" ERROR: {e}"),
}
println!("=== end ===");
}
#[tokio::test]
#[ignore = "network: dump our TLS+H2 fingerprint via tls.peet.ws"]
async fn tls_fingerprint_peet() {
let profile = browser_oxide::stealth::presets::chrome_148_macos();
let client = browser_oxide::net::HttpClient::new(&profile).unwrap();
let url = "https://tls.peet.ws/api/all";
println!("\n=== Our TLS fingerprint per tls.peet.ws ===");
let hdrs = browser_oxide::net::headers::chrome_headers_with_accept_ch(&profile);
match client.get_with_headers(url, &hdrs).await {
Ok(resp) => {
let body = resp.text();
println!(" status: {}", resp.status);
println!(" body bytes: {}", body.len());
let extract = |key: &str| -> String {
let pat = format!("\"{}\"", key);
if let Some(s) = body.find(&pat) {
let after = &body[s + pat.len()..];
if let Some(open) = after.find('"') {
let val_start = open + 1;
if let Some(close) = after[val_start..].find('"') {
return after[val_start..val_start + close].to_string();
}
}
}
"?".to_string()
};
println!(" ja3: {}", extract("ja3"));
println!(" ja3_hash: {}", extract("ja3_hash"));
println!(" ja4: {}", extract("ja4"));
println!(" ja4_r: {}", extract("ja4_r"));
println!(" peetprint: {}", extract("peetprint"));
println!(" peetprint_hash: {}", extract("peetprint_hash"));
println!(" akamai_fp: {}", extract("akamai_fingerprint"));
println!(
" akamai_hash: {}",
extract("akamai_fingerprint_hash")
);
println!(" user_agent: {}", extract("user_agent"));
println!(" http_version: {}", extract("http_version"));
}
Err(e) => println!(" ERROR: {e}"),
}
println!("=== end ===");
}
#[tokio::test]
#[ignore = "network: hits reddit.com"]
async fn reddit_smoke() {
let profile = browser_oxide::stealth::presets::chrome_148_macos();
let mut page = match Page::navigate("https://www.reddit.com/", profile, 5).await {
Ok(p) => p,
Err(e) => {
println!("\n=== reddit.com navigation FAILED ===");
println!("error: {e}");
panic!("navigate failed");
}
};
let title = page.title();
let url = page.url().to_string();
let body_len = page
.evaluate("document.body ? document.body.textContent.length : 0")
.unwrap_or_else(|e| format!("evaluate body err: {e}"));
let body_snippet = page
.evaluate(
"document.body ? \
document.body.textContent.replace(/\\s+/g, ' ').trim().slice(0, 400) \
: 'no body'",
)
.unwrap_or_else(|e| format!("evaluate snippet err: {e}"));
let html_len = page
.evaluate("document.documentElement ? document.documentElement.outerHTML.length : 0")
.unwrap_or_default();
let n_links = page
.evaluate("document.querySelectorAll('a').length")
.unwrap_or_default();
let n_scripts = page
.evaluate("document.querySelectorAll('script').length")
.unwrap_or_default();
let cookies = page
.evaluate("typeof document.cookie === 'string' ? document.cookie.length : 0")
.unwrap_or_default();
println!("\n=== reddit.com navigation result ===");
println!(" final url: {url}");
println!(" title: {title:?}");
println!(" html bytes: {html_len}");
println!(" body chars: {body_len}");
println!(" <a> count: {n_links}");
println!(" <script> count: {n_scripts}");
println!(" cookie chars: {cookies}");
println!(" body snippet: {body_snippet}");
println!("=== end ===\n");
assert!(
html_len.parse::<usize>().unwrap_or(0) > 1000,
"expected >1000 bytes of HTML, got {html_len}"
);
}
#[tokio::test]
async fn fingerprint_probe_vs_chrome() {
let js = r#"(() => {
const r = {};
r.chromeKeys = Object.keys(window.chrome||{}).sort().join(',');
r.chromeRuntimeExists = !!(window.chrome && window.chrome.runtime);
r.chromeWebstoreExists = !!(window.chrome && window.chrome.webstore);
if (window.chrome && window.chrome.loadTimes) {
try {
const lt = window.chrome.loadTimes();
r.loadTimesSpdy = lt.wasFetchedViaSpdy;
r.loadTimesNpn = lt.wasNpnNegotiated;
r.loadTimesProto = lt.npnNegotiatedProtocol;
r.loadTimesKeys = Object.keys(lt).sort().join(',');
} catch(e) { r.loadTimesError = ''+e; }
}
r.trustedTypes = typeof trustedTypes;
r.scheduler = typeof scheduler;
r.reportError = typeof reportError;
r.requestIdleCallback = typeof requestIdleCallback;
r.cancelIdleCallback = typeof cancelIdleCallback;
r.queueMicrotask = typeof queueMicrotask;
r.SharedArrayBuffer = typeof SharedArrayBuffer;
r.deviceMemory = navigator.deviceMemory;
r.connection_effectiveType = navigator.connection ? navigator.connection.effectiveType : 'NONE';
r.getBattery = typeof navigator.getBattery === 'function';
r.pdfViewerEnabled = navigator.pdfViewerEnabled;
r.webdriver = navigator.webdriver;
r.webdriverInNav = 'webdriver' in navigator;
r.pluginsLength = navigator.plugins ? navigator.plugins.length : -1;
r.mimeTypesLength = navigator.mimeTypes ? navigator.mimeTypes.length : -1;
r.perfMemory = !!(window.performance && window.performance.memory);
r.isSecureContext = window.isSecureContext;
r.touchExists = typeof Touch !== 'undefined';
r.PaymentRequest = typeof PaymentRequest;
r.docHasFocus = document.hasFocus();
r.docVisibility = document.visibilityState;
r.navigatorProto = Object.getPrototypeOf(navigator) ? Object.getPrototypeOf(navigator).constructor.name : null;
r.notificationExists = typeof Notification !== 'undefined';
r.paymentRequestExists = typeof PaymentRequest !== 'undefined';
r.indexedDBExists = typeof indexedDB !== 'undefined';
r.storageExists = typeof navigator.storage !== 'undefined';
r.serviceWorkerExists = typeof navigator.serviceWorker !== 'undefined';
r.credentialsExists = typeof navigator.credentials !== 'undefined';
r.mediaDevicesExists = typeof navigator.mediaDevices !== 'undefined';
r.geolocationExists = typeof navigator.geolocation !== 'undefined';
r.bluetoothExists = !!(navigator.bluetooth);
r.usbExists = !!(navigator.usb);
r.ResizeObserver = typeof ResizeObserver;
r.IntersectionObserver = typeof IntersectionObserver;
r.PerformanceObserver = typeof PerformanceObserver;
r.MutationObserver = typeof MutationObserver;
r.cryptoRandomUUID = typeof crypto !== 'undefined' && typeof crypto.randomUUID;
r.screen_orientation = screen.orientation ? screen.orientation.type : 'NONE';
r.devicePixelRatio = window.devicePixelRatio;
return JSON.stringify(r, null, 2);
})()"#;
let result = check(js).await;
println!("\nFINGERPRINT PROBE:\n{}", result);
assert!(
result.starts_with('{'),
"expected JSON, got: {}",
&result[..100.min(result.len())]
);
}
#[tokio::test]
async fn check_iterators_test() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = browser_oxide::Page::with_profile("", "about:blank", profile)
.await
.unwrap();
let js = r#"
(function() {
const results = {};
const targets = [
['navigator.plugins', navigator.plugins],
['navigator.mimeTypes', navigator.mimeTypes],
['document.fonts', document.fonts],
];
for (const [name, obj] of targets) {
try {
const it = obj[Symbol.iterator]();
results[name + '_iterator_iterable'] = it[Symbol.iterator]() === it;
} catch (e) {
results[name + '_error'] = e.message;
}
}
return JSON.stringify(results, null, 2);
})()
"#;
let result = page.evaluate(js).unwrap();
println!("ITERATOR CHECK:\n{}", result);
}
#[tokio::test]
async fn check_ctors_test() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = browser_oxide::Page::with_profile("", "about:blank", profile)
.await
.unwrap();
let js = r#"
(function() {
const results = {};
const targets = [
['navigator.plugins.item', navigator.plugins.item],
['navigator.plugins.namedItem', navigator.plugins.namedItem],
['navigator.plugins.refresh', navigator.plugins.refresh],
['MediaSource.isTypeSupported', MediaSource.isTypeSupported],
['Notification.requestPermission', Notification.requestPermission],
];
for (const [name, fn] of targets) {
try {
new fn();
results[name] = 'IS_CONSTRUCTOR';
} catch (e) {
results[name] = 'NOT_CONSTRUCTOR';
}
}
return JSON.stringify(results, null, 2);
})()
"#;
let result = page.evaluate(js).unwrap();
println!("CTOR CHECK:\n{}", result);
}
#[tokio::test]
async fn check_tostring_test() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = browser_oxide::Page::with_profile("", "about:blank", profile)
.await
.unwrap();
let js = r#"
(function() {
const res = {};
const targets = {
'mediaDevices.enumerateDevices': navigator.mediaDevices && navigator.mediaDevices.enumerateDevices,
'mediaDevices.getUserMedia': navigator.mediaDevices && navigator.mediaDevices.getUserMedia,
'mediaDevices.addEventListener': navigator.mediaDevices && navigator.mediaDevices.addEventListener,
'PublicKeyCredential.isUVPAA': globalThis.PublicKeyCredential && PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable,
'CredentialsContainer.get': navigator.credentials && navigator.credentials.get,
'plugins.item': navigator.plugins.item,
'plugins.refresh': navigator.plugins.refresh,
'fetch': globalThis.fetch,
'setTimeout': globalThis.setTimeout,
};
for (const [k, v] of Object.entries(targets)) {
try {
res[k + '_instance'] = v.toString();
res[k + '_protoCall'] = Function.prototype.toString.call(v);
res[k + '_length'] = v.length;
} catch (e) {
res[k + '_err'] = e.message;
}
}
return JSON.stringify(res, null, 2);
})()
"#;
let result = page.evaluate(js).unwrap();
println!("TOSTRING CHECK:\n{}", result);
}
#[tokio::test]
async fn check_spread_test() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = browser_oxide::Page::with_profile("", "about:blank", profile)
.await
.unwrap();
let js = r#"
(function() {
const res = {};
const targets = {
'plugins': navigator.plugins,
'mimeTypes': navigator.mimeTypes,
'fonts': document.fonts,
'scripts': document.scripts,
'styleSheets': document.styleSheets,
'all': document.all,
'htmlCollection': document.getElementsByTagName('div'),
'nodeList': document.querySelectorAll('div'),
};
for (const [k, v] of Object.entries(targets)) {
try {
[...v];
res[k] = true;
} catch(e) {
res[k] = e.message;
}
}
return JSON.stringify(res, null, 2);
})()
"#;
println!("SPREAD CHECK:\n{}", page.evaluate(js).unwrap());
}
#[tokio::test]
async fn check_ao_candidates_test() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = browser_oxide::Page::with_profile("", "about:blank", profile)
.await
.unwrap();
let js = r#"
(function() {
const res = {};
const tests = [
['navigator.plugins',
() => navigator.plugins],
['navigator.mimeTypes',
() => navigator.mimeTypes],
['navigator.userAgentData.brands',
() => navigator.userAgentData && navigator.userAgentData.brands],
['document.fonts',
() => document.fonts],
['document.scripts',
() => document.scripts],
['document.styleSheets',
() => document.styleSheets],
['document.all',
() => document.all],
['document.images',
() => document.images],
['document.links',
() => document.links],
['document.forms',
() => document.forms],
['document.embeds',
() => document.embeds],
['document.anchors',
() => document.anchors],
['HTMLCollection (children)',
() => document.body.children],
['NodeList (querySelectorAll)',
() => document.querySelectorAll('div')],
['NodeList (childNodes)',
() => document.body.childNodes],
['NamedNodeMap (attributes)',
() => document.body.attributes],
['DOMTokenList (classList)',
() => document.body.classList],
['FormData',
() => new FormData()],
['URLSearchParams',
() => new URLSearchParams('a=1&b=2')],
['Headers',
() => new Headers({'X-Test': 'a'})],
['MediaList',
() => document.styleSheets[0] && document.styleSheets[0].media],
['CSSRuleList',
() => document.styleSheets[0] && document.styleSheets[0].cssRules],
['MediaSource exists?',
() => typeof MediaSource],
['MediaSource.activeSourceBuffers',
() => { const ms = new MediaSource(); return ms.activeSourceBuffers; }],
['MediaSource.sourceBuffers',
() => { const ms = new MediaSource(); return ms.sourceBuffers; }],
['RTCRtpReceiver exists?',
() => typeof RTCRtpReceiver],
['RTCRtpReceiver.getCapabilities(audio)',
() => RTCRtpReceiver.getCapabilities('audio')],
['RTCRtpSender.getCapabilities(video)',
() => RTCRtpSender.getCapabilities('video')],
['ResizeObserverSize-like',
() => ({length: 2, 0: 'a', 1: 'b'})],
['FileList',
() => null /* requires <input type=file>; skip */],
['TouchList',
() => null],
];
for (const [name, fn] of tests) {
let r = {};
try {
const v = fn();
if (v === null) { r.skip = true; }
else if (typeof v === 'string') { r.value = v; }
else {
r.type = Object.prototype.toString.call(v);
r.hasSymbolIter = typeof v[Symbol.iterator] === 'function';
try {
const arr = [...v];
r.spread = `ok len=${arr.length}`;
} catch (e) {
r.spread = 'ERR: ' + e.message.split('\n')[0];
}
}
} catch (e) {
r.setup_err = e.message;
}
res[name] = r;
}
return JSON.stringify(res, null, 2);
})()
"#;
println!("AO CANDIDATES CHECK:\n{}", page.evaluate(js).unwrap());
}
async fn thin_body_diagnose(url: &str, name: &str) {
use browser_oxide::Page;
use std::time::Duration;
println!("\n========== {name} ({url}) ==========");
let r = tokio::time::timeout(
Duration::from_secs(60),
Page::navigate(url, browser_oxide::stealth::presets::chrome_148_macos(), 2),
)
.await;
match r {
Ok(Ok(mut p)) => {
let info = p.evaluate(r#"
JSON.stringify({
readyState: document.readyState,
bodyLen: (document.body && document.body.innerHTML || '').length,
bodyChildren: document.body ? document.body.children.length : 0,
headChildren: document.head ? document.head.children.length : 0,
scriptCount: document.scripts ? document.scripts.length : 0,
title: document.title,
url: location.href,
bodySnippet: (document.body && document.body.innerHTML || '').slice(0, 400),
htmlSnippet: document.documentElement ? document.documentElement.outerHTML.slice(0, 400) : '',
})
"#).unwrap_or_default();
println!("info: {info}");
}
Ok(Err(e)) => println!("ERROR: {e}"),
Err(_) => println!("TIMEOUT"),
}
}
#[tokio::test]
#[ignore = "network: THIN-BODY diagnostic for parallel sweep failures"]
async fn diag_thin_body_sites() {
thin_body_diagnose("https://www.cloudflare.com/", "cloudflare").await;
thin_body_diagnose("https://www.primevideo.com/", "prime-video").await;
}
#[tokio::test]
async fn check_payment_request_surface() {
let profile = browser_oxide::stealth::chrome_148_linux();
let mut page = Page::from_html_with_url(&html(""), "https://example.com/", Some(profile))
.await
.unwrap();
let sync_js = r#"
(function() {
const res = {};
res.PaymentRequest_typeof = typeof PaymentRequest;
res.PaymentRequest_length = typeof PaymentRequest === 'function' ? PaymentRequest.length : null;
res.PaymentResponse_typeof = typeof PaymentResponse;
res.PaymentMethodChangeEvent_typeof = typeof PaymentMethodChangeEvent;
res.PaymentRequestUpdateEvent_typeof = typeof PaymentRequestUpdateEvent;
res.ApplePaySession_typeof = typeof ApplePaySession;
res.canMakePayment_toString = typeof PaymentRequest === 'function'
? PaymentRequest.prototype.canMakePayment.toString()
: null;
// Constructor validation (sync throws)
try { new PaymentRequest([], { total: { label: 'x', amount: { currency: 'USD', value: '1' } } }); res.empty_methods = 'no throw'; }
catch (e) { res.empty_methods = e.name; }
try { new PaymentRequest([{supportedMethods: 'basic-card'}], {}); res.no_total = 'no throw'; }
catch (e) { res.no_total = e.name; }
return JSON.stringify(res, null, 2);
})()
"#;
let sync_result = page.evaluate(sync_js).unwrap();
println!("PAYMENT REQUEST SYNC CHECK:\n{}", sync_result);
assert!(sync_result.contains("\"PaymentRequest_typeof\": \"function\""));
assert!(sync_result.contains("\"PaymentRequest_length\": 2"));
assert!(sync_result.contains("\"PaymentResponse_typeof\": \"function\""));
assert!(sync_result.contains("\"PaymentMethodChangeEvent_typeof\": \"function\""));
assert!(sync_result.contains("\"PaymentRequestUpdateEvent_typeof\": \"function\""));
assert!(
sync_result.contains("\"ApplePaySession_typeof\": \"undefined\""),
"ApplePaySession must be undefined under Linux Chrome profile (cardinal sin)"
);
assert!(sync_result.contains("\"empty_methods\": \"TypeError\""));
assert!(sync_result.contains("\"no_total\": \"TypeError\""));
assert!(
sync_result.contains("[native code]"),
"PaymentRequest.prototype.canMakePayment.toString() must include [native code]"
);
page.evaluate(
r#"window.__r = {};
(async function() {
const r = window.__r;
try {
const pr = new PaymentRequest(
[{ supportedMethods: 'https://google.com/pay' }],
{ total: { label: 'x', amount: { currency: 'USD', value: '1.00' } } }
);
r.id_present = typeof pr.id === 'string' && pr.id.length > 0;
r.shippingAddress = pr.shippingAddress;
r.shippingOption = pr.shippingOption;
r.shippingType = pr.shippingType;
r.canMakePayment_googlepay = await pr.canMakePayment();
r.hasEnrolledInstrument = await pr.hasEnrolledInstrument();
try { await pr.show(); r.show_ok = 'unexpected resolve'; }
catch (e) { r.show_rejected = e.name; }
r.abort_resolved = await pr.abort();
} catch (e) { r.ctor_err = e.name + ': ' + e.message; }
try {
const pr2 = new PaymentRequest(
[{ supportedMethods: 'unknown://method' }],
{ total: { label: 'x', amount: { currency: 'USD', value: '1.00' } } }
);
r.canMakePayment_unknown = await pr2.canMakePayment();
} catch (e) { r.unknown_err = e.message; }
try {
r.spc = await PaymentRequest.securePaymentConfirmationAvailability();
} catch (e) { r.spc_err = e.message; }
window.__r_done = true;
})();"#,
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(500))
.await
.ok();
let async_result = page
.evaluate("JSON.stringify(window.__r, null, 2)")
.unwrap();
println!("PAYMENT REQUEST ASYNC CHECK:\n{}", async_result);
assert!(async_result.contains("\"id_present\": true"));
assert!(async_result.contains("\"shippingAddress\": null"));
assert!(async_result.contains("\"canMakePayment_googlepay\": true"));
assert!(async_result.contains("\"canMakePayment_unknown\": false"));
assert!(async_result.contains("\"hasEnrolledInstrument\": false"));
assert!(async_result.contains("\"show_rejected\": \"AbortError\""));
assert!(
async_result.contains("\"spc\": \"unavailable-no-user-verifying-platform-authenticator\"")
);
}
#[tokio::test]
async fn check_get_installed_related_apps() {
let profile = browser_oxide::stealth::chrome_148_linux();
let mut page = Page::from_html_with_url(&html(""), "https://example.com/", Some(profile))
.await
.unwrap();
let sync_js = r#"
(function() {
const res = {};
res.typeof_method = typeof navigator.getInstalledRelatedApps;
res.toString = typeof navigator.getInstalledRelatedApps === 'function'
? navigator.getInstalledRelatedApps.toString()
: null;
return JSON.stringify(res, null, 2);
})()
"#;
let sync_result = page.evaluate(sync_js).unwrap();
println!("getInstalledRelatedApps SYNC CHECK:\n{}", sync_result);
assert!(sync_result.contains("\"typeof_method\": \"function\""));
assert!(sync_result.contains("[native code]"));
page.evaluate(
r#"window.__r = null;
navigator.getInstalledRelatedApps().then(o => { window.__r = o; });"#,
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(200))
.await
.ok();
let async_result = page
.evaluate(
r#"(() => {
const r = window.__r;
if (!Array.isArray(r)) return 'not-array:' + typeof r;
return 'len=' + r.length;
})()"#,
)
.unwrap();
println!("getInstalledRelatedApps ASYNC CHECK: {}", async_result);
assert_eq!(async_result, "len=0");
}
#[tokio::test]
async fn check_tostring_audit_full() {
let profile = browser_oxide::stealth::chrome_148_linux();
let mut page = Page::from_html_with_url(&html(""), "https://example.com/", Some(profile))
.await
.unwrap();
let js = r#"
(function() {
const res = {};
const targets = [
['canPlayType', () => {
const v = document.createElement('video');
return v.canPlayType;
}],
['enumerateDevices', () => navigator.mediaDevices && navigator.mediaDevices.enumerateDevices],
['getBattery', () => navigator.getBattery],
['getInstalledRelatedApps', () => navigator.getInstalledRelatedApps],
['canMakePayment',
() => typeof PaymentRequest === 'function' && PaymentRequest.prototype.canMakePayment],
['plugins.item', () => navigator.plugins && navigator.plugins.item],
['fetch', () => globalThis.fetch],
];
for (const [label, getter] of targets) {
const fn = getter();
const r = {};
if (typeof fn !== 'function') { r.skip = 'not a function: ' + typeof fn; res[label] = r; continue; }
// Path 1: implicit + "" coercion
try {
const s = fn + "";
r.coerce = s.includes('[native code]') ? 'native' : ('SRC: ' + s.slice(0, 60));
} catch (e) { r.coerce_err = e.message; }
// Path 2: Function.prototype.toString.call(fn)
try {
const s = Function.prototype.toString.call(fn);
r.protoCall = s.includes('[native code]') ? 'native' : ('SRC: ' + s.slice(0, 60));
} catch (e) { r.protoCall_err = e.message; }
// Path 3: instance .toString()
try {
const s = fn.toString();
r.instance = s.includes('[native code]') ? 'native' : ('SRC: ' + s.slice(0, 60));
} catch (e) { r.instance_err = e.message; }
// Path 4: Object.prototype.toString — should NOT change shape
try {
r.objProto = Object.prototype.toString.call(fn);
} catch (e) { r.objProto_err = e.message; }
res[label] = r;
}
// Cross-realm: iframe.contentWindow.Function.prototype.toString
try {
const ifr = document.createElement('iframe');
document.body && document.body.appendChild(ifr);
const cw = ifr.contentWindow;
if (!cw) { res.iframe = 'no contentWindow'; }
else if (!cw.Function || !cw.Function.prototype || !cw.Function.prototype.toString) {
res.iframe = 'no cw.Function.prototype.toString';
} else {
const cwTs = cw.Function.prototype.toString;
const fns = {
'parent.fetch': globalThis.fetch,
'parent.canPlayType': document.createElement('video').canPlayType,
'parent.canMakePayment': PaymentRequest && PaymentRequest.prototype.canMakePayment,
'parent.getBattery': navigator.getBattery,
};
res.iframe = {};
for (const [k, fn] of Object.entries(fns)) {
if (typeof fn !== 'function') { res.iframe[k] = 'skip:' + typeof fn; continue; }
try {
const s = cwTs.call(fn);
res.iframe[k] = s.includes('[native code]') ? 'native' : ('SRC: ' + s.slice(0, 60));
} catch (e) { res.iframe[k] = 'ERR: ' + e.message.split('\n')[0]; }
}
}
} catch (e) { res.iframe_err = e.message; }
// Stack-trace sanitization
const proxyArtifacts = [
'at Object.apply', 'at Object.get', 'at Reflect.apply',
'at newHandler.', 'at Proxy.', 'at handler.', 'at trap.',
'_inPatchedToStr', '_nativeTag', '_origFnToStr', '_patchedFnToStr',
];
try {
Function.prototype.toString.call(undefined);
res.stack = 'unexpected: did not throw';
} catch (e) {
const stack = String(e.stack || '');
const hits = proxyArtifacts.filter(a => stack.includes(a));
res.stack = hits.length === 0 ? 'clean' : 'LEAK: ' + hits.join(', ');
res.stack_full = stack.split('\n').slice(0, 6).join(' | ');
}
try {
Function.prototype.toString.call({});
res.stack_obj = 'unexpected: did not throw';
} catch (e) {
const stack = String(e.stack || '');
const hits = proxyArtifacts.filter(a => stack.includes(a));
res.stack_obj = hits.length === 0 ? 'clean' : 'LEAK: ' + hits.join(', ');
}
return JSON.stringify(res, null, 2);
})()
"#;
let result = page.evaluate(js).unwrap();
println!("TOSTRING AUDIT:\n{}", result);
assert!(
!result.contains("\"coerce\": \"SRC:"),
"+ \"\" coercion leaked raw source somewhere: {result}"
);
assert!(
!result.contains("\"protoCall\": \"SRC:"),
"Function.prototype.toString.call leaked raw source: {result}"
);
assert!(
!result.contains("\"instance\": \"SRC:"),
"instance .toString() leaked raw source: {result}"
);
assert!(
!result.contains(": \"SRC:"),
"cross-realm iframe toString leaked source: {result}"
);
assert!(
result.contains("\"stack\": \"clean\""),
"stack trace from toString.call(undefined) leaked Proxy artifacts: {result}"
);
assert!(
result.contains("\"stack_obj\": \"clean\""),
"stack trace from toString.call({{}}) leaked Proxy artifacts: {result}"
);
}
#[tokio::test]
async fn check_audio_fingerprint_per_profile() {
let render_js = r#"
(function() {
const ctx = new OfflineAudioContext(1, 5000, 44100);
const osc = ctx.createOscillator();
osc.type = 'triangle';
osc.frequency.value = 10000;
const comp = ctx.createDynamicsCompressor();
comp.threshold.value = -50;
comp.knee.value = 40;
comp.ratio.value = 12;
comp.attack.value = 0;
comp.release.value = 0.25;
osc.connect(comp);
comp.connect(ctx.destination);
osc.start(0);
window.__audio_done = false;
window.__audio_hash = null;
ctx.startRendering().then(buf => {
const data = buf.getChannelData(0);
let s = 0;
// FPjs-canonical reduction: sum of abs values in [4500, 5000]
for (let i = 4500; i < Math.min(5000, data.length); i++) s += Math.abs(data[i]);
window.__audio_hash = s;
window.__audio_done = true;
});
})()
"#;
async fn render(profile: browser_oxide::stealth::StealthProfile) -> f64 {
let mut page = Page::from_html_with_url(&html(""), "https://example.com/", Some(profile))
.await
.unwrap();
page.evaluate(
r#"
(function() {
const ctx = new OfflineAudioContext(1, 5000, 44100);
const osc = ctx.createOscillator();
osc.type = 'triangle';
osc.frequency.value = 10000;
const comp = ctx.createDynamicsCompressor();
comp.threshold.value = -50;
comp.knee.value = 40;
comp.ratio.value = 12;
comp.attack.value = 0;
comp.release.value = 0.25;
osc.connect(comp);
comp.connect(ctx.destination);
osc.start(0);
window.__audio_done = false;
window.__audio_hash = null;
ctx.startRendering().then(buf => {
const data = buf.getChannelData(0);
let s = 0;
for (let i = 4500; i < Math.min(5000, data.length); i++) s += Math.abs(data[i]);
window.__audio_hash = s;
window.__audio_done = true;
});
})()
"#,
)
.unwrap();
page.evaluate_async("void 0", std::time::Duration::from_millis(500))
.await
.ok();
let raw = page.evaluate(r#"String(window.__audio_hash)"#).unwrap();
raw.parse::<f64>().unwrap_or(f64::NAN)
}
let _ = render_js;
let h_mac = render(browser_oxide::stealth::chrome_148_macos()).await;
let h_lin = render(browser_oxide::stealth::chrome_148_linux()).await;
let h_lin_2 = render(browser_oxide::stealth::chrome_148_linux()).await;
println!("audio hashes: mac={h_mac} lin={h_lin} lin_again={h_lin_2}");
assert!(
h_mac.is_finite() && h_mac > 0.0,
"macOS profile produced no audio output"
);
assert!(
h_lin.is_finite() && h_lin > 0.0,
"Linux profile produced no audio output"
);
assert!(
(h_mac - h_lin).abs() > 1e-6,
"audio fingerprint should differ across profiles with distinct audio_seed: mac={h_mac} lin={h_lin}"
);
assert!(
(h_lin - h_lin_2).abs() < 1e-6,
"same profile should produce identical audio hash: lin={h_lin} lin_again={h_lin_2}"
);
}
#[tokio::test]
async fn check_function_identity_preservation() {
let profile = browser_oxide::stealth::chrome_148_macos();
let mut page = Page::from_html_with_url(&html(""), "https://example.com/", Some(profile))
.await
.unwrap();
let js = r#"
(function() {
const res = {};
// Test 1: navigator.mediaDevices.enumerateDevices identity stability
try {
const a = navigator.mediaDevices.enumerateDevices;
const b = navigator.mediaDevices.enumerateDevices;
res.test1_same_ref = (a === b);
if (a === b) {
a.unjzomuybtbyyhwwkdpkxomylnab = 'tag';
res.test1_tag_persists = (b.unjzomuybtbyyhwwkdpkxomylnab === 'tag');
delete a.unjzomuybtbyyhwwkdpkxomylnab;
} else {
res.test1_tag_persists = 'skipped (refs differ)';
}
} catch (e) { res.test1_err = e.message; }
// Test 2: iframe contentWindow getOwnPropertyDescriptor.value identity
try {
const iframe = document.createElement('iframe');
document.body && document.body.appendChild(iframe);
const w = iframe.contentWindow;
if (!w) { res.test2_skipped = 'no contentWindow'; }
else {
const d1 = Object.getOwnPropertyDescriptor(w, 'Function');
const d2 = Object.getOwnPropertyDescriptor(w, 'Function');
if (!d1 || !d2) { res.test2_skipped = 'no Function descriptor'; }
else {
res.test2_same_ref = (d1.value === d2.value);
if (d1.value === d2.value && d1.value) {
d1.value.unjzomuybtbyyhwwkdpkxomylnab = 'tag';
res.test2_tag_persists = (d2.value.unjzomuybtbyyhwwkdpkxomylnab === 'tag');
try { delete d1.value.unjzomuybtbyyhwwkdpkxomylnab; } catch (_) {}
} else {
res.test2_tag_persists = 'skipped (refs differ)';
}
}
}
} catch (e) { res.test2_err = e.message; }
// Test 3: Navigator method identity via Object.getOwnPropertyDescriptor
try {
const proto = navigator.constructor.prototype;
const d1 = Object.getOwnPropertyDescriptor(proto, 'sendBeacon');
const d2 = Object.getOwnPropertyDescriptor(proto, 'sendBeacon');
if (!d1 || !d2) { res.test3_skipped = 'no sendBeacon descriptor'; }
else {
res.test3_same_ref = (d1.value === d2.value);
if (d1.value === d2.value && d1.value) {
d1.value.unjzomuybtbyyhwwkdpkxomylnab = 'tag';
res.test3_tag_persists = (d2.value.unjzomuybtbyyhwwkdpkxomylnab === 'tag');
try { delete d1.value.unjzomuybtbyyhwwkdpkxomylnab; } catch (_) {}
} else {
res.test3_tag_persists = 'skipped (refs differ)';
}
}
} catch (e) { res.test3_err = e.message; }
// Test 4 (bonus): navigator method via direct property access (the way
// most JS uses it). If THIS one differs while test 3 passes, the issue
// is in our descriptor wrapper, not the underlying function.
try {
const a = navigator.sendBeacon;
const b = navigator.sendBeacon;
res.test4_same_ref = (a === b);
if (a === b) {
a.unjzomuybtbyyhwwkdpkxomylnab = 'tag';
res.test4_tag_persists = (b.unjzomuybtbyyhwwkdpkxomylnab === 'tag');
try { delete a.unjzomuybtbyyhwwkdpkxomylnab; } catch (_) {}
}
} catch (e) { res.test4_err = e.message; }
// Test 5 (bonus): WebGL parameter — the vendor specifically tags
// WebGLRenderingContext.prototype.getParameter via the canvas probe.
try {
const c = document.createElement('canvas');
const g1 = c.getContext('webgl');
const g2 = c.getContext('webgl');
res.test5_ctx_same = (g1 === g2);
if (g1 && g2) {
const fn1 = g1.getParameter;
const fn2 = g2.getParameter;
res.test5_method_same = (fn1 === fn2);
}
} catch (e) { res.test5_err = e.message; }
return JSON.stringify(res, null, 2);
})()
"#;
let result = page.evaluate(js).unwrap();
println!("FUNCTION IDENTITY CHECK:\n{}", result);
assert!(
result.contains("\"test1_same_ref\": true"),
"FAIL test1: navigator.mediaDevices.enumerateDevices returns DIFFERENT object on re-access — divergence site #1"
);
assert!(
result.contains("\"test1_tag_persists\": true")
|| result.contains("\"test1_tag_persists\": \"skipped (refs differ)\""),
"FAIL test1: tag did NOT persist on enumerateDevices re-access — even though refs were equal"
);
assert!(
result.contains("\"test3_same_ref\": true") || result.contains("\"test3_skipped\""),
"FAIL test3: Navigator.sendBeacon descriptor.value returns DIFFERENT objects on re-access"
);
assert!(
result.contains("\"test4_same_ref\": true"),
"FAIL test4: direct navigator.sendBeacon returns DIFFERENT objects on re-access"
);
}
#[tokio::test]
async fn check_ios_safari_surface() {
let profile = browser_oxide::stealth::presets::iphone_15_pro_safari_18();
let mut page = Page::from_html_with_url(&html(""), "https://example.com/", Some(profile))
.await
.unwrap();
let js = r#"
(function() {
const res = {};
// 16 declined APIs — all must be absent on iOS profile
const declined = [
"Bluetooth", "USB", "Serial", "HID", "Sensor", "Accelerometer",
"Gyroscope", "Magnetometer", "NetworkInformation", "BatteryManager",
"IdleDetector",
];
res.declined_apis_present = declined.filter(k => typeof globalThis[k] !== "undefined");
// Navigator absences
res.bluetooth_typeof = typeof navigator.bluetooth;
res.usb_typeof = typeof navigator.usb;
res.serial_typeof = typeof navigator.serial;
res.hid_typeof = typeof navigator.hid;
res.getBattery_typeof = typeof navigator.getBattery;
res.connection_typeof = typeof navigator.connection;
res.userAgentData_typeof = typeof navigator.userAgentData;
res.deviceMemory_typeof = typeof navigator.deviceMemory;
res.requestMIDIAccess_typeof = typeof navigator.requestMIDIAccess;
// W1.5 — challenge-vendor UA-consistency check uses the `in`
// operator. The descriptor itself must
// not exist, not just return undefined — `typeof` returns
// "undefined" for both cases but `'X' in navigator` distinguishes
// them. This is the load-bearing check for wayfair/zillow/trulia/bloomberg.
res.chrome_in_window = ('chrome' in globalThis);
res.userActivation_in_navigator = ('userActivation' in navigator);
res.deviceMemory_in_navigator = ('deviceMemory' in navigator);
res.connection_in_navigator = ('connection' in navigator);
res.scheduling_in_navigator = ('scheduling' in navigator);
res.getInstalledRelatedApps_in_navigator = ('getInstalledRelatedApps' in navigator);
res.IdleDetector_in_window = ('IdleDetector' in globalThis);
res.UserActivation_in_window = ('UserActivation' in globalThis);
// PaymentRequest hasEnrolledInstrument MUST be absent (Chrome-only)
res.hasEnrolledInstrument_present = typeof PaymentRequest === "function"
? typeof PaymentRequest.prototype.hasEnrolledInstrument !== "undefined"
: "no_PaymentRequest";
// iOS-only globals
res.window_orientation = typeof globalThis.orientation;
res.window_orientation_value = globalThis.orientation;
res.ontouchstart_present = "ontouchstart" in globalThis;
// DeviceMotionEvent.requestPermission must exist (iOS 13+ tell)
res.deviceMotion_requestPermission =
typeof DeviceMotionEvent !== "undefined"
&& typeof DeviceMotionEvent.requestPermission;
res.deviceOrientation_requestPermission =
typeof DeviceOrientationEvent !== "undefined"
&& typeof DeviceOrientationEvent.requestPermission;
// Profile-driven values from preset
res.platform = navigator.platform;
res.maxTouchPoints = navigator.maxTouchPoints;
res.hardwareConcurrency = navigator.hardwareConcurrency;
res.userAgent = navigator.userAgent;
return JSON.stringify(res, null, 2);
})()
"#;
let result = page.evaluate(js).unwrap();
println!("iOS SAFARI SURFACE CHECK:\n{}", result);
assert!(
result.contains("\"declined_apis_present\": []"),
"iOS profile must strip all 16 declined APIs, got: {result}"
);
assert!(result.contains("\"bluetooth_typeof\": \"undefined\""));
assert!(result.contains("\"usb_typeof\": \"undefined\""));
assert!(result.contains("\"serial_typeof\": \"undefined\""));
assert!(result.contains("\"hid_typeof\": \"undefined\""));
assert!(result.contains("\"getBattery_typeof\": \"undefined\""));
assert!(result.contains("\"connection_typeof\": \"undefined\""));
assert!(result.contains("\"userAgentData_typeof\": \"undefined\""));
assert!(result.contains("\"deviceMemory_typeof\": \"undefined\""));
for (key, label) in [
(
"\"chrome_in_window\": false",
"window.chrome must be absent",
),
(
"\"userActivation_in_navigator\": false",
"navigator.userActivation must be absent",
),
(
"\"deviceMemory_in_navigator\": false",
"navigator.deviceMemory must be absent",
),
(
"\"connection_in_navigator\": false",
"navigator.connection must be absent",
),
(
"\"scheduling_in_navigator\": false",
"navigator.scheduling must be absent",
),
(
"\"getInstalledRelatedApps_in_navigator\": false",
"navigator.getInstalledRelatedApps must be absent",
),
(
"\"IdleDetector_in_window\": false",
"globalThis.IdleDetector must be absent",
),
(
"\"UserActivation_in_window\": false",
"globalThis.UserActivation must be absent",
),
] {
assert!(result.contains(key), "iOS surface: {label} (got: {result})");
}
assert!(
result.contains("\"hasEnrolledInstrument_present\": false"),
"iOS profile must NOT expose PaymentRequest.prototype.hasEnrolledInstrument (Chrome/Edge-only): {result}"
);
assert!(result.contains("\"window_orientation\": \"number\""));
assert!(result.contains("\"window_orientation_value\": 0"));
assert!(result.contains("\"ontouchstart_present\": true"));
assert!(
result.contains("\"deviceMotion_requestPermission\": \"function\""),
"iOS profile must expose DeviceMotionEvent.requestPermission static"
);
assert!(
result.contains("\"deviceOrientation_requestPermission\": \"function\""),
"iOS profile must expose DeviceOrientationEvent.requestPermission static"
);
assert!(result.contains("\"platform\": \"iPhone\""));
assert!(result.contains("\"maxTouchPoints\": 5"));
assert!(
result.contains("\"hardwareConcurrency\": 2"),
"Safari intentionally caps hardwareConcurrency to 2 — got: {result}"
);
assert!(result.contains("\"userAgent\":") && result.contains("iPhone"));
}
#[tokio::test]
async fn window_frame_registry_after_append() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
let js = r#"
(function(){
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const len = window.length;
const w0 = window[0];
let wd = 'ERR_NO_W0';
try { wd = String(w0 && w0.navigator && w0.navigator.webdriver); }
catch(e) { wd = 'ERR:'+e.message; }
return JSON.stringify({
windowLength: len,
hasW0: typeof w0 !== 'undefined',
w0Type: typeof w0,
webdriver: wd,
});
})()
"#;
let result = page.evaluate(js).unwrap_or_else(|e| format!("ERROR: {e}"));
eprintln!("window-frame-registry: {result}");
assert!(!result.starts_with("ERROR:"), "eval failed: {result}");
assert!(
result.contains("\"windowLength\":1"),
"window.length must be 1 after append: {result}"
);
assert!(
result.contains("\"hasW0\":true"),
"window[0] must be defined after append: {result}"
);
assert!(
!result.contains("ERR:"),
"window[0].navigator.webdriver must not throw: {result}"
);
}
#[tokio::test]
#[allow(non_snake_case, reason = "mirrors JS API name under test")]
async fn pluginarray_namedItem_must_show_native_code() {
let result = check_secure(
r#"
const fn = navigator.plugins.namedItem;
const s1 = String(fn);
const s2 = fn.toString();
const s3 = Function.prototype.toString.call(fn);
// All three should return the masked native-code shape.
JSON.stringify({
implicit: s1,
instance: s2,
protoCall: s3,
leak_pluginsLen: s1.includes('_pluginsLen'),
leak_allPlugins: s1.includes('_allPlugins'),
});
"#,
)
.await;
eprintln!("namedItem audit: {result}");
assert!(
result.contains("[native code]"),
"namedItem must mask to native shape; got: {result}"
);
assert!(
result.contains("\"leak_pluginsLen\":false"),
"namedItem leaks _pluginsLen identifier — toString masking broken: {result}"
);
assert!(
result.contains("\"leak_allPlugins\":false"),
"namedItem leaks _allPlugins identifier — toString masking broken: {result}"
);
}
#[tokio::test]
async fn child_realm_navigator_plugins_accessible() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
let js = r#"
(function(){
const ifr = document.createElement('iframe');
document.body.appendChild(ifr);
const cw = ifr.contentWindow;
let pluginsLen = 'ERR_NO_PLUGINS';
let mimeLen = 'ERR_NO_MIME';
try { pluginsLen = cw.navigator.plugins.length; } catch(e) { pluginsLen = 'ERR:'+e.message; }
try { mimeLen = cw.navigator.mimeTypes.length; } catch(e) { mimeLen = 'ERR:'+e.message; }
return JSON.stringify({
parentPlugins: navigator.plugins.length,
childPlugins: pluginsLen,
parentMime: navigator.mimeTypes.length,
childMime: mimeLen,
});
})()
"#;
let result = page.evaluate(js).unwrap_or_else(|e| format!("ERROR: {e}"));
eprintln!("child-realm-navigator-plugins: {result}");
assert!(!result.starts_with("ERROR:"), "eval failed: {result}");
assert!(
result.contains("\"childPlugins\":5"),
"child realm must have 5 plugins: {result}"
);
assert!(
result.contains("\"childMime\":2"),
"child realm must have 2 mimeTypes: {result}"
);
}
#[tokio::test]
async fn iframe_contentwindow_navigator_webdriver_no_throw() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
let js = r#"
(function(){
const ifr = document.createElement('iframe');
document.body.appendChild(ifr);
const cw = ifr.contentWindow;
let r = {};
r.hasCW = typeof cw !== 'undefined';
try { r.navType = typeof cw.navigator; } catch(e) { r.navType = 'ERR:'+e.message; }
try {
const nav = cw.navigator;
r.wdVal = String(nav.webdriver);
} catch(e) { r.wdVal = 'ERR:'+e.message; }
try {
const nav = cw.navigator;
const desc = Object.getOwnPropertyDescriptor(nav, 'webdriver');
r.descDefined = desc !== undefined;
} catch(e) { r.descDefined = 'ERR:'+e.message; }
return JSON.stringify(r);
})()
"#;
let result = page.evaluate(js).unwrap_or_else(|e| format!("ERROR: {e}"));
eprintln!("ifw-probe-sim: {result}");
assert!(!result.starts_with("ERROR:"), "eval failed: {result}");
assert!(
result.contains("\"hasCW\":true"),
"iframe.contentWindow must exist: {result}"
);
assert!(
result.contains("\"navType\":\"object\""),
"iframe.contentWindow.navigator must be object: {result}"
);
assert!(
!result.contains("ERR:"),
"no property access must throw: {result}"
);
}
#[tokio::test]
async fn iframe_postmessage_round_trip_via_proxy() {
let mut page = Page::from_html_with_url(
&html(""),
"https://example.com/",
None::<browser_oxide::stealth::StealthProfile>,
)
.await
.unwrap();
let setup = r#"
globalThis.__rt = { parentGot: null, sourceIsCW: null, hasCw: null };
const ifr = document.createElement('iframe');
ifr.srcdoc = "<scr" + "ipt>window.addEventListener('message', function(e){ try { e.source.postMessage('echo:' + e.data, '*'); } catch(_){} });</scr" + "ipt>";
document.body.appendChild(ifr);
const cw = ifr.contentWindow;
globalThis.__rt.hasCw = (cw != null);
window.addEventListener('message', function(e){
if (typeof e.data === 'string' && e.data.indexOf('echo:') === 0) {
globalThis.__rt.parentGot = e.data;
globalThis.__rt.sourceIsCW = (e.source === cw);
}
});
if (cw) cw.postMessage('hello', '*');
"#;
let _ = page
.evaluate_async(setup, std::time::Duration::from_secs(5))
.await;
let result = page
.evaluate("JSON.stringify(globalThis.__rt)")
.unwrap_or_default();
eprintln!("iframe postmessage round-trip: {result}");
assert!(
result.contains("\"hasCw\":true"),
"no contentWindow: {result}"
);
assert!(
result.contains("echo:hello"),
"bidirectional iframe postMessage failed (child listener never fired or reply not delivered): {result}"
);
assert!(
result.contains("\"sourceIsCW\":true"),
"event.source !== iframe.contentWindow on the reply: {result}"
);
}
#[tokio::test]
async fn location_origin_secure_page() {
let result = check_secure(r#"JSON.stringify({
origin: location.origin,
url_ok: (()=>{try{return new URL('https://x.com/').origin;}catch(e){return 'ERR:'+e.message;}})()
})"#).await;
eprintln!("loc-origin: {result}");
assert_eq!(
result, r#"{"origin":"https://example.com","url_ok":"https://x.com"}"#,
"location.origin or URL broken: {result}"
);
}
#[tokio::test]
#[ignore]
async fn native_code_mask_audit() {
let js = r#"
(() => {
const NATIVE_RE = /^function [A-Za-z_$][A-Za-z0-9_$]*\(\) \{ \[native code\] \}$/;
const SKIP_PROTOS = new Set([Object.prototype, Function.prototype]);
const failuresByIface = {};
const totalNames = Object.getOwnPropertyNames(globalThis);
for (const name of totalNames) {
let v;
try { v = globalThis[name]; } catch (e) { continue; }
if (typeof v !== 'function') continue;
const proto = v.prototype;
if (!proto || SKIP_PROTOS.has(proto)) continue;
let mnames;
try { mnames = Object.getOwnPropertyNames(proto); } catch (e) { continue; }
for (const mname of mnames) {
if (mname === 'constructor') continue;
let desc;
try { desc = Object.getOwnPropertyDescriptor(proto, mname); } catch (e) { continue; }
if (!desc || typeof desc.value !== 'function') continue;
let s;
try { s = String(desc.value); } catch (e) { continue; }
if (!NATIVE_RE.test(s)) {
(failuresByIface[name] = failuresByIface[name] || []).push({method: mname, got: s.slice(0, 100)});
}
}
}
// Flat list + per-iface counts
const flat = [];
const counts = {};
for (const iface of Object.keys(failuresByIface).sort()) {
counts[iface] = failuresByIface[iface].length;
for (const f of failuresByIface[iface]) flat.push({iface, ...f});
}
return JSON.stringify({count: flat.length, counts, failures: flat});
})()
"#;
let result = check(js).await;
let v: serde_json::Value = serde_json::from_str(&result)
.unwrap_or_else(|e| panic!("audit json parse: {e}; raw={result}"));
let count = v["count"].as_u64().unwrap_or(0);
if count > 0 {
let counts_pretty = serde_json::to_string_pretty(&v["counts"]).unwrap();
let dump = serde_json::to_string_pretty(&v["failures"]).unwrap();
panic!(
"native_code_mask_audit: {count} total failures.\nPer-interface counts:\n{counts_pretty}\nFull list:\n{dump}"
);
}
}
const CANVAS_FP_SEQUENCE_JS: &str = r#"(() => {
const c = document.createElement('canvas');
c.width = 200; c.height = 60;
const ctx = c.getContext('2d');
if (!ctx) return 'NO_CTX';
// FingerprintJS-style canonical sequence
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, 100, 30);
ctx.fillStyle = '#069';
ctx.fillText('browser_oxide', 2, 15);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.fillText('parity-test', 4, 17);
ctx.beginPath();
ctx.arc(150, 30, 12, 0, Math.PI * 2);
ctx.fill();
return c.toDataURL();
})()"#;
async fn canvas_hash_for(profile: browser_oxide::stealth::StealthProfile) -> String {
let mut page = Page::from_html(
"<!DOCTYPE html><html><head></head><body></body></html>",
Some(profile),
)
.await
.unwrap();
let data_url = page.evaluate(CANVAS_FP_SEQUENCE_JS).unwrap();
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(data_url.as_bytes());
format!("{:x}", h.finalize())
}
#[tokio::test]
async fn canvas_todataurl_deterministic_within_profile() {
let a = canvas_hash_for(browser_oxide::stealth::presets::chrome_148_macos()).await;
let b = canvas_hash_for(browser_oxide::stealth::presets::chrome_148_macos()).await;
assert_eq!(
a, b,
"two fresh pages with the same profile must hash to the same toDataURL: a={a} b={b}"
);
}
#[tokio::test]
async fn canvas_todataurl_differs_across_profiles() {
let mac = canvas_hash_for(browser_oxide::stealth::presets::chrome_148_macos()).await;
let win = canvas_hash_for(browser_oxide::stealth::presets::chrome_148_windows()).await;
let lin = canvas_hash_for(browser_oxide::stealth::presets::chrome_148_linux()).await;
let pairs_equal = (mac == win) as u32 + (mac == lin) as u32 + (win == lin) as u32;
assert!(
pairs_equal <= 1,
"≥2 of 3 profiles produced identical canvas hashes (per-profile uniqueness broken). \
mac={mac} win={win} lin={lin}"
);
}
async fn webgl_unmasked_for(profile: browser_oxide::stealth::StealthProfile) -> serde_json::Value {
let mut page = Page::from_html(
"<!DOCTYPE html><html><head></head><body><canvas id='c'></canvas></body></html>",
Some(profile),
)
.await
.unwrap();
let result = page
.evaluate(
r#"(() => {
const c = document.getElementById('c');
const gl = c.getContext('webgl');
if (!gl) return JSON.stringify({err: 'no gl'});
return JSON.stringify({
vendor: gl.getParameter(0x1F00),
renderer: gl.getParameter(0x1F01),
unmaskedVendor: gl.getParameter(0x9245),
unmaskedRenderer: gl.getParameter(0x9246),
});
})()"#,
)
.unwrap();
serde_json::from_str(&result).unwrap_or_else(|e| panic!("json: {e}; raw={result}"))
}
#[tokio::test]
async fn webgl_param_golden_snapshot_chrome_148_macos() {
let profile = browser_oxide::stealth::presets::chrome_148_macos();
let want_v = profile.gpu_profile.unmasked_vendor.clone();
let want_r = profile.gpu_profile.unmasked_renderer.clone();
let v = webgl_unmasked_for(profile).await;
assert_eq!(v["unmaskedVendor"], want_v, "UNMASKED_VENDOR drift: {v}");
assert_eq!(
v["unmaskedRenderer"], want_r,
"UNMASKED_RENDERER drift: {v}"
);
}
#[tokio::test]
async fn webgl_param_golden_snapshot_chrome_148_windows() {
let profile = browser_oxide::stealth::presets::chrome_148_windows();
let want_v = profile.gpu_profile.unmasked_vendor.clone();
let want_r = profile.gpu_profile.unmasked_renderer.clone();
let v = webgl_unmasked_for(profile).await;
assert_eq!(v["unmaskedVendor"], want_v, "drift: {v}");
assert_eq!(v["unmaskedRenderer"], want_r, "drift: {v}");
}
#[tokio::test]
async fn webgl_param_golden_snapshot_chrome_148_linux() {
let profile = browser_oxide::stealth::presets::chrome_148_linux();
let want_v = profile.gpu_profile.unmasked_vendor.clone();
let want_r = profile.gpu_profile.unmasked_renderer.clone();
let v = webgl_unmasked_for(profile).await;
assert_eq!(v["unmaskedVendor"], want_v, "drift: {v}");
assert_eq!(v["unmaskedRenderer"], want_r, "drift: {v}");
}
#[tokio::test]
async fn keystroke_schedule_slot_installed_and_monotonic() {
let result = check(
r#"
(() => {
const fn = globalThis[Symbol.for('__browser_oxide_keystroke_schedule__')];
if (typeof fn !== 'function') return JSON.stringify({err: 'slot missing'});
const sch = fn('abc', 50);
if (!Array.isArray(sch) || sch.length === 0) return JSON.stringify({err: 'empty schedule', sch});
let monotonic = true;
let prevUp = 0;
for (const s of sch) {
if (!(s.down_ms >= prevUp - 0.0001 && s.up_ms > s.down_ms)) monotonic = false;
prevUp = s.up_ms;
}
return JSON.stringify({
length: sch.length,
first: sch[0],
last: sch[sch.length - 1],
monotonic,
codes: sch.map(s => s.code),
});
})()
"#,
)
.await;
let v: serde_json::Value =
serde_json::from_str(&result).unwrap_or_else(|e| panic!("json: {e}; raw={result}"));
assert_eq!(
v["length"].as_u64().unwrap(),
3,
"expected 3 entries: {result}"
);
assert_eq!(v["monotonic"], true, "schedule not monotonic: {result}");
assert_eq!(v["codes"][0], "KeyA");
assert_eq!(v["codes"][1], "KeyB");
assert_eq!(v["codes"][2], "KeyC");
let first_down = v["first"]["down_ms"].as_f64().unwrap();
assert!(first_down >= 0.0, "first down_ms must be ≥ 0: {first_down}");
}
#[tokio::test]
async fn behavior_rand_slot_installed_and_in_unit_range() {
let result = check(
r#"
(() => {
const sym = Symbol.for('__browser_oxide_behavior_rand__');
const fn = globalThis[sym];
if (typeof fn !== 'function') return JSON.stringify({err: 'slot missing'});
const a = fn();
const b = fn();
const c = fn();
return JSON.stringify({
type: typeof fn,
inRange: (a >= 0 && a < 1) && (b >= 0 && b < 1) && (c >= 0 && c < 1),
advanced: !(a === b && b === c),
a, b, c,
});
})()
"#,
)
.await;
let v: serde_json::Value =
serde_json::from_str(&result).unwrap_or_else(|e| panic!("json: {e}; raw={result}"));
assert_eq!(v["type"], "function", "slot must be a function: {result}");
assert_eq!(v["inRange"], true, "values out of [0,1): {result}");
assert_eq!(v["advanced"], true, "sequence not advancing: {result}");
}
#[tokio::test]
async fn message_channel_paired_routing() {
let mut page = Page::from_html(&html(""), None::<browser_oxide::stealth::StealthProfile>)
.await
.unwrap();
let _ = page.evaluate(
r#"(() => {
globalThis.__mctest = { got: [] };
const ch = new MessageChannel();
globalThis.__mctest.ch = ch;
ch.port2.onmessage = (e) => { globalThis.__mctest.got.push(e.data); };
ch.port1.postMessage('hello');
ch.port1.postMessage({n: 42});
})()"#,
);
for _ in 0..10 {
let _ = page
.event_loop()
.run_until_idle(std::time::Duration::from_millis(10))
.await;
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
let result = page
.evaluate("JSON.stringify({len: globalThis.__mctest.got.length, first: globalThis.__mctest.got[0], second: globalThis.__mctest.got[1] && globalThis.__mctest.got[1].n})")
.unwrap();
let v: serde_json::Value =
serde_json::from_str(&result).unwrap_or_else(|e| panic!("json: {e}; raw={result}"));
assert_eq!(
v["len"].as_u64().unwrap(),
2,
"paired delivery failed: {result}"
);
assert_eq!(v["first"], "hello");
assert_eq!(v["second"].as_u64().unwrap(), 42);
}
#[tokio::test]
async fn message_channel_queue_then_start() {
let mut page = Page::from_html(&html(""), None::<browser_oxide::stealth::StealthProfile>)
.await
.unwrap();
let _ = page.evaluate(
r#"(() => {
globalThis.__mctest = { got: [] };
const ch = new MessageChannel();
globalThis.__mctest.ch = ch;
ch.port1.postMessage('queued-1');
ch.port1.postMessage('queued-2');
})()"#,
);
let pre = page
.evaluate("globalThis.__mctest.got.length")
.unwrap_or_default();
let _ = page.evaluate(
r#"(() => {
const ch = globalThis.__mctest.ch;
ch.port2.onmessage = (e) => { globalThis.__mctest.got.push(e.data); };
ch.port2.start();
})()"#,
);
for _ in 0..10 {
let _ = page
.event_loop()
.run_until_idle(std::time::Duration::from_millis(10))
.await;
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
let post = page
.evaluate("globalThis.__mctest.got.length")
.unwrap_or_default();
assert_eq!(pre, "0", "queued msgs should not deliver pre-start: {pre}");
assert_eq!(post, "2", "start should drain queue: {post}");
}
#[tokio::test]
async fn message_channel_close_detaches() {
let mut page = Page::from_html(&html(""), None::<browser_oxide::stealth::StealthProfile>)
.await
.unwrap();
let _ = page.evaluate(
r#"(() => {
globalThis.__mctest = { got: [] };
const ch = new MessageChannel();
ch.port2.onmessage = (e) => { globalThis.__mctest.got.push(e.data); };
ch.port1.close();
ch.port1.postMessage('after-close');
})()"#,
);
let len = page
.evaluate("globalThis.__mctest.got.length")
.unwrap_or_default();
assert_eq!(len, "0", "post-close postMessage must not deliver: {len}");
}
#[tokio::test]
async fn raf_cadence_jitter() {
let result = check(
r#"
(() => {
const fn = globalThis[Symbol.for('__browser_oxide_raf_jitter_ms__')];
if (typeof fn !== 'function') return JSON.stringify({err: 'sampler missing'});
const n = 1000;
const xs = new Array(n);
for (let i = 0; i < n; i++) xs[i] = fn();
const mean = xs.reduce((a,b) => a + b, 0) / n;
const variance = xs.reduce((a,b) => a + (b - mean) ** 2, 0) / n;
const stddev = Math.sqrt(variance);
let mn = Infinity, mx = -Infinity;
for (const v of xs) { if (v < mn) mn = v; if (v > mx) mx = v; }
return JSON.stringify({mean, stddev, min: mn, max: mx, n});
})()
"#,
)
.await;
let v: serde_json::Value =
serde_json::from_str(&result).unwrap_or_else(|e| panic!("json: {e}; raw={result}"));
let mean = v["mean"].as_f64().unwrap();
let stddev = v["stddev"].as_f64().unwrap();
let max = v["max"].as_f64().unwrap();
let min = v["min"].as_f64().unwrap();
assert!(
(mean - 16.67).abs() < 0.2,
"mean must track 16.67 ± 0.2 ms (got {mean}). raw={v}"
);
assert!(stddev > 0.2, "stddev too low: {stddev} ms. raw={v}");
assert!(max < 33.0, "max ≥ 33 ms (frame skip): {max} ms. raw={v}");
assert!(min >= 1.0, "min < 1 ms (clamp broke): {min} ms. raw={v}");
}
#[tokio::test]
async fn perf_origin_now_consistency() {
let result = check(
r#"
const to = performance.timeOrigin;
const nv = performance.now();
const dn = Date.now();
JSON.stringify({to, nv, dn, drift: to + nv - dn})
"#,
)
.await;
let v: serde_json::Value =
serde_json::from_str(&result).unwrap_or_else(|e| panic!("json: {e}; raw={result}"));
let drift = v["drift"].as_f64().unwrap_or(f64::INFINITY);
eprintln!("perf-origin probe: {v}");
assert!(
drift.abs() < 10.0,
"timeOrigin + now() vs Date.now() drift = {drift} ms (>= 10 ms threshold). raw={v}"
);
}
#[tokio::test]
async fn form_elements_collection() {
let body = r#"
<form id="f">
<input type="hidden" name="solution" value="">
<input type="text" name="user">
<input type="submit" value="Go">
<select name="kind"><option value="a">A</option></select>
<textarea name="notes"></textarea>
<button type="button" id="btn">Click</button>
</form>
"#;
let mut page = Page::from_html(&html(body), None::<browser_oxide::stealth::StealthProfile>)
.await
.unwrap();
let js = r#"
(() => {
const form = document.forms[0] || document.getElementById('f');
if (!form) return JSON.stringify({err: 'no form'});
const els = form.elements;
if (!els) return JSON.stringify({err: 'no elements getter'});
const sol = els.namedItem('solution');
const usr = els.namedItem('user');
const item0 = els.item(0);
let iterCount = 0;
try { for (const _ of els) iterCount++; } catch (e) {}
return JSON.stringify({
length: els.length,
solName: sol && sol.name,
solTag: sol && sol.tagName,
usrName: usr && usr.name,
item0Tag: item0 && item0.tagName,
iterCount,
missing: els.namedItem('nope'),
});
})()
"#;
let result = page
.evaluate(js)
.unwrap_or_else(|e| panic!("evaluate: {e}"));
let v: serde_json::Value =
serde_json::from_str(&result).unwrap_or_else(|e| panic!("json: {e}; raw={result}"));
assert_eq!(v["solName"], "solution", "namedItem('solution') wrong");
assert_eq!(v["solTag"], "INPUT", "tagName wrong");
assert_eq!(v["usrName"], "user");
assert!(v["length"].as_u64().unwrap() >= 5, "length < 5: {result}");
assert!(v["item0Tag"].as_str().is_some(), "item(0) failed");
assert!(v["iterCount"].as_u64().unwrap() >= 5, "iteration count low");
assert!(v["missing"].is_null(), "namedItem('nope') should be null");
}
#[tokio::test]
async fn file_reader_read_as_data_url_encodes_blob_bytes() {
let js = r#"
(() => {
const blob = new Blob([new Uint8Array([72,101,108,108,111,33])], { type: 'text/plain' });
const r = new FileReader();
r.readAsDataURL(blob);
return JSON.stringify({ result: r.result, state: r.readyState });
})()
"#;
let raw = check(js).await;
let v: serde_json::Value =
serde_json::from_str(&raw).unwrap_or_else(|e| panic!("json: {e}; raw={raw}"));
assert_eq!(
v["result"], "data:text/plain;base64,SGVsbG8h",
"readAsDataURL must base64-encode blob bytes with the blob's MIME type: {raw}"
);
assert_eq!(v["state"], 2, "readyState must be DONE after read");
}
#[tokio::test]
async fn file_reader_read_as_data_url_default_mime() {
let js = r#"
(() => {
const blob = new Blob([new Uint8Array([0xff, 0x00, 0x42])]);
const r = new FileReader();
r.readAsDataURL(blob);
return r.result;
})()
"#;
assert_eq!(check(js).await, "data:application/octet-stream;base64,/wBC");
}
#[tokio::test]
async fn file_reader_read_as_array_buffer_copies_blob_bytes() {
let js = r#"
(() => {
const blob = new Blob([new Uint8Array([1,2,3,4,5,6,7,8])]);
const r = new FileReader();
r.readAsArrayBuffer(blob);
const view = new Uint8Array(r.result);
return JSON.stringify({ len: view.byteLength, bytes: Array.from(view) });
})()
"#;
let raw = check(js).await;
let v: serde_json::Value =
serde_json::from_str(&raw).unwrap_or_else(|e| panic!("json: {e}; raw={raw}"));
assert_eq!(v["len"], 8);
assert_eq!(v["bytes"], serde_json::json!([1, 2, 3, 4, 5, 6, 7, 8]));
}
#[tokio::test]
async fn file_reader_read_as_text_decodes_utf8() {
let js = r#"
(() => {
const blob = new Blob(['héllo'], { type: 'text/plain' });
const r = new FileReader();
r.readAsText(blob);
return r.result;
})()
"#;
assert_eq!(check(js).await, "héllo");
}