use crate::browser::views::{DeviceMetrics, DevicePreset, ViewportConfig};
use crate::error::Result;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ViewportManager {
client: Arc<crate::browser::cdp::CdpClient>,
}
impl ViewportManager {
pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
Self { client }
}
pub async fn set_device_metrics(&self, config: &ViewportConfig) -> Result<()> {
let mut params = serde_json::json!({
"width": config.width,
"height": config.height,
"deviceScaleFactor": config.device_scale_factor,
"mobile": config.mobile,
});
if config.touch {
params["touch"] = serde_json::json!({ "enabled": true });
}
if let Some(ref orientation) = config.orientation {
params["screenOrientation"] = serde_json::json!({
"type": orientation,
"angle": if orientation.starts_with("landscape") { 90 } else { 0 }
});
}
if let Some(scale) = config.scale {
params["scale"] = serde_json::json!(scale);
}
self.client
.send_command("Emulation.setDeviceMetricsOverride", params)
.await?;
Ok(())
}
pub async fn clear_device_metrics(&self) -> Result<()> {
self.client
.send_command("Emulation.clearDeviceMetricsOverride", serde_json::json!({}))
.await?;
Ok(())
}
pub async fn set_touch_emulation(&self, enabled: bool) -> Result<()> {
self.client
.send_command(
"Emulation.setTouchEmulationEnabled",
serde_json::json!({ "enabled": enabled }),
)
.await?;
Ok(())
}
pub async fn set_user_agent(&self, user_agent: &str, accept_language: Option<&str>) -> Result<()> {
let mut params = serde_json::json!({
"userAgent": user_agent,
});
if let Some(lang) = accept_language {
params["acceptLanguage"] = serde_json::json!(lang);
}
self.client
.send_command("Emulation.setUserAgentOverride", params)
.await?;
Ok(())
}
pub async fn set_geolocation(&self, latitude: f64, longitude: f64, accuracy: Option<f64>) -> Result<()> {
let mut params = serde_json::json!({
"latitude": latitude,
"longitude": longitude,
});
if let Some(acc) = accuracy {
params["accuracy"] = serde_json::json!(acc);
}
self.client
.send_command("Emulation.setGeolocationOverride", params)
.await?;
Ok(())
}
pub async fn clear_geolocation(&self) -> Result<()> {
self.client
.send_command("Emulation.clearGeolocationOverride", serde_json::json!({}))
.await?;
Ok(())
}
pub async fn set_timezone(&self, timezone_id: &str) -> Result<()> {
self.client
.send_command(
"Emulation.setTimezoneOverride",
serde_json::json!({ "timezoneId": timezone_id }),
)
.await?;
Ok(())
}
pub async fn set_media_features(&self, media_type: Option<&str>, features: Vec<crate::browser::views::MediaFeature>) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(mt) = media_type {
params["media"] = serde_json::json!(mt);
}
let features_json: Vec<serde_json::Value> = features
.into_iter()
.map(|f| serde_json::json!({ "name": f.name, "value": f.value }))
.collect();
params["features"] = serde_json::json!(features_json);
self.client
.send_command("Emulation.setEmulatedMedia", params)
.await?;
Ok(())
}
pub async fn clear_media_features(&self) -> Result<()> {
self.client
.send_command(
"Emulation.setEmulatedMedia",
serde_json::json!({}),
)
.await?;
Ok(())
}
pub async fn set_color_scheme(&self, scheme: crate::browser::views::ColorScheme) -> Result<()> {
let value = match scheme {
crate::browser::views::ColorScheme::Light => "light",
crate::browser::views::ColorScheme::Dark => "dark",
crate::browser::views::ColorScheme::NoPreference => "no-preference",
};
self.set_media_features(
None,
vec![crate::browser::views::MediaFeature {
name: "prefers-color-scheme".to_string(),
value: value.to_string(),
}],
)
.await
}
pub async fn set_vision_deficiency(&self, deficiency: crate::browser::views::VisionDeficiency) -> Result<()> {
let value = match deficiency {
crate::browser::views::VisionDeficiency::None => "none",
crate::browser::views::VisionDeficiency::Achromatopsia => "achromatopsia",
crate::browser::views::VisionDeficiency::BlurredVision => "blurredVision",
crate::browser::views::VisionDeficiency::Deuteranopia => "deuteranopia",
crate::browser::views::VisionDeficiency::Protanopia => "protanopia",
crate::browser::views::VisionDeficiency::Tritanopia => "tritanopia",
};
self.client
.send_command(
"Emulation.setEmulatedVisionDeficiency",
serde_json::json!({ "type": value }),
)
.await?;
Ok(())
}
pub async fn set_locale(&self, locale: &str) -> Result<()> {
self.client
.send_command(
"Emulation.setLocaleOverride",
serde_json::json!({ "locale": locale }),
)
.await?;
Ok(())
}
pub async fn clear_locale(&self) -> Result<()> {
self.client
.send_command(
"Emulation.setLocaleOverride",
serde_json::json!({}),
)
.await?;
Ok(())
}
pub async fn override_languages_and_fonts(&self, languages: Vec<String>, font_families: Option<Vec<String>>) -> Result<()> {
let langs = languages.join(",");
let mut js = format!(
"Object.defineProperty(navigator, 'languages', {{ get: () => ['{}'] }});",
langs.replace(',', "', '")
);
if let Some(fonts) = font_families {
let font_list = fonts.join(", ");
js.push_str(&format!(
"document.documentElement.style.fontFamily = '{}';",
font_list.replace('\'', "\\'")
));
}
self.client
.send_command(
"Runtime.evaluate",
serde_json::json!({
"expression": js,
"returnByValue": true,
}),
)
.await?;
Ok(())
}
pub async fn preset_device(&self, preset: &DevicePreset) -> Result<()> {
self.set_device_metrics(&preset.viewport).await?;
self.set_user_agent(
&preset.user_agent,
preset.accept_language.as_deref(),
)
.await?;
if preset.viewport.touch {
self.set_touch_emulation(true).await?;
}
Ok(())
}
pub async fn get_metrics(&self) -> Result<DeviceMetrics> {
let result = self
.client
.send_command(
"Runtime.evaluate",
serde_json::json!({
"expression": "({ width: window.innerWidth, height: window.innerHeight, deviceScaleFactor: window.devicePixelRatio, mobile: /Mobi|Android|iPhone/i.test(navigator.userAgent) })",
"returnByValue": true,
}),
)
.await?;
let value = result
.get("result")
.and_then(|r| r.get("value"))
.ok_or_else(|| crate::error::BrowsingError::Browser("No metrics value".to_string()))?;
Ok(DeviceMetrics {
width: value.get("width").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
height: value.get("height").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
device_scale_factor: value.get("deviceScaleFactor").and_then(|v| v.as_f64()).unwrap_or(1.0),
mobile: value.get("mobile").and_then(|v| v.as_bool()).unwrap_or(false),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_viewport_config_default() {
let config = ViewportConfig::default();
assert_eq!(config.width, 1280);
assert_eq!(config.height, 720);
assert_eq!(config.device_scale_factor, 1.0);
assert!(!config.mobile);
assert!(!config.touch);
}
#[test]
fn test_viewport_config_custom() {
let config = ViewportConfig {
width: 375,
height: 812,
device_scale_factor: 3.0,
mobile: true,
touch: true,
orientation: Some("portraitPrimary".to_string()),
scale: None,
};
assert_eq!(config.width, 375);
assert_eq!(config.height, 812);
assert!(config.mobile);
assert!(config.touch);
}
#[test]
fn test_device_metrics_serialization() {
let metrics = DeviceMetrics {
width: 1920,
height: 1080,
device_scale_factor: 2.0,
mobile: false,
};
let json = serde_json::to_string(&metrics).unwrap();
assert!(json.contains("1920"));
assert!(json.contains("2.0"));
}
#[test]
fn test_device_preset_iphone_15() {
let preset = DevicePreset::iphone_15();
assert_eq!(preset.name, "iPhone 15");
assert_eq!(preset.viewport.width, 393);
assert_eq!(preset.viewport.height, 852);
assert_eq!(preset.viewport.device_scale_factor, 3.0);
assert!(preset.viewport.mobile);
assert!(preset.viewport.touch);
assert!(preset.user_agent.contains("iPhone"));
}
#[test]
fn test_device_preset_by_name() {
let preset = DevicePreset::by_name("iPhone 15").unwrap();
assert_eq!(preset.name, "iPhone 15");
let preset2 = DevicePreset::by_name("iphone15").unwrap();
assert_eq!(preset2.name, "iPhone 15");
assert!(DevicePreset::by_name("unknown device").is_none());
}
#[test]
fn test_device_preset_list_names() {
let names = DevicePreset::list_names();
assert!(names.contains(&"iPhone 15"));
assert!(names.contains(&"Desktop 1080p"));
}
#[test]
fn test_device_preset_pixel_8() {
let preset = DevicePreset::pixel_8();
assert_eq!(preset.name, "Pixel 8");
assert!(preset.user_agent.contains("Android"));
assert!(preset.user_agent.contains("Pixel 8"));
}
#[test]
fn test_color_scheme_serialization() {
let dark = crate::browser::views::ColorScheme::Dark;
let json = serde_json::to_string(&dark).unwrap();
assert_eq!(json, "\"dark\"");
let light = crate::browser::views::ColorScheme::Light;
let json = serde_json::to_string(&light).unwrap();
assert_eq!(json, "\"light\"");
}
#[test]
fn test_media_feature_creation() {
let feature = crate::browser::views::MediaFeature {
name: "prefers-color-scheme".to_string(),
value: "dark".to_string(),
};
assert_eq!(feature.name, "prefers-color-scheme");
assert_eq!(feature.value, "dark");
}
#[test]
fn test_vision_deficiency_serialization() {
let vd = crate::browser::views::VisionDeficiency::Deuteranopia;
let json = serde_json::to_string(&vd).unwrap();
assert_eq!(json, "\"deuteranopia\"");
}
#[test]
fn test_locale_methods_exist() {
let _ = std::any::type_name::<ViewportManager>();
}
}