use std::sync::Arc;
use tracing::{debug, info, instrument};
use viewpoint_cdp::CdpConnection;
use viewpoint_cdp::protocol::emulation::{
MediaFeature, SetDeviceMetricsOverrideParams, SetEmulatedMediaParams,
SetEmulatedVisionDeficiencyParams, ViewportSize, VisionDeficiency as CdpVisionDeficiency,
};
use super::Page;
use crate::context::{ColorScheme, ForcedColors, ReducedMotion};
use crate::error::PageError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaType {
Screen,
Print,
}
impl MediaType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Screen => "screen",
Self::Print => "print",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VisionDeficiency {
#[default]
None,
Achromatopsia,
BlurredVision,
Deuteranopia,
Protanopia,
Tritanopia,
}
impl From<VisionDeficiency> for CdpVisionDeficiency {
fn from(deficiency: VisionDeficiency) -> Self {
match deficiency {
VisionDeficiency::None => CdpVisionDeficiency::None,
VisionDeficiency::Achromatopsia => CdpVisionDeficiency::Achromatopsia,
VisionDeficiency::BlurredVision => CdpVisionDeficiency::BlurredVision,
VisionDeficiency::Deuteranopia => CdpVisionDeficiency::Deuteranopia,
VisionDeficiency::Protanopia => CdpVisionDeficiency::Protanopia,
VisionDeficiency::Tritanopia => CdpVisionDeficiency::Tritanopia,
}
}
}
#[derive(Debug)]
pub struct EmulateMediaBuilder<'a> {
connection: &'a Arc<CdpConnection>,
session_id: &'a str,
media: Option<MediaType>,
color_scheme: Option<ColorScheme>,
reduced_motion: Option<ReducedMotion>,
forced_colors: Option<ForcedColors>,
}
impl<'a> EmulateMediaBuilder<'a> {
pub(crate) fn new(connection: &'a Arc<CdpConnection>, session_id: &'a str) -> Self {
Self {
connection,
session_id,
media: None,
color_scheme: None,
reduced_motion: None,
forced_colors: None,
}
}
#[must_use]
pub fn media(mut self, media: MediaType) -> Self {
self.media = Some(media);
self
}
#[must_use]
pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
self.color_scheme = Some(color_scheme);
self
}
#[must_use]
pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
self.reduced_motion = Some(reduced_motion);
self
}
#[must_use]
pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
self.forced_colors = Some(forced_colors);
self
}
#[instrument(level = "debug", skip(self))]
pub async fn apply(self) -> Result<(), PageError> {
let mut features = Vec::new();
if let Some(color_scheme) = self.color_scheme {
features.push(MediaFeature {
name: "prefers-color-scheme".to_string(),
value: match color_scheme {
ColorScheme::Light => "light".to_string(),
ColorScheme::Dark => "dark".to_string(),
ColorScheme::NoPreference => "no-preference".to_string(),
},
});
}
if let Some(reduced_motion) = self.reduced_motion {
features.push(MediaFeature {
name: "prefers-reduced-motion".to_string(),
value: match reduced_motion {
ReducedMotion::Reduce => "reduce".to_string(),
ReducedMotion::NoPreference => "no-preference".to_string(),
},
});
}
if let Some(forced_colors) = self.forced_colors {
features.push(MediaFeature {
name: "forced-colors".to_string(),
value: match forced_colors {
ForcedColors::Active => "active".to_string(),
ForcedColors::None => "none".to_string(),
},
});
}
let media = self.media.map(|m| m.as_str().to_string());
let features_opt = if features.is_empty() {
None
} else {
Some(features)
};
debug!(
media = ?media,
features_count = features_opt.as_ref().map_or(0, std::vec::Vec::len),
"Applying media emulation"
);
self.connection
.send_command::<_, serde_json::Value>(
"Emulation.setEmulatedMedia",
Some(SetEmulatedMediaParams {
media,
features: features_opt,
}),
Some(self.session_id),
)
.await?;
Ok(())
}
#[instrument(level = "debug", skip(self))]
pub async fn clear(self) -> Result<(), PageError> {
debug!("Clearing media emulation");
self.connection
.send_command::<_, serde_json::Value>(
"Emulation.setEmulatedMedia",
Some(SetEmulatedMediaParams {
media: Some(String::new()), features: Some(Vec::new()), }),
Some(self.session_id),
)
.await?;
Ok(())
}
}
#[instrument(level = "debug", skip(connection))]
async fn emulate_vision_deficiency_impl(
connection: &Arc<CdpConnection>,
session_id: &str,
deficiency: VisionDeficiency,
) -> Result<(), PageError> {
debug!(?deficiency, "Emulating vision deficiency");
connection
.send_command::<_, serde_json::Value>(
"Emulation.setEmulatedVisionDeficiency",
Some(SetEmulatedVisionDeficiencyParams::new(deficiency.into())),
Some(session_id),
)
.await?;
Ok(())
}
impl Page {
pub fn viewport_size(&self) -> Option<ViewportSize> {
None
}
#[instrument(level = "info", skip(self), fields(width = width, height = height))]
pub async fn set_viewport_size(&self, width: i32, height: i32) -> Result<(), PageError> {
if self.closed {
return Err(PageError::Closed);
}
self.connection
.send_command::<_, serde_json::Value>(
"Emulation.setDeviceMetricsOverride",
Some(SetDeviceMetricsOverrideParams {
width,
height,
device_scale_factor: 1.0,
mobile: false,
scale: None,
screen_width: None,
screen_height: None,
position_x: None,
position_y: None,
dont_set_visible_size: None,
screen_orientation: None,
viewport: None,
display_feature: None,
device_posture: None,
}),
Some(&self.session_id),
)
.await?;
info!("Viewport size set to {}x{}", width, height);
Ok(())
}
#[instrument(level = "info", skip(self))]
pub async fn bring_to_front(&self) -> Result<(), PageError> {
if self.closed {
return Err(PageError::Closed);
}
self.connection
.send_command::<_, serde_json::Value>(
"Page.bringToFront",
None::<()>,
Some(&self.session_id),
)
.await?;
info!("Page brought to front");
Ok(())
}
pub fn emulate_media(&self) -> EmulateMediaBuilder<'_> {
EmulateMediaBuilder::new(&self.connection, &self.session_id)
}
pub async fn emulate_vision_deficiency(
&self,
deficiency: VisionDeficiency,
) -> Result<(), PageError> {
if self.closed {
return Err(PageError::Closed);
}
emulate_vision_deficiency_impl(&self.connection, &self.session_id, deficiency).await
}
}
#[cfg(test)]
mod tests;