Skip to main content

playwright_rs/protocol/page/
capture.rs

1use 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
10/// Capture: screenshots, PDF, screencast, coverage and accessibility snapshots.
11impl Page {
12    /// Takes a screenshot of the page and returns the image bytes.
13    ///
14    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
15    #[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            // Default to PNG with required timeout
25            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        // Decode base64 to bytes
39        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    /// Takes a screenshot and saves it to a file, also returning the bytes.
50    ///
51    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
52    #[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        // Get the screenshot bytes
60        let bytes = self.screenshot(options).await?;
61
62        // Write to file
63        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    /// Generates a PDF of the page and returns it as bytes.
71    ///
72    /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
73    /// not supported in Firefox or WebKit.
74    ///
75    /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
76    /// saved to that file.
77    ///
78    /// # Arguments
79    ///
80    /// * `options` - Optional PDF generation options
81    ///
82    /// # Example
83    ///
84    /// ```no_run
85    /// # use playwright_rs::protocol::Playwright;
86    /// # #[tokio::main]
87    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
88    /// # let playwright = Playwright::launch().await?;
89    /// # let browser = playwright.chromium().launch().await?;
90    /// # let page = browser.new_page().await?;
91    /// let pdf_bytes = page.pdf(None).await?;
92    /// assert!(!pdf_bytes.is_empty());
93    /// # Ok(())
94    /// # }
95    /// ```
96    ///
97    /// # Errors
98    ///
99    /// Returns error if:
100    /// - The browser is not Chromium (PDF only supported in Chromium)
101    /// - Page has been closed
102    /// - Communication with browser process fails
103    ///
104    /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
105    #[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            // Capture the file path before consuming opts
113            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        // Decode base64 to bytes
163        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 a path was specified, save the PDF to disk as well
170        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    /// Returns the `Accessibility` object for this page.
185    ///
186    /// Use `accessibility().snapshot()` to capture the current state of the
187    /// page's accessibility tree.
188    ///
189    /// See: <https://playwright.dev/docs/api/class-page#page-accessibility>
190    pub fn accessibility(&self) -> crate::protocol::Accessibility {
191        crate::protocol::Accessibility::new(self.clone())
192    }
193
194    /// Returns the ARIA accessibility tree for the page as a YAML string.
195    ///
196    /// Page-level shorthand for `page.locator("body").aria_snapshot(...)`. Useful
197    /// for asserting page-wide accessibility structure without first selecting
198    /// `body` explicitly.
199    ///
200    /// Pass `Some(AriaSnapshotOptions::default().mode(AriaSnapshotMode::Ai))`
201    /// to get the AI-friendly form intended for LLM/codegen consumption.
202    ///
203    /// See: <https://playwright.dev/docs/api/class-page#page-aria-snapshot>
204    #[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    /// The whole document's accessibility tree, as JSON rather than the YAML
221    /// markup [`aria_snapshot`](Self::aria_snapshot) returns, so a caller can
222    /// walk it instead of parsing text.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the driver rejects the request or the page closes
227    /// first.
228    ///
229    /// See: <https://playwright.dev/docs/api/class-page#page-aria-snapshot-json>
230    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    /// Returns the `Coverage` object for this page (Chromium only).
246    ///
247    /// Use `coverage().start_js_coverage()` / `stop_js_coverage()` and
248    /// `start_css_coverage()` / `stop_css_coverage()` to collect code coverage data.
249    ///
250    /// Coverage is only available in Chromium. Calling coverage methods on
251    /// Firefox or WebKit will return an error from the Playwright server.
252    ///
253    /// See: <https://playwright.dev/docs/api/class-page#page-coverage>
254    pub fn coverage(&self) -> crate::protocol::Coverage {
255        crate::protocol::Coverage::new(self.clone())
256    }
257
258    /// Returns the live-screencast handle for this page.
259    ///
260    /// Register frame handlers via [`Screencast::on_frame`](crate::Screencast::on_frame), then call
261    /// [`Screencast::start`](crate::Screencast::start) to begin streaming. JPEG frames arrive on
262    /// the registered handlers as the browser renders.
263    ///
264    /// See: <https://playwright.dev/docs/api/class-page#page-screencast>
265    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    // Internal accessibility method (called by Accessibility struct)
393    //
394    // The legacy `accessibilitySnapshot` RPC was removed in modern Playwright.
395    // We implement snapshot() using `FrameAriaSnapshot` on the main frame, which
396    // returns the ARIA accessibility tree as a YAML string (the current equivalent).
397    // The YAML string is returned as a JSON string Value for API compatibility.
398
399    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    // Internal coverage methods (called by Coverage struct)
410
411    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/// Margin options for PDF generation.
482///
483/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
484#[derive(Debug, Clone, Default, Serialize)]
485pub struct PdfMargin {
486    /// Top margin (e.g. `"1in"`)
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub top: Option<String>,
489    /// Right margin
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub right: Option<String>,
492    /// Bottom margin
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub bottom: Option<String>,
495    /// Left margin
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub left: Option<String>,
498}
499
500/// Options for generating a PDF from a page.
501///
502/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
503/// Firefox or WebKit will result in an error.
504///
505/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
506#[derive(Debug, Clone, Default)]
507#[non_exhaustive]
508pub struct PdfOptions {
509    /// If specified, the PDF will also be saved to this file path.
510    pub path: Option<std::path::PathBuf>,
511    /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
512    pub scale: Option<f64>,
513    /// Whether to display header and footer (default false).
514    pub display_header_footer: Option<bool>,
515    /// HTML template for the print header. Should be valid HTML.
516    pub header_template: Option<String>,
517    /// HTML template for the print footer.
518    pub footer_template: Option<String>,
519    /// Whether to print background graphics (default false).
520    pub print_background: Option<bool>,
521    /// Paper orientation — `true` for landscape (default false).
522    pub landscape: Option<bool>,
523    /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
524    pub page_ranges: Option<String>,
525    /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
526    pub format: Option<String>,
527    /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
528    pub width: Option<String>,
529    /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
530    pub height: Option<String>,
531    /// Whether or not to prefer page size as defined by CSS.
532    pub prefer_css_page_size: Option<bool>,
533    /// Paper margins, defaulting to none.
534    pub margin: Option<PdfMargin>,
535}
536
537impl PdfOptions {
538    /// Creates a new builder for PdfOptions
539    pub fn builder() -> PdfOptionsBuilder {
540        PdfOptionsBuilder::default()
541    }
542}
543
544/// Builder for PdfOptions
545#[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    /// Sets the file path for saving the PDF
564    pub fn path(mut self, path: std::path::PathBuf) -> Self {
565        self.path = Some(path);
566        self
567    }
568
569    /// Sets the scale of the webpage rendering
570    pub fn scale(mut self, scale: f64) -> Self {
571        self.scale = Some(scale);
572        self
573    }
574
575    /// Sets whether to display header and footer
576    pub fn display_header_footer(mut self, display: bool) -> Self {
577        self.display_header_footer = Some(display);
578        self
579    }
580
581    /// Sets the HTML template for the print header
582    pub fn header_template(mut self, template: impl Into<String>) -> Self {
583        self.header_template = Some(template.into());
584        self
585    }
586
587    /// Sets the HTML template for the print footer
588    pub fn footer_template(mut self, template: impl Into<String>) -> Self {
589        self.footer_template = Some(template.into());
590        self
591    }
592
593    /// Sets whether to print background graphics
594    pub fn print_background(mut self, print: bool) -> Self {
595        self.print_background = Some(print);
596        self
597    }
598
599    /// Sets whether to use landscape orientation
600    pub fn landscape(mut self, landscape: bool) -> Self {
601        self.landscape = Some(landscape);
602        self
603    }
604
605    /// Sets the page ranges to print
606    pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
607        self.page_ranges = Some(ranges.into());
608        self
609    }
610
611    /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
612    pub fn format(mut self, format: impl Into<String>) -> Self {
613        self.format = Some(format.into());
614        self
615    }
616
617    /// Sets the paper width
618    pub fn width(mut self, width: impl Into<String>) -> Self {
619        self.width = Some(width.into());
620        self
621    }
622
623    /// Sets the paper height
624    pub fn height(mut self, height: impl Into<String>) -> Self {
625        self.height = Some(height.into());
626        self
627    }
628
629    /// Sets whether to prefer page size as defined by CSS
630    pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
631        self.prefer_css_page_size = Some(prefer);
632        self
633    }
634
635    /// Sets the paper margins
636    pub fn margin(mut self, margin: PdfMargin) -> Self {
637        self.margin = Some(margin);
638        self
639    }
640
641    /// Builds the PdfOptions
642    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}