From 534de778822fb436a9f5c1db1bd986982d204a96 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:47:23 +0000 Subject: [PATCH] Add EventTarget methods to navigator.connection and serviceWorker NetworkInformation (navigator.connection) and ServiceWorkerContainer (navigator.serviceWorker) are EventTargets in real browsers. React 18 and mobile SPAs call addEventListener on them; the stubs lacked it, so the app bundle crashed with 'addEventListener is not a function' (e.g. goofish.com). --- crates/obscura-js/js/bootstrap.js | 4 ++-- crates/obscura-js/src/runtime.rs | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 8db55ff..1ba9463 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -2469,7 +2469,7 @@ globalThis.navigator = { vendor: "Google Inc.", product: "Gecko", productSub: "20030107", doNotTrack: null, deviceMemory: 8, - connection: { effectiveType: "4g", rtt: 50, downlink: 10, saveData: false }, + connection: { effectiveType: "4g", rtt: 50, downlink: 10, saveData: false, onchange: null, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} }, get webdriver() { return false; }, pdfViewerEnabled: true, get plugins() { @@ -2518,7 +2518,7 @@ globalThis.navigator = { }, toJSON() { return {brands:this.brands,mobile:this.mobile,platform:this.platform}; }, }, - serviceWorker: { ready: Promise.resolve(), register(){return Promise.resolve();}, getRegistrations(){return Promise.resolve([]);}, controller: null }, + serviceWorker: { ready: Promise.resolve(), register(){return Promise.resolve();}, getRegistrations(){return Promise.resolve([]);}, controller: null, oncontrollerchange: null, onmessage: null, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} }, mediaDevices: { enumerateDevices() { return Promise.resolve([ diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index 22daca0..92908d7 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -2357,6 +2357,27 @@ mod tests { /// Setting an ARIA reflection property must write through to the /// underlying attribute so frameworks that toggle state via /// `el.ariaExpanded = 'true'` actually update the DOM. + /// Regression: React 18 / mobile SPAs (e.g. goofish.com) call + /// addEventListener on navigator.connection (NetworkInformation) and + /// navigator.serviceWorker (ServiceWorkerContainer). Both are EventTargets + /// in real browsers; missing the method crashed the app bundle with + /// "addEventListener is not a function". + #[test] + fn navigator_eventtarget_stubs_expose_add_event_listener() { + let mut rt = setup_runtime("
"); + let result = rt + .evaluate( + r#" + return [ + typeof navigator.connection.addEventListener, + typeof navigator.serviceWorker.addEventListener, + ]; + "#, + ) + .unwrap(); + assert_eq!(result, serde_json::json!(["function", "function"])); + } + /// Regression test for #285: DDoS-Guard's challenge calls /// `t.insertAdjacentText(...)` and dies with `TypeError: ... is not a /// function` because `Element.prototype.insertAdjacentText` was missing.