Skip to main content

playwright_rs/protocol/
screenshot.rs

1// Screenshot types and options
2//
3// Provides configuration for page and element screenshots, matching Playwright's API.
4
5use serde::Serialize;
6
7/// Whether to play or freeze CSS animations and transitions during capture.
8///
9/// Used by [`ScreenshotOptions`] and by the `to_have_screenshot` visual
10/// assertions. `Disabled` is the value to use for stable screenshots.
11///
12/// See: <https://playwright.dev/docs/api/class-page#page-screenshot-option-animations>
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "lowercase")]
15#[non_exhaustive]
16pub enum Animations {
17    /// Allow animations to run normally.
18    Allow,
19    /// Disable CSS animations and transitions before capturing.
20    Disabled,
21}
22
23/// Screenshot image format
24///
25/// # Example
26///
27/// ```no_run
28/// use playwright_rs::protocol::ScreenshotType;
29///
30/// let screenshot_type = ScreenshotType::Jpeg;
31/// ```
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "lowercase")]
34#[non_exhaustive]
35pub enum ScreenshotType {
36    /// PNG format (lossless, supports transparency)
37    Png,
38    /// JPEG format (lossy compression, smaller file size)
39    Jpeg,
40    /// WebP format (lossy compression, smaller than JPEG at equal quality)
41    ///
42    /// Honors `quality` the same way `Jpeg` does.
43    Webp,
44}
45
46/// Text-caret handling during screenshot capture.
47///
48/// See: <https://playwright.dev/docs/api/class-page#page-screenshot-option-caret>
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "lowercase")]
51#[non_exhaustive]
52pub enum Caret {
53    /// Hide the text caret before capturing (Playwright's default).
54    Hide,
55    /// Leave the caret untouched.
56    Initial,
57}
58
59/// Pixel scale for the captured image.
60///
61/// See: <https://playwright.dev/docs/api/class-page#page-screenshot-option-scale>
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
63#[serde(rename_all = "lowercase")]
64#[non_exhaustive]
65pub enum Scale {
66    /// One pixel per CSS pixel; keeps screenshots small (Playwright's default).
67    Css,
68    /// One pixel per device pixel; sharper on HiDPI displays.
69    Device,
70}
71
72/// Clip region for screenshot
73///
74/// Specifies a rectangular region to capture.
75///
76/// # Example
77///
78/// ```no_run
79/// use playwright_rs::protocol::ScreenshotClip;
80///
81/// let clip = ScreenshotClip {
82///     x: 10.0,
83///     y: 20.0,
84///     width: 300.0,
85///     height: 200.0,
86/// };
87/// ```
88#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
89pub struct ScreenshotClip {
90    /// X coordinate of clip region origin
91    pub x: f64,
92    /// Y coordinate of clip region origin
93    pub y: f64,
94    /// Width of clip region
95    pub width: f64,
96    /// Height of clip region
97    pub height: f64,
98}
99
100/// Screenshot options
101///
102/// Configuration options for page and element screenshots.
103///
104/// Use the builder pattern to construct options:
105///
106/// # Example
107///
108/// ```no_run
109/// use playwright_rs::protocol::{ScreenshotOptions, ScreenshotType, ScreenshotClip};
110/// use playwright_rs::Animations;
111///
112/// // JPEG with quality
113/// let options = ScreenshotOptions::builder()
114///     .screenshot_type(ScreenshotType::Jpeg)
115///     .quality(80)
116///     .build();
117///
118/// // Stable screenshot: freeze animations and hide the caret
119/// let options = ScreenshotOptions::builder()
120///     .animations(Animations::Disabled)
121///     .build();
122/// ```
123///
124/// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
125#[derive(Debug, Clone, Default, serde::Serialize)]
126#[serde(rename_all = "camelCase")]
127#[non_exhaustive]
128pub struct ScreenshotOptions {
129    /// Image format (png, jpeg or webp)
130    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
131    pub screenshot_type: Option<ScreenshotType>,
132    /// Image quality (0-100). Applies to the lossy formats, `Jpeg` and `Webp`.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub quality: Option<u8>,
135    /// Capture full scrollable page
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub full_page: Option<bool>,
138    /// Clip region to capture
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub clip: Option<ScreenshotClip>,
141    /// Hide default white background (PNG only)
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub omit_background: Option<bool>,
144    /// Freeze CSS animations and transitions before capturing (stable shots)
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub animations: Option<Animations>,
147    /// Hide or keep the text caret
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub caret: Option<Caret>,
150    /// CSS-pixel vs device-pixel scale
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub scale: Option<Scale>,
153    /// CSS to inject into the page before capturing (e.g. hide dynamic elements)
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub style: Option<String>,
156    /// Locators to mask out (overpaint) with a solid box, pre-serialized to the
157    /// protocol `{ frame, selector }` shape. Build via the builder's `mask`.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub mask: Option<Vec<serde_json::Value>>,
160    /// CSS color of the mask boxes (e.g. `"#FF00FF"`); Playwright defaults to pink.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub mask_color: Option<String>,
163    /// Screenshot timeout in milliseconds
164    #[serde(serialize_with = "crate::protocol::serialize_timeout_or_default")]
165    pub timeout: Option<f64>,
166}
167
168impl ScreenshotOptions {
169    /// Create a new builder for ScreenshotOptions
170    pub fn builder() -> ScreenshotOptionsBuilder {
171        ScreenshotOptionsBuilder::default()
172    }
173
174    /// Convert options to JSON value for protocol
175    pub(crate) fn to_json(&self) -> serde_json::Value {
176        serde_json::to_value(self).expect("ScreenshotOptions serialization cannot fail")
177    }
178}
179
180/// Builder for ScreenshotOptions
181///
182/// Provides a fluent API for constructing screenshot options.
183#[derive(Debug, Clone, Default)]
184pub struct ScreenshotOptionsBuilder {
185    screenshot_type: Option<ScreenshotType>,
186    quality: Option<u8>,
187    full_page: Option<bool>,
188    clip: Option<ScreenshotClip>,
189    omit_background: Option<bool>,
190    animations: Option<Animations>,
191    caret: Option<Caret>,
192    scale: Option<Scale>,
193    style: Option<String>,
194    mask: Option<Vec<serde_json::Value>>,
195    mask_color: Option<String>,
196    timeout: Option<f64>,
197}
198
199impl ScreenshotOptionsBuilder {
200    /// Set the screenshot format (png, jpeg or webp)
201    pub fn screenshot_type(mut self, screenshot_type: ScreenshotType) -> Self {
202        self.screenshot_type = Some(screenshot_type);
203        self
204    }
205
206    /// Set image quality (0-100), for the lossy formats (`Jpeg`, `Webp`)
207    ///
208    /// Ignored for `Png`, which is lossless.
209    pub fn quality(mut self, quality: u8) -> Self {
210        self.quality = Some(quality);
211        self
212    }
213
214    /// Capture full scrollable page beyond viewport
215    pub fn full_page(mut self, full_page: bool) -> Self {
216        self.full_page = Some(full_page);
217        self
218    }
219
220    /// Set clip region to capture
221    pub fn clip(mut self, clip: ScreenshotClip) -> Self {
222        self.clip = Some(clip);
223        self
224    }
225
226    /// Hide default white background (creates transparent PNG)
227    pub fn omit_background(mut self, omit_background: bool) -> Self {
228        self.omit_background = Some(omit_background);
229        self
230    }
231
232    /// Freeze CSS animations and transitions before capturing.
233    ///
234    /// Use [`Animations::Disabled`] for stable screenshots (the value
235    /// Playwright's own visual assertions use).
236    pub fn animations(mut self, animations: Animations) -> Self {
237        self.animations = Some(animations);
238        self
239    }
240
241    /// Hide or keep the text caret during capture.
242    pub fn caret(mut self, caret: Caret) -> Self {
243        self.caret = Some(caret);
244        self
245    }
246
247    /// Capture at CSS-pixel or device-pixel scale.
248    pub fn scale(mut self, scale: Scale) -> Self {
249        self.scale = Some(scale);
250        self
251    }
252
253    /// Inject a CSS stylesheet into the page before capturing.
254    pub fn style(mut self, style: impl Into<String>) -> Self {
255        self.style = Some(style.into());
256        self
257    }
258
259    /// Overpaint the given locators with a solid box (mask out dynamic or
260    /// sensitive content). Each locator may match multiple elements; all are
261    /// masked. Pair with [`mask_color`](Self::mask_color) to set the box color.
262    pub fn mask(mut self, mask: Vec<crate::protocol::Locator>) -> Self {
263        self.mask = Some(mask.iter().map(|l| l.mask_json()).collect());
264        self
265    }
266
267    /// CSS color of the [`mask`](Self::mask) boxes (e.g. `"#FF00FF"` or
268    /// `"rgba(0,0,0,0.5)"`). Defaults to pink when unset.
269    pub fn mask_color(mut self, mask_color: impl Into<String>) -> Self {
270        self.mask_color = Some(mask_color.into());
271        self
272    }
273
274    /// Set screenshot timeout in milliseconds
275    pub fn timeout(mut self, timeout: f64) -> Self {
276        self.timeout = Some(timeout);
277        self
278    }
279
280    /// Build the ScreenshotOptions
281    pub fn build(self) -> ScreenshotOptions {
282        ScreenshotOptions {
283            screenshot_type: self.screenshot_type,
284            quality: self.quality,
285            full_page: self.full_page,
286            clip: self.clip,
287            omit_background: self.omit_background,
288            animations: self.animations,
289            caret: self.caret,
290            scale: self.scale,
291            style: self.style,
292            mask: self.mask,
293            mask_color: self.mask_color,
294            timeout: self.timeout,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_screenshot_type_serialization() {
305        assert_eq!(
306            serde_json::to_string(&ScreenshotType::Png).unwrap(),
307            "\"png\""
308        );
309        assert_eq!(
310            serde_json::to_string(&ScreenshotType::Jpeg).unwrap(),
311            "\"jpeg\""
312        );
313        assert_eq!(
314            serde_json::to_string(&ScreenshotType::Webp).unwrap(),
315            "\"webp\""
316        );
317    }
318
319    #[test]
320    fn test_builder_webp_with_quality() {
321        let options = ScreenshotOptions::builder()
322            .screenshot_type(ScreenshotType::Webp)
323            .quality(70)
324            .build();
325
326        let json = options.to_json();
327        assert_eq!(json["type"], "webp");
328        assert_eq!(json["quality"], 70);
329    }
330
331    #[test]
332    fn test_builder_jpeg_with_quality() {
333        let options = ScreenshotOptions::builder()
334            .screenshot_type(ScreenshotType::Jpeg)
335            .quality(80)
336            .build();
337
338        let json = options.to_json();
339        assert_eq!(json["type"], "jpeg");
340        assert_eq!(json["quality"], 80);
341    }
342
343    #[test]
344    fn test_builder_full_page() {
345        let options = ScreenshotOptions::builder().full_page(true).build();
346
347        let json = options.to_json();
348        assert_eq!(json["fullPage"], true);
349    }
350
351    #[test]
352    fn test_builder_clip() {
353        let clip = ScreenshotClip {
354            x: 10.0,
355            y: 20.0,
356            width: 300.0,
357            height: 200.0,
358        };
359        let options = ScreenshotOptions::builder().clip(clip).build();
360
361        let json = options.to_json();
362        assert_eq!(json["clip"]["x"], 10.0);
363        assert_eq!(json["clip"]["y"], 20.0);
364        assert_eq!(json["clip"]["width"], 300.0);
365        assert_eq!(json["clip"]["height"], 200.0);
366    }
367
368    #[test]
369    fn test_builder_omit_background() {
370        let options = ScreenshotOptions::builder().omit_background(true).build();
371
372        let json = options.to_json();
373        assert_eq!(json["omitBackground"], true);
374    }
375
376    #[test]
377    fn test_builder_animations() {
378        let json = ScreenshotOptions::builder()
379            .animations(Animations::Disabled)
380            .build()
381            .to_json();
382        assert_eq!(json["animations"], "disabled");
383
384        let json = ScreenshotOptions::builder()
385            .animations(Animations::Allow)
386            .build()
387            .to_json();
388        assert_eq!(json["animations"], "allow");
389    }
390
391    #[test]
392    fn test_builder_caret() {
393        let json = ScreenshotOptions::builder()
394            .caret(Caret::Hide)
395            .build()
396            .to_json();
397        assert_eq!(json["caret"], "hide");
398    }
399
400    #[test]
401    fn test_builder_scale() {
402        let json = ScreenshotOptions::builder()
403            .scale(Scale::Device)
404            .build()
405            .to_json();
406        assert_eq!(json["scale"], "device");
407    }
408
409    #[test]
410    fn test_builder_style() {
411        let json = ScreenshotOptions::builder()
412            .style(".flaky { visibility: hidden; }")
413            .build()
414            .to_json();
415        assert_eq!(json["style"], ".flaky { visibility: hidden; }");
416    }
417
418    #[test]
419    fn test_builder_mask_color() {
420        let json = ScreenshotOptions::builder()
421            .mask_color("#FF00FF")
422            .build()
423            .to_json();
424        assert_eq!(json["maskColor"], "#FF00FF");
425    }
426
427    #[test]
428    fn test_mask_serializes_to_array() {
429        // The builder's `mask` needs a real Locator (browser-only), so construct
430        // the pre-serialized form directly to lock the `to_json` mask branch.
431        let options = ScreenshotOptions {
432            mask: Some(vec![serde_json::json!({
433                "frame": { "guid": "frame@1" },
434                "selector": "h1",
435            })]),
436            ..Default::default()
437        };
438        let json = options.to_json();
439        assert_eq!(json["mask"][0]["frame"]["guid"], "frame@1");
440        assert_eq!(json["mask"][0]["selector"], "h1");
441    }
442
443    #[test]
444    fn test_unset_options_absent() {
445        let json = ScreenshotOptions::builder().build().to_json();
446        assert!(json.get("animations").is_none());
447        assert!(json.get("caret").is_none());
448        assert!(json.get("scale").is_none());
449        assert!(json.get("style").is_none());
450        assert!(json.get("mask").is_none());
451        assert!(json.get("maskColor").is_none());
452    }
453
454    #[test]
455    fn test_builder_multiple_options() {
456        let options = ScreenshotOptions::builder()
457            .screenshot_type(ScreenshotType::Jpeg)
458            .quality(90)
459            .full_page(true)
460            .timeout(5000.0)
461            .build();
462
463        let json = options.to_json();
464        assert_eq!(json["type"], "jpeg");
465        assert_eq!(json["quality"], 90);
466        assert_eq!(json["fullPage"], true);
467        assert_eq!(json["timeout"], 5000.0);
468    }
469}