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 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    // Internal accessibility method (called by Accessibility struct)
417    //
418    // The legacy `accessibilitySnapshot` RPC was removed in modern Playwright.
419    // We implement snapshot() using `FrameAriaSnapshot` on the main frame, which
420    // returns the ARIA accessibility tree as a YAML string (the current equivalent).
421    // The YAML string is returned as a JSON string Value for API compatibility.
422
423    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    // Internal coverage methods (called by Coverage struct)
434
435    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/// Margin options for PDF generation.
506///
507/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
508#[derive(Debug, Clone, Default, Serialize)]
509pub struct PdfMargin {
510    /// Top margin (e.g. `"1in"`)
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub top: Option<String>,
513    /// Right margin
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub right: Option<String>,
516    /// Bottom margin
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub bottom: Option<String>,
519    /// Left margin
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub left: Option<String>,
522}
523
524/// Options for generating a PDF from a page.
525///
526/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
527/// Firefox or WebKit will result in an error.
528///
529/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
530#[derive(Debug, Clone, Default)]
531#[non_exhaustive]
532pub struct PdfOptions {
533    /// If specified, the PDF will also be saved to this file path.
534    pub path: Option<std::path::PathBuf>,
535    /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
536    pub scale: Option<f64>,
537    /// Whether to display header and footer (default false).
538    pub display_header_footer: Option<bool>,
539    /// HTML template for the print header. Should be valid HTML.
540    pub header_template: Option<String>,
541    /// HTML template for the print footer.
542    pub footer_template: Option<String>,
543    /// Whether to print background graphics (default false).
544    pub print_background: Option<bool>,
545    /// Paper orientation — `true` for landscape (default false).
546    pub landscape: Option<bool>,
547    /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
548    pub page_ranges: Option<String>,
549    /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
550    pub format: Option<String>,
551    /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
552    pub width: Option<String>,
553    /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
554    pub height: Option<String>,
555    /// Whether or not to prefer page size as defined by CSS.
556    pub prefer_css_page_size: Option<bool>,
557    /// Paper margins, defaulting to none.
558    pub margin: Option<PdfMargin>,
559}
560
561impl PdfOptions {
562    /// Creates a new builder for PdfOptions
563    pub fn builder() -> PdfOptionsBuilder {
564        PdfOptionsBuilder::default()
565    }
566}
567
568/// Builder for PdfOptions
569#[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    /// Sets the file path for saving the PDF
588    pub fn path(mut self, path: std::path::PathBuf) -> Self {
589        self.path = Some(path);
590        self
591    }
592
593    /// Sets the scale of the webpage rendering
594    pub fn scale(mut self, scale: f64) -> Self {
595        self.scale = Some(scale);
596        self
597    }
598
599    /// Sets whether to display header and footer
600    pub fn display_header_footer(mut self, display: bool) -> Self {
601        self.display_header_footer = Some(display);
602        self
603    }
604
605    /// Sets the HTML template for the print header
606    pub fn header_template(mut self, template: impl Into<String>) -> Self {
607        self.header_template = Some(template.into());
608        self
609    }
610
611    /// Sets the HTML template for the print footer
612    pub fn footer_template(mut self, template: impl Into<String>) -> Self {
613        self.footer_template = Some(template.into());
614        self
615    }
616
617    /// Sets whether to print background graphics
618    pub fn print_background(mut self, print: bool) -> Self {
619        self.print_background = Some(print);
620        self
621    }
622
623    /// Sets whether to use landscape orientation
624    pub fn landscape(mut self, landscape: bool) -> Self {
625        self.landscape = Some(landscape);
626        self
627    }
628
629    /// Sets the page ranges to print
630    pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
631        self.page_ranges = Some(ranges.into());
632        self
633    }
634
635    /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
636    pub fn format(mut self, format: impl Into<String>) -> Self {
637        self.format = Some(format.into());
638        self
639    }
640
641    /// Sets the paper width
642    pub fn width(mut self, width: impl Into<String>) -> Self {
643        self.width = Some(width.into());
644        self
645    }
646
647    /// Sets the paper height
648    pub fn height(mut self, height: impl Into<String>) -> Self {
649        self.height = Some(height.into());
650        self
651    }
652
653    /// Sets whether to prefer page size as defined by CSS
654    pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
655        self.prefer_css_page_size = Some(prefer);
656        self
657    }
658
659    /// Sets the paper margins
660    pub fn margin(mut self, margin: PdfMargin) -> Self {
661        self.margin = Some(margin);
662        self
663    }
664
665    /// Builds the PdfOptions
666    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}