//! Minimal `matchMedia` support. //! //! Full media query parsing and live viewport updates belong to the style and //! layout layers. This module provides a conservative `matchMedia()` shape so //! real-world feature-detection and telemetry code can run without treating the //! API as missing. use crate::builtins::{make_native, set_builtin_prop}; use crate::vm::*; fn match_media(args: &[Value], ctx: &mut NativeContext) -> Result { let media = args .first() .map(|v| v.to_js_string(ctx.gc)) .unwrap_or_default(); let mut data = ObjectData::new(); data.insert_property( "matches".to_string(), Property::builtin(Value::Boolean(false)), ctx.shapes, ); data.insert_property( "media".to_string(), Property::builtin(Value::String(media)), ctx.shapes, ); data.insert_property( "onchange".to_string(), Property::data(Value::Null), ctx.shapes, ); let list_ref = ctx.gc.alloc(HeapObject::Object(data)); for name in [ "addListener", "removeListener", "addEventListener", "removeEventListener", ] { let func = make_native(ctx.gc, name, media_query_list_noop); set_builtin_prop(ctx.gc, ctx.shapes, list_ref, name, Value::Function(func)); } let dispatch = make_native(ctx.gc, "dispatchEvent", media_query_list_dispatch_event); set_builtin_prop( ctx.gc, ctx.shapes, list_ref, "dispatchEvent", Value::Function(dispatch), ); Ok(Value::Object(list_ref)) } fn media_query_list_noop(_args: &[Value], _ctx: &mut NativeContext) -> Result { Ok(Value::Undefined) } fn media_query_list_dispatch_event( _args: &[Value], _ctx: &mut NativeContext, ) -> Result { Ok(Value::Boolean(true)) } pub fn init_match_media(vm: &mut Vm) { let func = make_native(&mut vm.gc, "matchMedia", match_media); vm.set_global("matchMedia", Value::Function(func)); } #[cfg(test)] mod tests { use super::*; use crate::compiler; use crate::iframe_bridge; use crate::parser::Parser; fn eval(source: &str, with_window: bool) -> Result { let program = Parser::parse(source).expect("parse"); let func = compiler::compile(&program).expect("compile"); let mut vm = Vm::new(); if with_window { iframe_bridge::init_window_object(&mut vm, "main", "https://example.com"); } vm.execute(&func) } #[test] fn match_media_returns_media_query_list_shape() { let result = eval( "var m = matchMedia('(min-width: 900px)'); \ m.addListener(function(){}); \ m.addEventListener('change', function(){}); \ m.media + ':' + m.matches + ':' + m.dispatchEvent({})", false, ) .expect("ok"); assert!(matches!( result, Value::String(s) if s == "(min-width: 900px):false:true" )); } #[test] fn window_match_media_falls_through_to_global() { let result = eval("typeof window.matchMedia", true).expect("ok"); assert!(matches!(result, Value::String(s) if s == "function")); } #[test] fn bing_dark_mode_pattern_reads_window_match_media() { let result = eval( "var sj_cook = { get: function(){ return '2'; } }; \ var sj_evt = { bind: function(){}, unbind: function(){} }; \ function sj_be(){} \ var navigator = { cookieEnabled: true }; \ var DarkModeLogging; \ (function(){ \ function l(){ \ if (!t.matchMedia) return '0'; \ var n = t.matchMedia('(prefers-color-scheme: dark)'); \ return n.matches ? '1' : '0'; \ } \ function u(){ l(); } \ var r = 'undefined', t = window, f = document; \ typeof sj_cook !== r && sj_cook && u(); \ })(DarkModeLogging || (DarkModeLogging = {})); \ 'ok'", true, ) .expect("ok"); assert!(matches!(result, Value::String(s) if s == "ok")); } }