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 has_handlers = !self.screencast_frame_handlers.lock().unwrap().is_empty();
274 let params = options.params(has_handlers);
275 let recording = options.path.is_some();
276
277 #[derive(serde::Deserialize)]
278 struct StartResponse {
279 artifact: Option<serde_json::Value>,
280 }
281 let response: StartResponse = self.channel().send("screencastStart", params).await?;
282
283 if recording {
284 *self.screencast_save_path.lock().unwrap() = options.path;
285 if let Some(artifact_value) = response.artifact
286 && let Some(guid) = artifact_value.get("guid").and_then(|v| v.as_str())
287 {
288 *self.screencast_artifact_guid.lock().unwrap() = Some(guid.to_string());
289 }
290 }
291 Ok(())
292 }
293
294 pub(crate) async fn screencast_stop(&self) -> Result<()> {
295 self.channel()
296 .send_no_result("screencastStop", serde_json::json!({}))
297 .await?;
298
299 let path = self.screencast_save_path.lock().unwrap().take();
300 let artifact_guid = self.screencast_artifact_guid.lock().unwrap().take();
301 if let (Some(path), Some(guid)) = (path, artifact_guid) {
302 let artifact = self
303 .connection()
304 .get_typed::<crate::protocol::artifact::Artifact>(&guid)
305 .await?;
306 artifact.save_as(path.to_string_lossy().as_ref()).await?;
307 }
308 Ok(())
309 }
310
311 pub(crate) fn screencast_on_frame<F, Fut>(&self, handler: F)
312 where
313 F: Fn(crate::protocol::ScreencastFrame) -> Fut + Send + Sync + 'static,
314 Fut: Future<Output = Result<()>> + Send + 'static,
315 {
316 let h: ScreencastFrameHandler = Arc::new(
317 move |f: crate::protocol::ScreencastFrame| -> ScreencastFrameHandlerFuture {
318 Box::pin(handler(f))
319 },
320 );
321 self.screencast_frame_handlers.lock().unwrap().push(h);
322 }
323
324 pub(crate) async fn screencast_show_actions(
325 &self,
326 options: crate::protocol::ShowActionsOptions,
327 ) -> Result<()> {
328 self.channel()
329 .send_no_result("screencastShowActions", options)
330 .await
331 }
332
333 pub(crate) async fn screencast_hide_actions(&self) -> Result<()> {
334 self.channel()
335 .send_no_result("screencastHideActions", serde_json::json!({}))
336 .await
337 }
338
339 pub(crate) async fn screencast_chapter(
340 &self,
341 title: &str,
342 options: crate::protocol::ChapterOptions,
343 ) -> Result<()> {
344 let mut params = serde_json::json!({ "title": title });
345 if let Some(desc) = options.description {
346 params["description"] = serde_json::json!(desc);
347 }
348 if let Some(d) = options.duration {
349 params["duration"] = serde_json::json!(d);
350 }
351 self.channel()
352 .send_no_result("screencastChapter", params)
353 .await
354 }
355
356 pub(crate) async fn screencast_show_overlay(
357 &self,
358 html: &str,
359 options: crate::protocol::ShowOverlayOptions,
360 ) -> Result<crate::protocol::OverlayId> {
361 let mut params = serde_json::json!({ "html": html });
362 if let Some(d) = options.duration {
363 params["duration"] = serde_json::json!(d);
364 }
365 #[derive(serde::Deserialize)]
366 struct OverlayResponse {
367 id: String,
368 }
369 let response: OverlayResponse =
370 self.channel().send("screencastShowOverlay", params).await?;
371 Ok(crate::protocol::OverlayId(response.id))
372 }
373
374 pub(crate) async fn screencast_remove_overlay(
375 &self,
376 id: crate::protocol::OverlayId,
377 ) -> Result<()> {
378 self.channel()
379 .send_no_result("screencastRemoveOverlay", serde_json::json!({ "id": id.0 }))
380 .await
381 }
382
383 pub(crate) async fn screencast_set_overlay_visible(&self, visible: bool) -> Result<()> {
384 self.channel()
385 .send_no_result(
386 "screencastSetOverlayVisible",
387 serde_json::json!({ "visible": visible }),
388 )
389 .await
390 }
391
392 pub(crate) async fn accessibility_snapshot(
400 &self,
401 _options: Option<crate::protocol::accessibility::AccessibilitySnapshotOptions>,
402 ) -> Result<serde_json::Value> {
403 let frame = self.main_frame().await?;
404 let timeout = self.default_timeout_ms();
405 let snapshot = frame.aria_snapshot_raw("body", timeout, None).await?;
406 Ok(serde_json::Value::String(snapshot))
407 }
408
409 pub(crate) async fn coverage_start_js(
412 &self,
413 options: Option<crate::protocol::coverage::StartJSCoverageOptions>,
414 ) -> Result<()> {
415 let mut params = serde_json::json!({});
416
417 if let Some(opts) = options {
418 if let Some(v) = opts.reset_on_navigation {
419 params["resetOnNavigation"] = serde_json::json!(v);
420 }
421 if let Some(v) = opts.report_anonymous_scripts {
422 params["reportAnonymousScripts"] = serde_json::json!(v);
423 }
424 }
425
426 self.channel()
427 .send_no_result("startJSCoverage", params)
428 .await
429 }
430
431 pub(crate) async fn coverage_stop_js(
432 &self,
433 ) -> Result<Vec<crate::protocol::coverage::JSCoverageEntry>> {
434 #[derive(serde::Deserialize)]
435 struct StopJSCoverageResponse {
436 entries: Vec<crate::protocol::coverage::JSCoverageEntry>,
437 }
438
439 let response: StopJSCoverageResponse = self
440 .channel()
441 .send("stopJSCoverage", serde_json::json!({}))
442 .await?;
443
444 Ok(response.entries)
445 }
446
447 pub(crate) async fn coverage_start_css(
448 &self,
449 options: Option<crate::protocol::coverage::StartCSSCoverageOptions>,
450 ) -> Result<()> {
451 let mut params = serde_json::json!({});
452
453 if let Some(opts) = options
454 && let Some(v) = opts.reset_on_navigation
455 {
456 params["resetOnNavigation"] = serde_json::json!(v);
457 }
458
459 self.channel()
460 .send_no_result("startCSSCoverage", params)
461 .await
462 }
463
464 pub(crate) async fn coverage_stop_css(
465 &self,
466 ) -> Result<Vec<crate::protocol::coverage::CSSCoverageEntry>> {
467 #[derive(serde::Deserialize)]
468 struct StopCSSCoverageResponse {
469 entries: Vec<crate::protocol::coverage::CSSCoverageEntry>,
470 }
471
472 let response: StopCSSCoverageResponse = self
473 .channel()
474 .send("stopCSSCoverage", serde_json::json!({}))
475 .await?;
476
477 Ok(response.entries)
478 }
479}
480
481#[derive(Debug, Clone, Default, Serialize)]
485pub struct PdfMargin {
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub top: Option<String>,
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub right: Option<String>,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub bottom: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub left: Option<String>,
498}
499
500#[derive(Debug, Clone, Default)]
507#[non_exhaustive]
508pub struct PdfOptions {
509 pub path: Option<std::path::PathBuf>,
511 pub scale: Option<f64>,
513 pub display_header_footer: Option<bool>,
515 pub header_template: Option<String>,
517 pub footer_template: Option<String>,
519 pub print_background: Option<bool>,
521 pub landscape: Option<bool>,
523 pub page_ranges: Option<String>,
525 pub format: Option<String>,
527 pub width: Option<String>,
529 pub height: Option<String>,
531 pub prefer_css_page_size: Option<bool>,
533 pub margin: Option<PdfMargin>,
535}
536
537impl PdfOptions {
538 pub fn builder() -> PdfOptionsBuilder {
540 PdfOptionsBuilder::default()
541 }
542}
543
544#[derive(Debug, Clone, Default)]
546pub struct PdfOptionsBuilder {
547 path: Option<std::path::PathBuf>,
548 scale: Option<f64>,
549 display_header_footer: Option<bool>,
550 header_template: Option<String>,
551 footer_template: Option<String>,
552 print_background: Option<bool>,
553 landscape: Option<bool>,
554 page_ranges: Option<String>,
555 format: Option<String>,
556 width: Option<String>,
557 height: Option<String>,
558 prefer_css_page_size: Option<bool>,
559 margin: Option<PdfMargin>,
560}
561
562impl PdfOptionsBuilder {
563 pub fn path(mut self, path: std::path::PathBuf) -> Self {
565 self.path = Some(path);
566 self
567 }
568
569 pub fn scale(mut self, scale: f64) -> Self {
571 self.scale = Some(scale);
572 self
573 }
574
575 pub fn display_header_footer(mut self, display: bool) -> Self {
577 self.display_header_footer = Some(display);
578 self
579 }
580
581 pub fn header_template(mut self, template: impl Into<String>) -> Self {
583 self.header_template = Some(template.into());
584 self
585 }
586
587 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
589 self.footer_template = Some(template.into());
590 self
591 }
592
593 pub fn print_background(mut self, print: bool) -> Self {
595 self.print_background = Some(print);
596 self
597 }
598
599 pub fn landscape(mut self, landscape: bool) -> Self {
601 self.landscape = Some(landscape);
602 self
603 }
604
605 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
607 self.page_ranges = Some(ranges.into());
608 self
609 }
610
611 pub fn format(mut self, format: impl Into<String>) -> Self {
613 self.format = Some(format.into());
614 self
615 }
616
617 pub fn width(mut self, width: impl Into<String>) -> Self {
619 self.width = Some(width.into());
620 self
621 }
622
623 pub fn height(mut self, height: impl Into<String>) -> Self {
625 self.height = Some(height.into());
626 self
627 }
628
629 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
631 self.prefer_css_page_size = Some(prefer);
632 self
633 }
634
635 pub fn margin(mut self, margin: PdfMargin) -> Self {
637 self.margin = Some(margin);
638 self
639 }
640
641 pub fn build(self) -> PdfOptions {
643 PdfOptions {
644 path: self.path,
645 scale: self.scale,
646 display_header_footer: self.display_header_footer,
647 header_template: self.header_template,
648 footer_template: self.footer_template,
649 print_background: self.print_background,
650 landscape: self.landscape,
651 page_ranges: self.page_ranges,
652 format: self.format,
653 width: self.width,
654 height: self.height,
655 prefer_css_page_size: self.prefer_css_page_size,
656 margin: self.margin,
657 }
658 }
659}