Skip to main content

playwright_rs/protocol/page/
emulation.rs

1use super::Page;
2use crate::error::{Error, Result};
3use crate::protocol::browser_context::Viewport;
4use crate::server::channel_owner::ChannelOwner;
5use serde::Serialize;
6use std::sync::Arc;
7
8/// Emulation: media, viewport, and injected style and script tags.
9impl Page {
10    /// Adds a `<style>` tag into the page with the desired content.
11    ///
12    /// # Arguments
13    ///
14    /// * `options` - Style tag options (content, url, or path)
15    ///
16    /// # Returns
17    ///
18    /// Returns an ElementHandle pointing to the injected `<style>` tag
19    ///
20    /// # Example
21    ///
22    /// ```no_run
23    /// # use playwright_rs::protocol::Playwright;
24    /// # #[tokio::main]
25    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
26    /// # let playwright = Playwright::launch().await?;
27    /// # let browser = playwright.chromium().launch().await?;
28    /// # let context = browser.new_context().await?;
29    /// # let page = context.new_page().await?;
30    /// use playwright_rs::protocol::AddStyleTagOptions;
31    ///
32    /// // With inline CSS
33    /// page.add_style_tag(
34    ///     AddStyleTagOptions::builder()
35    ///         .content("body { background-color: red; }")
36    ///         .build()
37    /// ).await?;
38    ///
39    /// // With external URL
40    /// page.add_style_tag(
41    ///     AddStyleTagOptions::builder()
42    ///         .url("https://example.com/style.css")
43    ///         .build()
44    /// ).await?;
45    ///
46    /// // From file
47    /// page.add_style_tag(
48    ///     AddStyleTagOptions::builder()
49    ///         .path("./styles/custom.css")
50    ///         .build()
51    /// ).await?;
52    /// # Ok(())
53    /// # }
54    /// ```
55    ///
56    /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
57    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
58    pub async fn add_style_tag(
59        &self,
60        options: AddStyleTagOptions,
61    ) -> Result<Arc<crate::protocol::ElementHandle>> {
62        let frame = self.main_frame().await?;
63        frame.add_style_tag(options).await
64    }
65
66    /// Sets the viewport size for the page.
67    ///
68    /// This method allows dynamic resizing of the viewport after page creation,
69    /// useful for testing responsive layouts at different screen sizes.
70    ///
71    /// # Arguments
72    ///
73    /// * `viewport` - The viewport dimensions (width and height in pixels)
74    ///
75    /// # Example
76    ///
77    /// ```no_run
78    /// # use playwright_rs::protocol::{Playwright, Viewport};
79    /// # #[tokio::main]
80    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
81    /// # let playwright = Playwright::launch().await?;
82    /// # let browser = playwright.chromium().launch().await?;
83    /// # let page = browser.new_page().await?;
84    /// // Set viewport to mobile size
85    /// let mobile = Viewport {
86    ///     width: 375,
87    ///     height: 667,
88    /// };
89    /// page.set_viewport_size(mobile).await?;
90    ///
91    /// // Later, test desktop layout
92    /// let desktop = Viewport {
93    ///     width: 1920,
94    ///     height: 1080,
95    /// };
96    /// page.set_viewport_size(desktop).await?;
97    /// # Ok(())
98    /// # }
99    /// ```
100    ///
101    /// # Errors
102    ///
103    /// Returns error if:
104    /// - Page has been closed
105    /// - Communication with browser process fails
106    ///
107    /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
108    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
109    pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
110        // Store the new viewport locally so viewport_size() can reflect the change
111        if let Ok(mut guard) = self.viewport.write() {
112            *guard = Some(viewport.clone());
113        }
114        self.channel()
115            .send_no_result(
116                "setViewportSize",
117                serde_json::json!({ "viewportSize": viewport }),
118            )
119            .await
120    }
121
122    /// Emulates media features for the page.
123    ///
124    /// This method allows emulating CSS media features such as `media`, `color-scheme`,
125    /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
126    ///
127    /// To reset a specific feature to the browser default, use the `NoOverride` variant.
128    ///
129    /// # Arguments
130    ///
131    /// * `options` - Optional emulation options. If `None`, this is a no-op.
132    ///
133    /// # Example
134    ///
135    /// ```no_run
136    /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
137    /// # #[tokio::main]
138    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
139    /// # let playwright = Playwright::launch().await?;
140    /// # let browser = playwright.chromium().launch().await?;
141    /// # let page = browser.new_page().await?;
142    /// // Emulate print media
143    /// page.emulate_media(Some(
144    ///     EmulateMediaOptions::builder()
145    ///         .media(Media::Print)
146    ///         .build()
147    /// )).await?;
148    ///
149    /// // Emulate dark color scheme
150    /// page.emulate_media(Some(
151    ///     EmulateMediaOptions::builder()
152    ///         .color_scheme(ColorScheme::Dark)
153    ///         .build()
154    /// )).await?;
155    /// # Ok(())
156    /// # }
157    /// ```
158    ///
159    /// # Errors
160    ///
161    /// Returns error if:
162    /// - Page has been closed
163    /// - Communication with browser process fails
164    ///
165    /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
166    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
167    pub async fn emulate_media(
168        &self,
169        options: impl Into<Option<EmulateMediaOptions>>,
170    ) -> Result<()> {
171        let options = options.into();
172        let mut params = serde_json::json!({});
173
174        if let Some(opts) = options {
175            if let Some(media) = opts.media {
176                params["media"] = serde_json::to_value(media).map_err(|e| {
177                    crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
178                })?;
179            }
180            if let Some(color_scheme) = opts.color_scheme {
181                params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
182                    crate::error::Error::ProtocolError(format!(
183                        "Failed to serialize colorScheme: {}",
184                        e
185                    ))
186                })?;
187            }
188            if let Some(reduced_motion) = opts.reduced_motion {
189                params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
190                    crate::error::Error::ProtocolError(format!(
191                        "Failed to serialize reducedMotion: {}",
192                        e
193                    ))
194                })?;
195            }
196            if let Some(forced_colors) = opts.forced_colors {
197                params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
198                    crate::error::Error::ProtocolError(format!(
199                        "Failed to serialize forcedColors: {}",
200                        e
201                    ))
202                })?;
203            }
204        }
205
206        self.channel().send_no_result("emulateMedia", params).await
207    }
208
209    /// Adds a `<script>` tag into the page with the desired URL or content.
210    ///
211    /// # Arguments
212    ///
213    /// * `options` - Optional script tag options (content, url, or path).
214    ///   If `None`, returns an error because no source is specified.
215    ///
216    /// At least one of `content`, `url`, or `path` must be provided.
217    ///
218    /// # Example
219    ///
220    /// ```no_run
221    /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
222    /// # #[tokio::main]
223    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
224    /// # let playwright = Playwright::launch().await?;
225    /// # let browser = playwright.chromium().launch().await?;
226    /// # let context = browser.new_context().await?;
227    /// # let page = context.new_page().await?;
228    /// // With inline JavaScript
229    /// page.add_script_tag(Some(
230    ///     AddScriptTagOptions::builder()
231    ///         .content("window.myVar = 42;")
232    ///         .build()
233    /// )).await?;
234    ///
235    /// // With external URL
236    /// page.add_script_tag(Some(
237    ///     AddScriptTagOptions::builder()
238    ///         .url("https://example.com/script.js")
239    ///         .build()
240    /// )).await?;
241    /// # Ok(())
242    /// # }
243    /// ```
244    ///
245    /// # Errors
246    ///
247    /// Returns error if:
248    /// - `options` is `None` or no content/url/path is specified
249    /// - Page has been closed
250    /// - Script loading fails (e.g., invalid URL)
251    ///
252    /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
253    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
254    pub async fn add_script_tag(
255        &self,
256        options: impl Into<Option<AddScriptTagOptions>>,
257    ) -> Result<Arc<crate::protocol::ElementHandle>> {
258        let options = options.into();
259        let opts = options.ok_or_else(|| {
260            Error::InvalidArgument(
261                "At least one of content, url, or path must be specified".to_string(),
262            )
263        })?;
264        let frame = self.main_frame().await?;
265        frame.add_script_tag(opts).await
266    }
267
268    /// Returns the current viewport size of the page, or `None` if no viewport is set.
269    ///
270    /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
271    /// returns the dimensions configured at context creation time or updated via
272    /// `set_viewport_size()`.
273    ///
274    /// # Example
275    ///
276    /// ```no_run
277    /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
278    /// # #[tokio::main]
279    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
280    /// # let playwright = Playwright::launch().await?;
281    /// # let browser = playwright.chromium().launch().await?;
282    /// let context = browser.new_context_with_options(
283    ///     BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
284    /// ).await?;
285    /// let page = context.new_page().await?;
286    /// let size = page.viewport_size().expect("Viewport should be set");
287    /// assert_eq!(size.width, 1280);
288    /// assert_eq!(size.height, 720);
289    /// # Ok(())
290    /// # }
291    /// ```
292    ///
293    /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
294    pub fn viewport_size(&self) -> Option<Viewport> {
295        self.viewport.read().ok()?.clone()
296    }
297}
298
299/// Options for adding a style tag to the page
300///
301/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
302#[derive(Debug, Clone, Default)]
303#[non_exhaustive]
304pub struct AddStyleTagOptions {
305    /// Raw CSS content to inject
306    pub content: Option<String>,
307    /// URL of the `<link>` tag to add
308    pub url: Option<String>,
309    /// Path to a CSS file to inject
310    pub path: Option<String>,
311}
312
313impl AddStyleTagOptions {
314    /// Creates a new builder for AddStyleTagOptions
315    pub fn builder() -> AddStyleTagOptionsBuilder {
316        AddStyleTagOptionsBuilder::default()
317    }
318
319    /// Validates that at least one option is specified
320    pub(crate) fn validate(&self) -> Result<()> {
321        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
322            return Err(Error::InvalidArgument(
323                "At least one of content, url, or path must be specified".to_string(),
324            ));
325        }
326        Ok(())
327    }
328}
329
330/// Builder for AddStyleTagOptions
331#[derive(Debug, Clone, Default)]
332pub struct AddStyleTagOptionsBuilder {
333    content: Option<String>,
334    url: Option<String>,
335    path: Option<String>,
336}
337
338impl AddStyleTagOptionsBuilder {
339    /// Sets the CSS content to inject
340    pub fn content(mut self, content: impl Into<String>) -> Self {
341        self.content = Some(content.into());
342        self
343    }
344
345    /// Sets the URL of the stylesheet
346    pub fn url(mut self, url: impl Into<String>) -> Self {
347        self.url = Some(url.into());
348        self
349    }
350
351    /// Sets the path to a CSS file
352    pub fn path(mut self, path: impl Into<String>) -> Self {
353        self.path = Some(path.into());
354        self
355    }
356
357    /// Builds the AddStyleTagOptions
358    pub fn build(self) -> AddStyleTagOptions {
359        AddStyleTagOptions {
360            content: self.content,
361            url: self.url,
362            path: self.path,
363        }
364    }
365}
366
367// ============================================================================
368// AddScriptTagOptions
369// ============================================================================
370
371/// Options for adding a `<script>` tag to the page.
372///
373/// At least one of `content`, `url`, or `path` must be specified.
374///
375/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
376#[derive(Debug, Clone, Default)]
377#[non_exhaustive]
378pub struct AddScriptTagOptions {
379    /// Raw JavaScript content to inject
380    pub content: Option<String>,
381    /// URL of the `<script>` tag to add
382    pub url: Option<String>,
383    /// Path to a JavaScript file to inject (file contents will be read and sent as content)
384    pub path: Option<String>,
385    /// Script type attribute (e.g., `"module"`)
386    pub type_: Option<String>,
387}
388
389impl AddScriptTagOptions {
390    /// Creates a new builder for AddScriptTagOptions
391    pub fn builder() -> AddScriptTagOptionsBuilder {
392        AddScriptTagOptionsBuilder::default()
393    }
394
395    /// Validates that at least one option is specified
396    pub(crate) fn validate(&self) -> Result<()> {
397        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
398            return Err(Error::InvalidArgument(
399                "At least one of content, url, or path must be specified".to_string(),
400            ));
401        }
402        Ok(())
403    }
404}
405
406/// Builder for AddScriptTagOptions
407#[derive(Debug, Clone, Default)]
408pub struct AddScriptTagOptionsBuilder {
409    content: Option<String>,
410    url: Option<String>,
411    path: Option<String>,
412    type_: Option<String>,
413}
414
415impl AddScriptTagOptionsBuilder {
416    /// Sets the JavaScript content to inject
417    pub fn content(mut self, content: impl Into<String>) -> Self {
418        self.content = Some(content.into());
419        self
420    }
421
422    /// Sets the URL of the script to load
423    pub fn url(mut self, url: impl Into<String>) -> Self {
424        self.url = Some(url.into());
425        self
426    }
427
428    /// Sets the path to a JavaScript file to inject
429    pub fn path(mut self, path: impl Into<String>) -> Self {
430        self.path = Some(path.into());
431        self
432    }
433
434    /// Sets the script type attribute (e.g., `"module"`)
435    pub fn type_(mut self, type_: impl Into<String>) -> Self {
436        self.type_ = Some(type_.into());
437        self
438    }
439
440    /// Builds the AddScriptTagOptions
441    pub fn build(self) -> AddScriptTagOptions {
442        AddScriptTagOptions {
443            content: self.content,
444            url: self.url,
445            path: self.path,
446            type_: self.type_,
447        }
448    }
449}
450
451// ============================================================================
452// EmulateMediaOptions and related enums
453// ============================================================================
454
455/// Media type for `page.emulate_media()`.
456///
457/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
459#[serde(rename_all = "lowercase")]
460#[non_exhaustive]
461pub enum Media {
462    /// Emulate screen media type
463    Screen,
464    /// Emulate print media type
465    Print,
466    /// Reset media emulation to browser default (sends `"no-override"` to protocol)
467    #[serde(rename = "no-override")]
468    NoOverride,
469}
470
471/// Preferred color scheme for `page.emulate_media()`.
472///
473/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
475#[non_exhaustive]
476pub enum ColorScheme {
477    /// Emulate light color scheme
478    #[serde(rename = "light")]
479    Light,
480    /// Emulate dark color scheme
481    #[serde(rename = "dark")]
482    Dark,
483    /// Emulate no preference for color scheme
484    #[serde(rename = "no-preference")]
485    NoPreference,
486    /// Reset color scheme to browser default
487    #[serde(rename = "no-override")]
488    NoOverride,
489}
490
491/// Reduced motion preference for `page.emulate_media()`.
492///
493/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
495#[non_exhaustive]
496pub enum ReducedMotion {
497    /// Emulate reduced motion preference
498    #[serde(rename = "reduce")]
499    Reduce,
500    /// Emulate no preference for reduced motion
501    #[serde(rename = "no-preference")]
502    NoPreference,
503    /// Reset reduced motion to browser default
504    #[serde(rename = "no-override")]
505    NoOverride,
506}
507
508/// Forced colors preference for `page.emulate_media()`.
509///
510/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
512#[non_exhaustive]
513pub enum ForcedColors {
514    /// Emulate active forced colors
515    #[serde(rename = "active")]
516    Active,
517    /// Emulate no forced colors
518    #[serde(rename = "none")]
519    None_,
520    /// Reset forced colors to browser default
521    #[serde(rename = "no-override")]
522    NoOverride,
523}
524
525/// Options for `page.emulate_media()`.
526///
527/// All fields are optional. Fields that are `None` are omitted from the protocol
528/// message (meaning they are not changed). To reset a field to browser default,
529/// use the `NoOverride` variant.
530///
531/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
532#[derive(Debug, Clone, Default)]
533#[non_exhaustive]
534pub struct EmulateMediaOptions {
535    /// Media type to emulate (screen, print, or no-override)
536    pub media: Option<Media>,
537    /// Color scheme preference to emulate
538    pub color_scheme: Option<ColorScheme>,
539    /// Reduced motion preference to emulate
540    pub reduced_motion: Option<ReducedMotion>,
541    /// Forced colors preference to emulate
542    pub forced_colors: Option<ForcedColors>,
543}
544
545impl EmulateMediaOptions {
546    /// Creates a new builder for EmulateMediaOptions
547    pub fn builder() -> EmulateMediaOptionsBuilder {
548        EmulateMediaOptionsBuilder::default()
549    }
550}
551
552/// Builder for EmulateMediaOptions
553#[derive(Debug, Clone, Default)]
554pub struct EmulateMediaOptionsBuilder {
555    media: Option<Media>,
556    color_scheme: Option<ColorScheme>,
557    reduced_motion: Option<ReducedMotion>,
558    forced_colors: Option<ForcedColors>,
559}
560
561impl EmulateMediaOptionsBuilder {
562    /// Sets the media type to emulate
563    pub fn media(mut self, media: Media) -> Self {
564        self.media = Some(media);
565        self
566    }
567
568    /// Sets the color scheme preference
569    pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
570        self.color_scheme = Some(color_scheme);
571        self
572    }
573
574    /// Sets the reduced motion preference
575    pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
576        self.reduced_motion = Some(reduced_motion);
577        self
578    }
579
580    /// Sets the forced colors preference
581    pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
582        self.forced_colors = Some(forced_colors);
583        self
584    }
585
586    /// Builds the EmulateMediaOptions
587    pub fn build(self) -> EmulateMediaOptions {
588        EmulateMediaOptions {
589            media: self.media,
590            color_scheme: self.color_scheme,
591            reduced_motion: self.reduced_motion,
592            forced_colors: self.forced_colors,
593        }
594    }
595}