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}