diff --git a/crates/js/src/iframe_bridge.rs b/crates/js/src/iframe_bridge.rs index cf5f003..a3bcf0d 100644 --- a/crates/js/src/iframe_bridge.rs +++ b/crates/js/src/iframe_bridge.rs @@ -193,6 +193,17 @@ pub fn init_window_object(vm: &mut Vm, context_id: &str, origin: &str) { Value::Function(dispatch_event), ); + // window.screen — Screen interface with conservative HD-desktop defaults. + let screen_ref = crate::screen::create_screen_object(&mut vm.gc, &mut vm.shapes); + set_builtin_prop( + &mut vm.gc, + &mut vm.shapes, + window_ref, + "screen", + Value::Object(screen_ref), + ); + vm.set_global("screen", Value::Object(screen_ref)); + // Set as global. vm.set_global("window", Value::Object(window_ref)); diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 73d5e6f..1cb876f 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -24,6 +24,7 @@ pub mod media_query; pub mod parser; pub mod performance; pub mod regex; +pub mod screen; pub mod service_worker; pub mod service_worker_global; pub mod shape; diff --git a/crates/js/src/screen.rs b/crates/js/src/screen.rs new file mode 100644 index 0000000..e7d2804 --- /dev/null +++ b/crates/js/src/screen.rs @@ -0,0 +1,114 @@ +//! `window.screen` — minimal Screen object for analytics/UA-detection paths. +//! +//! Real-world bundles read `screen.width` / `screen.height` to gate desktop vs. +//! tablet detection (e.g. Bing's `PuxwB9k...js` does +//! `Math.max(_w.screen.width, _w.screen.height)`). Without this object the +//! script throws "Cannot read properties of undefined (reading 'width')" and +//! later hydration steps that depend on the same bundle silently skip. +//! +//! `we` is headless and has no notion of a physical display, so the width / +//! height values are conservative HD-desktop defaults. They are not viewport +//! aware on purpose — in a real browser `screen` reports the display, not the +//! window. Pages that want the window size read `innerWidth` / `innerHeight` +//! instead. +//! +//! Spec: + +use crate::builtins::set_builtin_prop; +use crate::gc::{Gc, GcRef}; +use crate::shape::ShapeTable; +use crate::vm::*; + +const DEFAULT_WIDTH: f64 = 1920.0; +const DEFAULT_HEIGHT: f64 = 1080.0; +const DEFAULT_COLOR_DEPTH: f64 = 24.0; + +/// Create a `Screen` object populated with conservative HD-desktop defaults. +pub fn create_screen_object(gc: &mut Gc, shapes: &mut ShapeTable) -> GcRef { + let data = ObjectData::new(); + let screen_ref = gc.alloc(HeapObject::Object(data)); + + for (name, value) in [ + ("width", DEFAULT_WIDTH), + ("height", DEFAULT_HEIGHT), + ("availWidth", DEFAULT_WIDTH), + ("availHeight", DEFAULT_HEIGHT), + ("availLeft", 0.0), + ("availTop", 0.0), + ("colorDepth", DEFAULT_COLOR_DEPTH), + ("pixelDepth", DEFAULT_COLOR_DEPTH), + ] { + set_builtin_prop(gc, shapes, screen_ref, name, Value::Number(value)); + } + + screen_ref +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compiler; + use crate::iframe_bridge; + use crate::parser::Parser; + + fn eval(vm: &mut Vm, source: &str) -> Value { + let program = Parser::parse(source).expect("parse"); + let func = compiler::compile(&program).expect("compile"); + vm.execute(&func).expect("execute") + } + + #[test] + fn screen_width_height_are_available_on_window() { + let mut vm = Vm::new(); + iframe_bridge::init_window_object(&mut vm, "main", "https://example.test"); + + let v = eval(&mut vm, "window.screen.width + 'x' + window.screen.height"); + match v { + Value::String(s) => assert_eq!(s, "1920x1080"), + other => panic!("expected 1920x1080, got {other:?}"), + } + } + + #[test] + fn screen_is_reachable_through_the_window_alias() { + let mut vm = Vm::new(); + iframe_bridge::init_window_object(&mut vm, "main", "https://example.test"); + + // The Bing PuxwB9k bundle reaches screen via the `_w` alias for window. + let v = eval(&mut vm, "Math.max(_w.screen.width, _w.screen.height)"); + match v { + Value::Number(n) => assert_eq!(n, 1920.0), + other => panic!("expected 1920, got {other:?}"), + } + } + + #[test] + fn screen_exposes_avail_and_depth_properties() { + let mut vm = Vm::new(); + iframe_bridge::init_window_object(&mut vm, "main", "https://example.test"); + + let v = eval( + &mut vm, + "screen.availWidth + ':' + screen.availHeight + ':' \ + + screen.availLeft + ':' + screen.availTop + ':' \ + + screen.colorDepth + ':' + screen.pixelDepth", + ); + match v { + Value::String(s) => assert_eq!(s, "1920:1080:0:0:24:24"), + other => panic!("expected avail+depth string, got {other:?}"), + } + } + + #[test] + fn screen_is_exposed_as_a_bare_global() { + let mut vm = Vm::new(); + iframe_bridge::init_window_object(&mut vm, "main", "https://example.test"); + + // Bare `screen` (no `window.` prefix) resolves to the same object. + let v = eval(&mut vm, "screen === window.screen"); + match v { + Value::Boolean(b) => assert!(b), + other => panic!("expected true, got {other:?}"), + } + } +}