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