use super::{Page, ScreencastFrameHandler, ScreencastFrameHandlerFuture};
use crate::error::Result;
use crate::server::channel_owner::ChannelOwner;
use crate::server::connection::ConnectionExt;
use base64::Engine;
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::sync::Arc;
impl Page {
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
pub async fn screenshot(
&self,
options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
) -> Result<Vec<u8>> {
let options = options.into();
let params = if let Some(opts) = options {
opts.to_json()
} else {
serde_json::json!({
"type": "png",
"timeout": crate::DEFAULT_TIMEOUT_MS
})
};
#[derive(Deserialize)]
struct ScreenshotResponse {
binary: String,
}
let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
let bytes = base64::prelude::BASE64_STANDARD
.decode(&response.binary)
.map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
})?;
tracing::Span::current().record("bytes_len", bytes.len());
Ok(bytes)
}
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
pub async fn screenshot_to_file(
&self,
path: &std::path::Path,
options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
) -> Result<Vec<u8>> {
let options = options.into();
let bytes = self.screenshot(options).await?;
tokio::fs::write(path, &bytes).await.map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
})?;
Ok(bytes)
}
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
pub async fn pdf(&self, options: impl Into<Option<PdfOptions>>) -> Result<Vec<u8>> {
let options = options.into();
let mut params = serde_json::json!({});
let mut save_path: Option<std::path::PathBuf> = None;
if let Some(opts) = options {
save_path = opts.path;
if let Some(scale) = opts.scale {
params["scale"] = serde_json::json!(scale);
}
if let Some(v) = opts.display_header_footer {
params["displayHeaderFooter"] = serde_json::json!(v);
}
if let Some(v) = opts.header_template {
params["headerTemplate"] = serde_json::json!(v);
}
if let Some(v) = opts.footer_template {
params["footerTemplate"] = serde_json::json!(v);
}
if let Some(v) = opts.print_background {
params["printBackground"] = serde_json::json!(v);
}
if let Some(v) = opts.landscape {
params["landscape"] = serde_json::json!(v);
}
if let Some(v) = opts.page_ranges {
params["pageRanges"] = serde_json::json!(v);
}
if let Some(v) = opts.format {
params["format"] = serde_json::json!(v);
}
if let Some(v) = opts.width {
params["width"] = serde_json::json!(v);
}
if let Some(v) = opts.height {
params["height"] = serde_json::json!(v);
}
if let Some(v) = opts.prefer_css_page_size {
params["preferCSSPageSize"] = serde_json::json!(v);
}
if let Some(margin) = opts.margin {
params["margin"] = serde_json::to_value(margin).map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
})?;
}
}
#[derive(Deserialize)]
struct PdfResponse {
pdf: String,
}
let response: PdfResponse = self.channel().send("pdf", params).await?;
let pdf_bytes = base64::engine::general_purpose::STANDARD
.decode(&response.pdf)
.map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
})?;
if let Some(path) = save_path {
tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
crate::error::Error::InvalidArgument(format!(
"Failed to write PDF to '{}': {}",
path.display(),
e
))
})?;
}
tracing::Span::current().record("bytes_len", pdf_bytes.len());
Ok(pdf_bytes)
}
pub fn accessibility(&self) -> crate::protocol::Accessibility {
crate::protocol::Accessibility::new(self.clone())
}
#[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
pub async fn aria_snapshot(
&self,
options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
) -> Result<String> {
let options = options.into();
let frame = self.main_frame().await?;
let timeout = options
.as_ref()
.and_then(|o| o.timeout)
.unwrap_or_else(|| self.default_timeout_ms());
frame
.aria_snapshot_raw("body", timeout, options.as_ref())
.await
}
pub async fn aria_snapshot_json(
&self,
options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
) -> Result<serde_json::Value> {
let options = options.into();
let frame = self.main_frame().await?;
let timeout = options
.as_ref()
.and_then(|o| o.timeout)
.unwrap_or_else(|| self.default_timeout_ms());
frame
.aria_snapshot_json_raw("body", timeout, options.as_ref())
.await
}
pub fn coverage(&self) -> crate::protocol::Coverage {
crate::protocol::Coverage::new(self.clone())
}
pub fn screencast(&self) -> crate::protocol::Screencast {
crate::protocol::Screencast::new(self.clone())
}
pub(crate) async fn screencast_start(
&self,
options: crate::protocol::ScreencastStartOptions,
) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(size) = options.size {
params["size"] = serde_json::json!({
"width": size.width,
"height": size.height,
});
}
if let Some(quality) = options.quality {
params["quality"] = serde_json::json!(quality);
}
let has_handlers = !self.screencast_frame_handlers.lock().unwrap().is_empty();
params["sendFrames"] = serde_json::json!(has_handlers);
let recording = options.path.is_some();
params["record"] = serde_json::json!(recording);
#[derive(serde::Deserialize)]
struct StartResponse {
artifact: Option<serde_json::Value>,
}
let response: StartResponse = self.channel().send("screencastStart", params).await?;
if recording {
*self.screencast_save_path.lock().unwrap() = options.path;
if let Some(artifact_value) = response.artifact
&& let Some(guid) = artifact_value.get("guid").and_then(|v| v.as_str())
{
*self.screencast_artifact_guid.lock().unwrap() = Some(guid.to_string());
}
}
Ok(())
}
pub(crate) async fn screencast_stop(&self) -> Result<()> {
self.channel()
.send_no_result("screencastStop", serde_json::json!({}))
.await?;
let path = self.screencast_save_path.lock().unwrap().take();
let artifact_guid = self.screencast_artifact_guid.lock().unwrap().take();
if let (Some(path), Some(guid)) = (path, artifact_guid) {
let artifact = self
.connection()
.get_typed::<crate::protocol::artifact::Artifact>(&guid)
.await?;
artifact.save_as(path.to_string_lossy().as_ref()).await?;
}
Ok(())
}
pub(crate) fn screencast_on_frame<F, Fut>(&self, handler: F)
where
F: Fn(crate::protocol::ScreencastFrame) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let h: ScreencastFrameHandler = Arc::new(
move |f: crate::protocol::ScreencastFrame| -> ScreencastFrameHandlerFuture {
Box::pin(handler(f))
},
);
self.screencast_frame_handlers.lock().unwrap().push(h);
}
pub(crate) async fn screencast_show_actions(
&self,
options: crate::protocol::ShowActionsOptions,
) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(d) = options.duration {
params["duration"] = serde_json::json!(d);
}
if let Some(p) = options.position {
params["position"] = serde_json::json!(p.as_str());
}
if let Some(f) = options.font_size {
params["fontSize"] = serde_json::json!(f);
}
if let Some(c) = options.cursor {
params["cursor"] = serde_json::json!(c.as_str());
}
self.channel()
.send_no_result("screencastShowActions", params)
.await
}
pub(crate) async fn screencast_hide_actions(&self) -> Result<()> {
self.channel()
.send_no_result("screencastHideActions", serde_json::json!({}))
.await
}
pub(crate) async fn screencast_chapter(
&self,
title: &str,
options: crate::protocol::ChapterOptions,
) -> Result<()> {
let mut params = serde_json::json!({ "title": title });
if let Some(desc) = options.description {
params["description"] = serde_json::json!(desc);
}
if let Some(d) = options.duration {
params["duration"] = serde_json::json!(d);
}
self.channel()
.send_no_result("screencastChapter", params)
.await
}
pub(crate) async fn screencast_show_overlay(
&self,
html: &str,
options: crate::protocol::ShowOverlayOptions,
) -> Result<crate::protocol::OverlayId> {
let mut params = serde_json::json!({ "html": html });
if let Some(d) = options.duration {
params["duration"] = serde_json::json!(d);
}
#[derive(serde::Deserialize)]
struct OverlayResponse {
id: String,
}
let response: OverlayResponse =
self.channel().send("screencastShowOverlay", params).await?;
Ok(crate::protocol::OverlayId(response.id))
}
pub(crate) async fn screencast_remove_overlay(
&self,
id: crate::protocol::OverlayId,
) -> Result<()> {
self.channel()
.send_no_result("screencastRemoveOverlay", serde_json::json!({ "id": id.0 }))
.await
}
pub(crate) async fn screencast_set_overlay_visible(&self, visible: bool) -> Result<()> {
self.channel()
.send_no_result(
"screencastSetOverlayVisible",
serde_json::json!({ "visible": visible }),
)
.await
}
pub(crate) async fn accessibility_snapshot(
&self,
_options: Option<crate::protocol::accessibility::AccessibilitySnapshotOptions>,
) -> Result<serde_json::Value> {
let frame = self.main_frame().await?;
let timeout = self.default_timeout_ms();
let snapshot = frame.aria_snapshot_raw("body", timeout, None).await?;
Ok(serde_json::Value::String(snapshot))
}
pub(crate) async fn coverage_start_js(
&self,
options: Option<crate::protocol::coverage::StartJSCoverageOptions>,
) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(opts) = options {
if let Some(v) = opts.reset_on_navigation {
params["resetOnNavigation"] = serde_json::json!(v);
}
if let Some(v) = opts.report_anonymous_scripts {
params["reportAnonymousScripts"] = serde_json::json!(v);
}
}
self.channel()
.send_no_result("startJSCoverage", params)
.await
}
pub(crate) async fn coverage_stop_js(
&self,
) -> Result<Vec<crate::protocol::coverage::JSCoverageEntry>> {
#[derive(serde::Deserialize)]
struct StopJSCoverageResponse {
entries: Vec<crate::protocol::coverage::JSCoverageEntry>,
}
let response: StopJSCoverageResponse = self
.channel()
.send("stopJSCoverage", serde_json::json!({}))
.await?;
Ok(response.entries)
}
pub(crate) async fn coverage_start_css(
&self,
options: Option<crate::protocol::coverage::StartCSSCoverageOptions>,
) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(opts) = options
&& let Some(v) = opts.reset_on_navigation
{
params["resetOnNavigation"] = serde_json::json!(v);
}
self.channel()
.send_no_result("startCSSCoverage", params)
.await
}
pub(crate) async fn coverage_stop_css(
&self,
) -> Result<Vec<crate::protocol::coverage::CSSCoverageEntry>> {
#[derive(serde::Deserialize)]
struct StopCSSCoverageResponse {
entries: Vec<crate::protocol::coverage::CSSCoverageEntry>,
}
let response: StopCSSCoverageResponse = self
.channel()
.send("stopCSSCoverage", serde_json::json!({}))
.await?;
Ok(response.entries)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct PdfMargin {
#[serde(skip_serializing_if = "Option::is_none")]
pub top: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub right: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bottom: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub left: Option<String>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct PdfOptions {
pub path: Option<std::path::PathBuf>,
pub scale: Option<f64>,
pub display_header_footer: Option<bool>,
pub header_template: Option<String>,
pub footer_template: Option<String>,
pub print_background: Option<bool>,
pub landscape: Option<bool>,
pub page_ranges: Option<String>,
pub format: Option<String>,
pub width: Option<String>,
pub height: Option<String>,
pub prefer_css_page_size: Option<bool>,
pub margin: Option<PdfMargin>,
}
impl PdfOptions {
pub fn builder() -> PdfOptionsBuilder {
PdfOptionsBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct PdfOptionsBuilder {
path: Option<std::path::PathBuf>,
scale: Option<f64>,
display_header_footer: Option<bool>,
header_template: Option<String>,
footer_template: Option<String>,
print_background: Option<bool>,
landscape: Option<bool>,
page_ranges: Option<String>,
format: Option<String>,
width: Option<String>,
height: Option<String>,
prefer_css_page_size: Option<bool>,
margin: Option<PdfMargin>,
}
impl PdfOptionsBuilder {
pub fn path(mut self, path: std::path::PathBuf) -> Self {
self.path = Some(path);
self
}
pub fn scale(mut self, scale: f64) -> Self {
self.scale = Some(scale);
self
}
pub fn display_header_footer(mut self, display: bool) -> Self {
self.display_header_footer = Some(display);
self
}
pub fn header_template(mut self, template: impl Into<String>) -> Self {
self.header_template = Some(template.into());
self
}
pub fn footer_template(mut self, template: impl Into<String>) -> Self {
self.footer_template = Some(template.into());
self
}
pub fn print_background(mut self, print: bool) -> Self {
self.print_background = Some(print);
self
}
pub fn landscape(mut self, landscape: bool) -> Self {
self.landscape = Some(landscape);
self
}
pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
self.page_ranges = Some(ranges.into());
self
}
pub fn format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
pub fn width(mut self, width: impl Into<String>) -> Self {
self.width = Some(width.into());
self
}
pub fn height(mut self, height: impl Into<String>) -> Self {
self.height = Some(height.into());
self
}
pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
self.prefer_css_page_size = Some(prefer);
self
}
pub fn margin(mut self, margin: PdfMargin) -> Self {
self.margin = Some(margin);
self
}
pub fn build(self) -> PdfOptions {
PdfOptions {
path: self.path,
scale: self.scale,
display_header_footer: self.display_header_footer,
header_template: self.header_template,
footer_template: self.footer_template,
print_background: self.print_background,
landscape: self.landscape,
page_ranges: self.page_ranges,
format: self.format,
width: self.width,
height: self.height,
prefer_css_page_size: self.prefer_css_page_size,
margin: self.margin,
}
}
}