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, serde::Serialize)]
122#[serde(rename_all = "camelCase")]
123#[non_exhaustive]
124pub struct ScreenshotOptions {
125    /// Image format (png or jpeg)
126    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
127    pub screenshot_type: Option<ScreenshotType>,
128    /// JPEG quality (0-100), only applies to jpeg format
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub quality: Option<u8>,
131    /// Capture full scrollable page
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub full_page: Option<bool>,
134    /// Clip region to capture
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub clip: Option<ScreenshotClip>,
137    /// Hide default white background (PNG only)
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub omit_background: Option<bool>,
140    /// Freeze CSS animations and transitions before capturing (stable shots)
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub animations: Option<Animations>,
143    /// Hide or keep the text caret
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub caret: Option<Caret>,
146    /// CSS-pixel vs device-pixel scale
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub scale: Option<Scale>,
149    /// CSS to inject into the page before capturing (e.g. hide dynamic elements)
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub style: Option<String>,
152    /// Locators to mask out (overpaint) with a solid box, pre-serialized to the
153    /// protocol `{ frame, selector }` shape. Build via the builder's `mask`.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub mask: Option<Vec<serde_json::Value>>,
156    /// CSS color of the mask boxes (e.g. `"#FF00FF"`); Playwright defaults to pink.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub mask_color: Option<String>,
159    /// Screenshot timeout in milliseconds
160    #[serde(serialize_with = "crate::protocol::serialize_timeout_or_default")]
161    pub timeout: Option<f64>,
162}
163
164impl ScreenshotOptions {
165    /// Create a new builder for ScreenshotOptions
166    pub fn builder() -> ScreenshotOptionsBuilder {
167        ScreenshotOptionsBuilder::default()
168    }
169
170    /// Convert options to JSON value for protocol
171    pub(crate) fn to_json(&self) -> serde_json::Value {
172        serde_json::to_value(self).expect("ScreenshotOptions serialization cannot fail")
173    }
174}
175
176/// Builder for ScreenshotOptions
177///
178/// Provides a fluent API for constructing screenshot options.
179#[derive(Debug, Clone, Default)]
180pub struct ScreenshotOptionsBuilder {
181    screenshot_type: Option<ScreenshotType>,
182    quality: Option<u8>,
183    full_page: Option<bool>,
184    clip: Option<ScreenshotClip>,
185    omit_background: Option<bool>,
186    animations: Option<Animations>,
187    caret: Option<Caret>,
188    scale: Option<Scale>,
189    style: Option<String>,
190    mask: Option<Vec<serde_json::Value>>,
191    mask_color: Option<String>,
192    timeout: Option<f64>,
193}
194
195impl ScreenshotOptionsBuilder {
196    /// Set the screenshot format (png or jpeg)
197    pub fn screenshot_type(mut self, screenshot_type: ScreenshotType) -> Self {
198        self.screenshot_type = Some(screenshot_type);
199        self
200    }
201
202    /// Set JPEG quality (0-100)
203    ///
204    /// Only applies when screenshot_type is Jpeg.
205    pub fn quality(mut self, quality: u8) -> Self {
206        self.quality = Some(quality);
207        self
208    }
209
210    /// Capture full scrollable page beyond viewport
211    pub fn full_page(mut self, full_page: bool) -> Self {
212        self.full_page = Some(full_page);
213        self
214    }
215
216    /// Set clip region to capture
217    pub fn clip(mut self, clip: ScreenshotClip) -> Self {
218        self.clip = Some(clip);
219        self
220    }
221
222    /// Hide default white background (creates transparent PNG)
223    pub fn omit_background(mut self, omit_background: bool) -> Self {
224        self.omit_background = Some(omit_background);
225        self
226    }
227
228    /// Freeze CSS animations and transitions before capturing.
229    ///
230    /// Use [`Animations::Disabled`] for stable screenshots (the value
231    /// Playwright's own visual assertions use).
232    pub fn animations(mut self, animations: Animations) -> Self {
233        self.animations = Some(animations);
234        self
235    }
236
237    /// Hide or keep the text caret during capture.
238    pub fn caret(mut self, caret: Caret) -> Self {
239        self.caret = Some(caret);
240        self
241    }
242
243    /// Capture at CSS-pixel or device-pixel scale.
244    pub fn scale(mut self, scale: Scale) -> Self {
245        self.scale = Some(scale);
246        self
247    }
248
249    /// Inject a CSS stylesheet into the page before capturing.
250    pub fn style(mut self, style: impl Into<String>) -> Self {
251        self.style = Some(style.into());
252        self
253    }
254
255    /// Overpaint the given locators with a solid box (mask out dynamic or
256    /// sensitive content). Each locator may match multiple elements; all are
257    /// masked. Pair with [`mask_color`](Self::mask_color) to set the box color.
258    pub fn mask(mut self, mask: Vec<crate::protocol::Locator>) -> Self {
259        self.mask = Some(mask.iter().map(|l| l.mask_json()).collect());
260        self
261    }
262
263    /// CSS color of the [`mask`](Self::mask) boxes (e.g. `"#FF00FF"` or
264    /// `"rgba(0,0,0,0.5)"`). Defaults to pink when unset.
265    pub fn mask_color(mut self, mask_color: impl Into<String>) -> Self {
266        self.mask_color = Some(mask_color.into());
267        self
268    }
269
270    /// Set screenshot timeout in milliseconds
271    pub fn timeout(mut self, timeout: f64) -> Self {
272        self.timeout = Some(timeout);
273        self
274    }
275
276    /// Build the ScreenshotOptions
277    pub fn build(self) -> ScreenshotOptions {
278        ScreenshotOptions {
279            screenshot_type: self.screenshot_type,
280            quality: self.quality,
281            full_page: self.full_page,
282            clip: self.clip,
283            omit_background: self.omit_background,
284            animations: self.animations,
285            caret: self.caret,
286            scale: self.scale,
287            style: self.style,
288            mask: self.mask,
289            mask_color: self.mask_color,
290            timeout: self.timeout,
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_screenshot_type_serialization() {
301        assert_eq!(
302            serde_json::to_string(&ScreenshotType::Png).unwrap(),
303            "\"png\""
304        );
305        assert_eq!(
306            serde_json::to_string(&ScreenshotType::Jpeg).unwrap(),
307            "\"jpeg\""
308        );
309    }
310
311    #[test]
312    fn test_builder_jpeg_with_quality() {
313        let options = ScreenshotOptions::builder()
314            .screenshot_type(ScreenshotType::Jpeg)
315            .quality(80)
316            .build();
317
318        let json = options.to_json();
319        assert_eq!(json["type"], "jpeg");
320        assert_eq!(json["quality"], 80);
321    }
322
323    #[test]
324    fn test_builder_full_page() {
325        let options = ScreenshotOptions::builder().full_page(true).build();
326
327        let json = options.to_json();
328        assert_eq!(json["fullPage"], true);
329    }
330
331    #[test]
332    fn test_builder_clip() {
333        let clip = ScreenshotClip {
334            x: 10.0,
335            y: 20.0,
336            width: 300.0,
337            height: 200.0,
338        };
339        let options = ScreenshotOptions::builder().clip(clip).build();
340
341        let json = options.to_json();
342        assert_eq!(json["clip"]["x"], 10.0);
343        assert_eq!(json["clip"]["y"], 20.0);
344        assert_eq!(json["clip"]["width"], 300.0);
345        assert_eq!(json["clip"]["height"], 200.0);
346    }
347
348    #[test]
349    fn test_builder_omit_background() {
350        let options = ScreenshotOptions::builder().omit_background(true).build();
351
352        let json = options.to_json();
353        assert_eq!(json["omitBackground"], true);
354    }
355
356    #[test]
357    fn test_builder_animations() {
358        let json = ScreenshotOptions::builder()
359            .animations(Animations::Disabled)
360            .build()
361            .to_json();
362        assert_eq!(json["animations"], "disabled");
363
364        let json = ScreenshotOptions::builder()
365            .animations(Animations::Allow)
366            .build()
367            .to_json();
368        assert_eq!(json["animations"], "allow");
369    }
370
371    #[test]
372    fn test_builder_caret() {
373        let json = ScreenshotOptions::builder()
374            .caret(Caret::Hide)
375            .build()
376            .to_json();
377        assert_eq!(json["caret"], "hide");
378    }
379
380    #[test]
381    fn test_builder_scale() {
382        let json = ScreenshotOptions::builder()
383            .scale(Scale::Device)
384            .build()
385            .to_json();
386        assert_eq!(json["scale"], "device");
387    }
388
389    #[test]
390    fn test_builder_style() {
391        let json = ScreenshotOptions::builder()
392            .style(".flaky { visibility: hidden; }")
393            .build()
394            .to_json();
395        assert_eq!(json["style"], ".flaky { visibility: hidden; }");
396    }
397
398    #[test]
399    fn test_builder_mask_color() {
400        let json = ScreenshotOptions::builder()
401            .mask_color("#FF00FF")
402            .build()
403            .to_json();
404        assert_eq!(json["maskColor"], "#FF00FF");
405    }
406
407    #[test]
408    fn test_mask_serializes_to_array() {
409        // The builder's `mask` needs a real Locator (browser-only), so construct
410        // the pre-serialized form directly to lock the `to_json` mask branch.
411        let options = ScreenshotOptions {
412            mask: Some(vec![serde_json::json!({
413                "frame": { "guid": "frame@1" },
414                "selector": "h1",
415            })]),
416            ..Default::default()
417        };
418        let json = options.to_json();
419        assert_eq!(json["mask"][0]["frame"]["guid"], "frame@1");
420        assert_eq!(json["mask"][0]["selector"], "h1");
421    }
422
423    #[test]
424    fn test_unset_options_absent() {
425        let json = ScreenshotOptions::builder().build().to_json();
426        assert!(json.get("animations").is_none());
427        assert!(json.get("caret").is_none());
428        assert!(json.get("scale").is_none());
429        assert!(json.get("style").is_none());
430        assert!(json.get("mask").is_none());
431        assert!(json.get("maskColor").is_none());
432    }
433
434    #[test]
435    fn test_builder_multiple_options() {
436        let options = ScreenshotOptions::builder()
437            .screenshot_type(ScreenshotType::Jpeg)
438            .quality(90)
439            .full_page(true)
440            .timeout(5000.0)
441            .build();
442
443        let json = options.to_json();
444        assert_eq!(json["type"], "jpeg");
445        assert_eq!(json["quality"], 90);
446        assert_eq!(json["fullPage"], true);
447        assert_eq!(json["timeout"], 5000.0);
448    }
449}