playwright_rs/protocol/page/
capture.rs1use super::{Page, ScreencastFrameHandler, ScreencastFrameHandlerFuture};
2use crate::error::Result;
3use crate::server::channel_owner::ChannelOwner;
4use crate::server::connection::ConnectionExt;
5use base64::Engine;
6use serde::{Deserialize, Serialize};
7use std::future::Future;
8use std::sync::Arc;
9
10impl Page {
12 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
16 pub async fn screenshot(
17 &self,
18 options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
19 ) -> Result<Vec<u8>> {
20 let options = options.into();
21 let params = if let Some(opts) = options {
22 opts.to_json()
23 } else {
24 serde_json::json!({
26 "type": "png",
27 "timeout": crate::DEFAULT_TIMEOUT_MS
28 })
29 };
30
31 #[derive(Deserialize)]
32 struct ScreenshotResponse {
33 binary: String,
34 }
35
36 let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
37
38 let bytes = base64::prelude::BASE64_STANDARD
40 .decode(&response.binary)
41 .map_err(|e| {
42 crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
43 })?;
44
45 tracing::Span::current().record("bytes_len", bytes.len());
46 Ok(bytes)
47 }
48
49 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
53 pub async fn screenshot_to_file(
54 &self,
55 path: &std::path::Path,
56 options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
57 ) -> Result<Vec<u8>> {
58 let options = options.into();
59 let bytes = self.screenshot(options).await?;
61
62 tokio::fs::write(path, &bytes).await.map_err(|e| {
64 crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
65 })?;
66
67 Ok(bytes)
68 }
69
70 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
106 pub async fn pdf(&self, options: impl Into<Option<PdfOptions>>) -> Result<Vec<u8>> {
107 let options = options.into();
108 let mut params = serde_json::json!({});
109 let mut save_path: Option<std::path::PathBuf> = None;
110
111 if let Some(opts) = options {
112 save_path = opts.path;
114
115 if let Some(scale) = opts.scale {
116 params["scale"] = serde_json::json!(scale);
117 }
118 if let Some(v) = opts.display_header_footer {
119 params["displayHeaderFooter"] = serde_json::json!(v);
120 }
121 if let Some(v) = opts.header_template {
122 params["headerTemplate"] = serde_json::json!(v);
123 }
124 if let Some(v) = opts.footer_template {
125 params["footerTemplate"] = serde_json::json!(v);
126 }
127 if let Some(v) = opts.print_background {
128 params["printBackground"] = serde_json::json!(v);
129 }
130 if let Some(v) = opts.landscape {
131 params["landscape"] = serde_json::json!(v);
132 }
133 if let Some(v) = opts.page_ranges {
134 params["pageRanges"] = serde_json::json!(v);
135 }
136 if let Some(v) = opts.format {
137 params["format"] = serde_json::json!(v);
138 }
139 if let Some(v) = opts.width {
140 params["width"] = serde_json::json!(v);
141 }
142 if let Some(v) = opts.height {
143 params["height"] = serde_json::json!(v);
144 }
145 if let Some(v) = opts.prefer_css_page_size {
146 params["preferCSSPageSize"] = serde_json::json!(v);
147 }
148 if let Some(margin) = opts.margin {
149 params["margin"] = serde_json::to_value(margin).map_err(|e| {
150 crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
151 })?;
152 }
153 }
154
155 #[derive(Deserialize)]
156 struct PdfResponse {
157 pdf: String,
158 }
159
160 let response: PdfResponse = self.channel().send("pdf", params).await?;
161
162 let pdf_bytes = base64::engine::general_purpose::STANDARD
164 .decode(&response.pdf)
165 .map_err(|e| {
166 crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
167 })?;
168
169 if let Some(path) = save_path {
171 tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
172 crate::error::Error::InvalidArgument(format!(
173 "Failed to write PDF to '{}': {}",
174 path.display(),
175 e
176 ))
177 })?;
178 }
179
180 tracing::Span::current().record("bytes_len", pdf_bytes.len());
181 Ok(pdf_bytes)
182 }
183
184 pub fn accessibility(&self) -> crate::protocol::Accessibility {
191 crate::protocol::Accessibility::new(self.clone())
192 }
193
194 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
205 pub async fn aria_snapshot(
206 &self,
207 options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
208 ) -> Result<String> {
209 let options = options.into();
210 let frame = self.main_frame().await?;
211 let timeout = options
212 .as_ref()
213 .and_then(|o| o.timeout)
214 .unwrap_or_else(|| self.default_timeout_ms());
215 frame
216 .aria_snapshot_raw("body", timeout, options.as_ref())
217 .await
218 }
219
220 pub async fn aria_snapshot_json(
231 &self,
232 options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
233 ) -> Result<serde_json::Value> {
234 let options = options.into();
235 let frame = self.main_frame().await?;
236 let timeout = options
237 .as_ref()
238 .and_then(|o| o.timeout)
239 .unwrap_or_else(|| self.default_timeout_ms());
240 frame
241 .aria_snapshot_json_raw("body", timeout, options.as_ref())
242 .await
243 }
244
245 pub fn coverage(&self) -> crate::protocol::Coverage {
255 crate::protocol::Coverage::new(self.clone())
256 }
257
258 pub fn screencast(&self) -> crate::protocol::Screencast {
266 crate::protocol::Screencast::new(self.clone())
267 }
268
269 pub(crate) async fn screencast_start(
270 &self,
271 options: crate::protocol::ScreencastStartOptions,
272 ) -> Result<()> {
273 let mut params = serde_json::json!({});
274 if let Some(size) = options.size {
275 params["size"] = serde_json::json!({
276 "width": size.width,
277 "height": size.height,
278 });
279 }
280 if let Some(quality) = options.quality {
281 params["quality"] = serde_json::json!(quality);
282 }
283 let has_handlers = !self.screencast_frame_handlers.lock().unwrap().is_empty();
284 params["sendFrames"] = serde_json::json!(has_handlers);
285 let recording = options.path.is_some();
286 params["record"] = serde_json::json!(recording);
287
288 #[derive(serde::Deserialize)]
289 struct StartResponse {
290 artifact: Option<serde_json::Value>,
291 }
292 let response: StartResponse = self.channel().send("screencastStart", params).await?;
293
294 if recording {
295 *self.screencast_save_path.lock().unwrap() = options.path;
296 if let Some(artifact_value) = response.artifact
297 && let Some(guid) = artifact_value.get("guid").and_then(|v| v.as_str())
298 {
299 *self.screencast_artifact_guid.lock().unwrap() = Some(guid.to_string());
300 }
301 }
302 Ok(())
303 }
304
305 pub(crate) async fn screencast_stop(&self) -> Result<()> {
306 self.channel()
307 .send_no_result("screencastStop", serde_json::json!({}))
308 .await?;
309
310 let path = self.screencast_save_path.lock().unwrap().take();
311 let artifact_guid = self.screencast_artifact_guid.lock().unwrap().take();
312 if let (Some(path), Some(guid)) = (path, artifact_guid) {
313 let artifact = self
314 .connection()
315 .get_typed::<crate::protocol::artifact::Artifact>(&guid)
316 .await?;
317 artifact.save_as(path.to_string_lossy().as_ref()).await?;
318 }
319 Ok(())
320 }
321
322 pub(crate) fn screencast_on_frame<F, Fut>(&self, handler: F)
323 where
324 F: Fn(crate::protocol::ScreencastFrame) -> Fut + Send + Sync + 'static,
325 Fut: Future<Output = Result<()>> + Send + 'static,
326 {
327 let h: ScreencastFrameHandler = Arc::new(
328 move |f: crate::protocol::ScreencastFrame| -> ScreencastFrameHandlerFuture {
329 Box::pin(handler(f))
330 },
331 );
332 self.screencast_frame_handlers.lock().unwrap().push(h);
333 }
334
335 pub(crate) async fn screencast_show_actions(
336 &self,
337 options: crate::protocol::ShowActionsOptions,
338 ) -> Result<()> {
339 let mut params = serde_json::json!({});
340 if let Some(d) = options.duration {
341 params["duration"] = serde_json::json!(d);
342 }
343 if let Some(p) = options.position {
344 params["position"] = serde_json::json!(p.as_str());
345 }
346 if let Some(f) = options.font_size {
347 params["fontSize"] = serde_json::json!(f);
348 }
349 if let Some(c) = options.cursor {
350 params["cursor"] = serde_json::json!(c.as_str());
351 }
352 self.channel()
353 .send_no_result("screencastShowActions", params)
354 .await
355 }
356
357 pub(crate) async fn screencast_hide_actions(&self) -> Result<()> {
358 self.channel()
359 .send_no_result("screencastHideActions", serde_json::json!({}))
360 .await
361 }
362
363 pub(crate) async fn screencast_chapter(
364 &self,
365 title: &str,
366 options: crate::protocol::ChapterOptions,
367 ) -> Result<()> {
368 let mut params = serde_json::json!({ "title": title });
369 if let Some(desc) = options.description {
370 params["description"] = serde_json::json!(desc);
371 }
372 if let Some(d) = options.duration {
373 params["duration"] = serde_json::json!(d);
374 }
375 self.channel()
376 .send_no_result("screencastChapter", params)
377 .await
378 }
379
380 pub(crate) async fn screencast_show_overlay(
381 &self,
382 html: &str,
383 options: crate::protocol::ShowOverlayOptions,
384 ) -> Result<crate::protocol::OverlayId> {
385 let mut params = serde_json::json!({ "html": html });
386 if let Some(d) = options.duration {
387 params["duration"] = serde_json::json!(d);
388 }
389 #[derive(serde::Deserialize)]
390 struct OverlayResponse {
391 id: String,
392 }
393 let response: OverlayResponse =
394 self.channel().send("screencastShowOverlay", params).await?;
395 Ok(crate::protocol::OverlayId(response.id))
396 }
397
398 pub(crate) async fn screencast_remove_overlay(
399 &self,
400 id: crate::protocol::OverlayId,
401 ) -> Result<()> {
402 self.channel()
403 .send_no_result("screencastRemoveOverlay", serde_json::json!({ "id": id.0 }))
404 .await
405 }
406
407 pub(crate) async fn screencast_set_overlay_visible(&self, visible: bool) -> Result<()> {
408 self.channel()
409 .send_no_result(
410 "screencastSetOverlayVisible",
411 serde_json::json!({ "visible": visible }),
412 )
413 .await
414 }
415
416 pub(crate) async fn accessibility_snapshot(
424 &self,
425 _options: Option<crate::protocol::accessibility::AccessibilitySnapshotOptions>,
426 ) -> Result<serde_json::Value> {
427 let frame = self.main_frame().await?;
428 let timeout = self.default_timeout_ms();
429 let snapshot = frame.aria_snapshot_raw("body", timeout, None).await?;
430 Ok(serde_json::Value::String(snapshot))
431 }
432
433 pub(crate) async fn coverage_start_js(
436 &self,
437 options: Option<crate::protocol::coverage::StartJSCoverageOptions>,
438 ) -> Result<()> {
439 let mut params = serde_json::json!({});
440
441 if let Some(opts) = options {
442 if let Some(v) = opts.reset_on_navigation {
443 params["resetOnNavigation"] = serde_json::json!(v);
444 }
445 if let Some(v) = opts.report_anonymous_scripts {
446 params["reportAnonymousScripts"] = serde_json::json!(v);
447 }
448 }
449
450 self.channel()
451 .send_no_result("startJSCoverage", params)
452 .await
453 }
454
455 pub(crate) async fn coverage_stop_js(
456 &self,
457 ) -> Result<Vec<crate::protocol::coverage::JSCoverageEntry>> {
458 #[derive(serde::Deserialize)]
459 struct StopJSCoverageResponse {
460 entries: Vec<crate::protocol::coverage::JSCoverageEntry>,
461 }
462
463 let response: StopJSCoverageResponse = self
464 .channel()
465 .send("stopJSCoverage", serde_json::json!({}))
466 .await?;
467
468 Ok(response.entries)
469 }
470
471 pub(crate) async fn coverage_start_css(
472 &self,
473 options: Option<crate::protocol::coverage::StartCSSCoverageOptions>,
474 ) -> Result<()> {
475 let mut params = serde_json::json!({});
476
477 if let Some(opts) = options
478 && let Some(v) = opts.reset_on_navigation
479 {
480 params["resetOnNavigation"] = serde_json::json!(v);
481 }
482
483 self.channel()
484 .send_no_result("startCSSCoverage", params)
485 .await
486 }
487
488 pub(crate) async fn coverage_stop_css(
489 &self,
490 ) -> Result<Vec<crate::protocol::coverage::CSSCoverageEntry>> {
491 #[derive(serde::Deserialize)]
492 struct StopCSSCoverageResponse {
493 entries: Vec<crate::protocol::coverage::CSSCoverageEntry>,
494 }
495
496 let response: StopCSSCoverageResponse = self
497 .channel()
498 .send("stopCSSCoverage", serde_json::json!({}))
499 .await?;
500
501 Ok(response.entries)
502 }
503}
504
505#[derive(Debug, Clone, Default, Serialize)]
509pub struct PdfMargin {
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub top: Option<String>,
513 #[serde(skip_serializing_if = "Option::is_none")]
515 pub right: Option<String>,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub bottom: Option<String>,
519 #[serde(skip_serializing_if = "Option::is_none")]
521 pub left: Option<String>,
522}
523
524#[derive(Debug, Clone, Default)]
531#[non_exhaustive]
532pub struct PdfOptions {
533 pub path: Option<std::path::PathBuf>,
535 pub scale: Option<f64>,
537 pub display_header_footer: Option<bool>,
539 pub header_template: Option<String>,
541 pub footer_template: Option<String>,
543 pub print_background: Option<bool>,
545 pub landscape: Option<bool>,
547 pub page_ranges: Option<String>,
549 pub format: Option<String>,
551 pub width: Option<String>,
553 pub height: Option<String>,
555 pub prefer_css_page_size: Option<bool>,
557 pub margin: Option<PdfMargin>,
559}
560
561impl PdfOptions {
562 pub fn builder() -> PdfOptionsBuilder {
564 PdfOptionsBuilder::default()
565 }
566}
567
568#[derive(Debug, Clone, Default)]
570pub struct PdfOptionsBuilder {
571 path: Option<std::path::PathBuf>,
572 scale: Option<f64>,
573 display_header_footer: Option<bool>,
574 header_template: Option<String>,
575 footer_template: Option<String>,
576 print_background: Option<bool>,
577 landscape: Option<bool>,
578 page_ranges: Option<String>,
579 format: Option<String>,
580 width: Option<String>,
581 height: Option<String>,
582 prefer_css_page_size: Option<bool>,
583 margin: Option<PdfMargin>,
584}
585
586impl PdfOptionsBuilder {
587 pub fn path(mut self, path: std::path::PathBuf) -> Self {
589 self.path = Some(path);
590 self
591 }
592
593 pub fn scale(mut self, scale: f64) -> Self {
595 self.scale = Some(scale);
596 self
597 }
598
599 pub fn display_header_footer(mut self, display: bool) -> Self {
601 self.display_header_footer = Some(display);
602 self
603 }
604
605 pub fn header_template(mut self, template: impl Into<String>) -> Self {
607 self.header_template = Some(template.into());
608 self
609 }
610
611 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
613 self.footer_template = Some(template.into());
614 self
615 }
616
617 pub fn print_background(mut self, print: bool) -> Self {
619 self.print_background = Some(print);
620 self
621 }
622
623 pub fn landscape(mut self, landscape: bool) -> Self {
625 self.landscape = Some(landscape);
626 self
627 }
628
629 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
631 self.page_ranges = Some(ranges.into());
632 self
633 }
634
635 pub fn format(mut self, format: impl Into<String>) -> Self {
637 self.format = Some(format.into());
638 self
639 }
640
641 pub fn width(mut self, width: impl Into<String>) -> Self {
643 self.width = Some(width.into());
644 self
645 }
646
647 pub fn height(mut self, height: impl Into<String>) -> Self {
649 self.height = Some(height.into());
650 self
651 }
652
653 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
655 self.prefer_css_page_size = Some(prefer);
656 self
657 }
658
659 pub fn margin(mut self, margin: PdfMargin) -> Self {
661 self.margin = Some(margin);
662 self
663 }
664
665 pub fn build(self) -> PdfOptions {
667 PdfOptions {
668 path: self.path,
669 scale: self.scale,
670 display_header_footer: self.display_header_footer,
671 header_template: self.header_template,
672 footer_template: self.footer_template,
673 print_background: self.print_background,
674 landscape: self.landscape,
675 page_ranges: self.page_ranges,
676 format: self.format,
677 width: self.width,
678 height: self.height,
679 prefer_css_page_size: self.prefer_css_page_size,
680 margin: self.margin,
681 }
682 }
683}