use serde_json::{json, Value}; use crate::dispatch::CdpContext; const MAX_DEVICE_METRIC_DIMENSION: i64 = 10_000_000; const DEFAULT_VIEWPORT: (f32, f32) = (1280.0, 720.0); fn metric_dimension(params: &Value, name: &str) -> Result { let value = params .get(name) .and_then(Value::as_i64) .ok_or_else(|| format!("Emulation.setDeviceMetricsOverride requires integer {name}"))?; if !(0..=MAX_DEVICE_METRIC_DIMENSION).contains(&value) { return Err(format!( "Emulation.setDeviceMetricsOverride {name} must be between 0 and {MAX_DEVICE_METRIC_DIMENSION}" )); } Ok(value as u32) } fn default_background_color(params: &Value) -> Result, String> { let Some(color) = params.get("color") else { return Ok(None); }; let color = color.as_object().ok_or( "Emulation.setDefaultBackgroundColorOverride color must be an RGBA object", )?; let channel = |name: &str| -> Result { let value = color.get(name).and_then(Value::as_i64).ok_or_else(|| { format!( "Emulation.setDefaultBackgroundColorOverride requires integer color.{name}" ) })?; Ok(value.clamp(0, 255) as u8) }; let alpha = match color.get("a") { Some(value) => value.as_f64().ok_or( "Emulation.setDefaultBackgroundColorOverride color.a must be a number", )?, None => 1.0, }; if !alpha.is_finite() { return Err( "Emulation.setDefaultBackgroundColorOverride color.a must be finite".to_string(), ); } Ok(Some([ channel("r")?, channel("g")?, channel("b")?, ((alpha as f32).clamp(0.0, 1.0) * 255.0).round() as u8, ])) } pub async fn handle( method: &str, params: &Value, ctx: &mut CdpContext, session_id: &Option, ) -> Result { match method { "setDeviceMetricsOverride" => { let width = metric_dimension(params, "width")?; let height = metric_dimension(params, "height")?; let device_scale_factor = params .get("deviceScaleFactor") .and_then(Value::as_f64) .ok_or("Emulation.setDeviceMetricsOverride requires deviceScaleFactor")?; if !device_scale_factor.is_finite() || device_scale_factor < 0.0 { return Err( "Emulation.setDeviceMetricsOverride requires a non-negative finite deviceScaleFactor" .to_string(), ); } let page = ctx .get_session_page_mut(session_id) .ok_or("No page for session")?; page.set_viewport(( if width == 0 { DEFAULT_VIEWPORT.0 } else { width as f32 }, if height == 0 { DEFAULT_VIEWPORT.1 } else { height as f32 }, )); page.set_device_scale_factor(device_scale_factor as f32); Ok(json!({})) } "clearDeviceMetricsOverride" => { let page = ctx .get_session_page_mut(session_id) .ok_or("No page for session")?; page.set_viewport(DEFAULT_VIEWPORT); page.set_device_scale_factor(1.0); Ok(json!({})) } "setDefaultBackgroundColorOverride" => { let color = default_background_color(params)?; let page = ctx .get_session_page_mut(session_id) .ok_or("No page for session")?; page.set_default_background_color_override(color); Ok(json!({})) } // Touch emulation does not affect layout yet, but acknowledging it is // compatible with clients that pair it with a metrics override. "setTouchEmulationEnabled" => Ok(json!({})), _ => Ok(json!({})), } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn device_metrics_override_updates_page_and_window_viewport() { let mut ctx = CdpContext::new(); let page_id = ctx.create_page(); let session_id = Some("viewport-session".to_string()); ctx.sessions.insert(session_id.clone().unwrap(), page_id); handle( "setDeviceMetricsOverride", &json!({"width": 1024, "height": 768, "deviceScaleFactor": 2}), &mut ctx, &session_id, ) .await .expect("metrics override"); let page = ctx .get_session_page_mut(&session_id) .expect("page for session"); assert_eq!(page.viewport, (1024.0, 768.0)); assert_eq!(page.device_scale_factor, 2.0); assert_eq!( page.evaluate( "return [innerWidth, innerHeight, visualViewport.width,\ visualViewport.height, devicePixelRatio];" ), json!([1024, 768, 1024, 768, 2]) ); } #[tokio::test] async fn device_metrics_override_rejects_fractional_and_out_of_range_dimensions() { let mut ctx = CdpContext::new(); let page_id = ctx.create_page(); let session_id = Some("viewport-session".to_string()); ctx.sessions.insert(session_id.clone().unwrap(), page_id); for params in [ json!({"width": 0.5, "height": 768, "deviceScaleFactor": 1}), json!({"width": 10_000_001, "height": 768, "deviceScaleFactor": 1}), json!({"width": -1, "height": 768, "deviceScaleFactor": 1}), ] { assert!( handle("setDeviceMetricsOverride", ¶ms, &mut ctx, &session_id) .await .is_err(), "must reject {params}" ); } } #[tokio::test] async fn zero_dimensions_disable_size_override() { let mut ctx = CdpContext::new(); let page_id = ctx.create_page(); let session_id = Some("zero-size-session".to_string()); ctx.sessions.insert(session_id.clone().unwrap(), page_id); handle( "setDeviceMetricsOverride", &json!({"width": 0, "height": 0, "deviceScaleFactor": 0}), &mut ctx, &session_id, ) .await .expect("zero disables the overrides"); let page = ctx.get_session_page_mut(&session_id).unwrap(); assert_eq!(page.viewport, DEFAULT_VIEWPORT); assert_eq!(page.device_scale_factor, 1.0); } #[tokio::test] async fn device_scale_factor_zero_disables_override_and_clear_restores_defaults() { let mut ctx = CdpContext::new(); let page_id = ctx.create_page(); let session_id = Some("scale-session".to_string()); ctx.sessions.insert(session_id.clone().unwrap(), page_id); handle( "setDeviceMetricsOverride", &json!({"width": 640, "height": 480, "deviceScaleFactor": 3}), &mut ctx, &session_id, ) .await .unwrap(); assert_eq!( ctx.get_session_page(&session_id) .unwrap() .device_scale_factor, 3.0 ); handle( "setDeviceMetricsOverride", &json!({"width": 640, "height": 480, "deviceScaleFactor": 0}), &mut ctx, &session_id, ) .await .expect("zero disables the scale override"); assert_eq!( ctx.get_session_page(&session_id) .unwrap() .device_scale_factor, 1.0 ); handle( "clearDeviceMetricsOverride", &json!({}), &mut ctx, &session_id, ) .await .unwrap(); let page = ctx.get_session_page(&session_id).unwrap(); assert_eq!(page.viewport, (1280.0, 720.0)); assert_eq!(page.device_scale_factor, 1.0); } #[test] fn default_background_color_matches_blink_defaults_rounding_and_clamping() { assert_eq!(default_background_color(&json!({})), Ok(None)); assert_eq!( default_background_color(&json!({"color": {"r": 1, "g": 2, "b": 3}})), Ok(Some([1, 2, 3, 255])) ); assert_eq!( default_background_color(&json!({ "color": {"r": -20, "g": 400, "b": 30, "a": 16.0 / 255.0} })), Ok(Some([0, 255, 30, 16])) ); assert_eq!( default_background_color(&json!({ "color": {"r": 1, "g": 2, "b": 3, "a": 2} })), Ok(Some([1, 2, 3, 255])) ); assert_eq!( default_background_color(&json!({ "color": {"r": 1, "g": 2, "b": 3, "a": -1} })), Ok(Some([1, 2, 3, 0])) ); } #[test] fn default_background_color_rejects_malformed_rgba() { for params in [ json!({"color": null}), json!({"color": {"g": 2, "b": 3}}), json!({"color": {"r": 1.5, "g": 2, "b": 3}}), json!({"color": {"r": 1, "g": 2, "b": 3, "a": "opaque"}}), ] { assert!( default_background_color(¶ms).is_err(), "must reject {params}" ); } } }