playwright_rs/protocol/
browser_context.rs

1// BrowserContext protocol object
2//
3// Represents an isolated browser context (session) within a browser instance.
4// Multiple contexts can exist in a single browser, each with its own cookies,
5// cache, and local storage.
6
7use crate::error::Result;
8use crate::protocol::Page;
9use crate::server::channel::Channel;
10use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::any::Any;
14use std::collections::HashMap;
15use std::sync::Arc;
16
17/// BrowserContext represents an isolated browser session.
18///
19/// Contexts are isolated environments within a browser instance. Each context
20/// has its own cookies, cache, and local storage, enabling independent sessions
21/// without interference.
22///
23/// # Example
24///
25/// ```ignore
26/// use playwright_rs::protocol::Playwright;
27///
28/// #[tokio::main]
29/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
30///     let playwright = Playwright::launch().await?;
31///     let browser = playwright.chromium().launch().await?;
32///
33///     // Create isolated contexts
34///     let context1 = browser.new_context().await?;
35///     let context2 = browser.new_context().await?;
36///
37///     // Create pages in each context
38///     let page1 = context1.new_page().await?;
39///     let page2 = context2.new_page().await?;
40///
41///     // Cleanup
42///     context1.close().await?;
43///     context2.close().await?;
44///     browser.close().await?;
45///     Ok(())
46/// }
47/// ```
48///
49/// See: <https://playwright.dev/docs/api/class-browsercontext>
50#[derive(Clone)]
51pub struct BrowserContext {
52    base: ChannelOwnerImpl,
53}
54
55impl BrowserContext {
56    /// Creates a new BrowserContext from protocol initialization
57    ///
58    /// This is called by the object factory when the server sends a `__create__` message
59    /// for a BrowserContext object.
60    ///
61    /// # Arguments
62    ///
63    /// * `parent` - The parent Browser object
64    /// * `type_name` - The protocol type name ("BrowserContext")
65    /// * `guid` - The unique identifier for this context
66    /// * `initializer` - The initialization data from the server
67    ///
68    /// # Errors
69    ///
70    /// Returns error if initializer is malformed
71    pub fn new(
72        parent: Arc<dyn ChannelOwner>,
73        type_name: String,
74        guid: Arc<str>,
75        initializer: Value,
76    ) -> Result<Self> {
77        let base = ChannelOwnerImpl::new(
78            ParentOrConnection::Parent(parent),
79            type_name,
80            guid,
81            initializer,
82        );
83
84        let context = Self { base };
85
86        // Enable dialog event subscription
87        // Dialog events need to be explicitly subscribed to via updateSubscription command
88        let channel = context.channel().clone();
89        tokio::spawn(async move {
90            let _ = channel
91                .send_no_result(
92                    "updateSubscription",
93                    serde_json::json!({
94                        "event": "dialog",
95                        "enabled": true
96                    }),
97                )
98                .await;
99        });
100
101        Ok(context)
102    }
103
104    /// Returns the channel for sending protocol messages
105    ///
106    /// Used internally for sending RPC calls to the context.
107    fn channel(&self) -> &Channel {
108        self.base.channel()
109    }
110
111    /// Adds a script which would be evaluated in one of the following scenarios:
112    ///
113    /// - Whenever a page is created in the browser context or is navigated.
114    /// - Whenever a child frame is attached or navigated in any page in the browser context.
115    ///
116    /// The script is evaluated after the document was created but before any of its scripts
117    /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
118    ///
119    /// # Arguments
120    ///
121    /// * `script` - Script to be evaluated in all pages in the browser context.
122    ///
123    /// # Errors
124    ///
125    /// Returns error if:
126    /// - Context has been closed
127    /// - Communication with browser process fails
128    ///
129    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
130    pub async fn add_init_script(&self, script: &str) -> Result<()> {
131        self.channel()
132            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
133            .await
134    }
135
136    /// Creates a new page in this browser context.
137    ///
138    /// Pages are isolated tabs/windows within a context. Each page starts
139    /// at "about:blank" and can be navigated independently.
140    ///
141    /// # Errors
142    ///
143    /// Returns error if:
144    /// - Context has been closed
145    /// - Communication with browser process fails
146    ///
147    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
148    pub async fn new_page(&self) -> Result<Page> {
149        // Response contains the GUID of the created Page
150        #[derive(Deserialize)]
151        struct NewPageResponse {
152            page: GuidRef,
153        }
154
155        #[derive(Deserialize)]
156        struct GuidRef {
157            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
158            guid: Arc<str>,
159        }
160
161        // Send newPage RPC to server
162        let response: NewPageResponse = self
163            .channel()
164            .send("newPage", serde_json::json!({}))
165            .await?;
166
167        // Retrieve the Page object from the connection registry
168        let page_arc = self.connection().get_object(&response.page.guid).await?;
169
170        // Downcast to Page
171        let page = page_arc.as_any().downcast_ref::<Page>().ok_or_else(|| {
172            crate::error::Error::ProtocolError(format!(
173                "Expected Page object, got {}",
174                page_arc.type_name()
175            ))
176        })?;
177
178        Ok(page.clone())
179    }
180
181    /// Closes the browser context and all its pages.
182    ///
183    /// This is a graceful operation that sends a close command to the context
184    /// and waits for it to shut down properly.
185    ///
186    /// # Errors
187    ///
188    /// Returns error if:
189    /// - Context has already been closed
190    /// - Communication with browser process fails
191    ///
192    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
193    pub async fn close(&self) -> Result<()> {
194        // Send close RPC to server
195        self.channel()
196            .send_no_result("close", serde_json::json!({}))
197            .await
198    }
199
200    /// Pauses the browser context.
201    ///
202    /// This pauses the execution of all pages in the context.
203    pub async fn pause(&self) -> Result<()> {
204        self.channel()
205            .send_no_result("pause", serde_json::Value::Null)
206            .await
207    }
208}
209
210impl ChannelOwner for BrowserContext {
211    fn guid(&self) -> &str {
212        self.base.guid()
213    }
214
215    fn type_name(&self) -> &str {
216        self.base.type_name()
217    }
218
219    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
220        self.base.parent()
221    }
222
223    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
224        self.base.connection()
225    }
226
227    fn initializer(&self) -> &Value {
228        self.base.initializer()
229    }
230
231    fn channel(&self) -> &Channel {
232        self.base.channel()
233    }
234
235    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
236        self.base.dispose(reason)
237    }
238
239    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
240        self.base.adopt(child)
241    }
242
243    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
244        self.base.add_child(guid, child)
245    }
246
247    fn remove_child(&self, guid: &str) {
248        self.base.remove_child(guid)
249    }
250
251    fn on_event(&self, method: &str, params: Value) {
252        match method {
253            "dialog" => {
254                // Dialog events come to BrowserContext, need to forward to the associated Page
255                // Event format: {dialog: {guid: "..."}}
256                // The Dialog protocol object has the Page as its parent
257                if let Some(dialog_guid) = params
258                    .get("dialog")
259                    .and_then(|v| v.get("guid"))
260                    .and_then(|v| v.as_str())
261                {
262                    let connection = self.connection();
263                    let dialog_guid_owned = dialog_guid.to_string();
264
265                    tokio::spawn(async move {
266                        // Get the Dialog object
267                        let dialog_arc = match connection.get_object(&dialog_guid_owned).await {
268                            Ok(obj) => obj,
269                            Err(_) => return,
270                        };
271
272                        // Downcast to Dialog
273                        let dialog = match dialog_arc
274                            .as_any()
275                            .downcast_ref::<crate::protocol::Dialog>()
276                        {
277                            Some(d) => d.clone(),
278                            None => return,
279                        };
280
281                        // Get the Page from the Dialog's parent
282                        let page_arc = match dialog_arc.parent() {
283                            Some(parent) => parent,
284                            None => return,
285                        };
286
287                        // Downcast to Page
288                        let page = match page_arc.as_any().downcast_ref::<Page>() {
289                            Some(p) => p.clone(),
290                            None => return,
291                        };
292
293                        // Forward to Page's dialog handlers
294                        page.trigger_dialog_event(dialog).await;
295                    });
296                }
297            }
298            _ => {
299                // Other events will be handled in future phases
300            }
301        }
302    }
303
304    fn was_collected(&self) -> bool {
305        self.base.was_collected()
306    }
307
308    fn as_any(&self) -> &dyn Any {
309        self
310    }
311}
312
313impl std::fmt::Debug for BrowserContext {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        f.debug_struct("BrowserContext")
316            .field("guid", &self.guid())
317            .finish()
318    }
319}
320
321/// Viewport dimensions for browser context.
322///
323/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct Viewport {
326    /// Page width in pixels
327    pub width: u32,
328    /// Page height in pixels
329    pub height: u32,
330}
331
332/// Geolocation coordinates.
333///
334/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct Geolocation {
337    /// Latitude between -90 and 90
338    pub latitude: f64,
339    /// Longitude between -180 and 180
340    pub longitude: f64,
341    /// Optional accuracy in meters (default: 0)
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub accuracy: Option<f64>,
344}
345
346/// Cookie information for storage state.
347///
348/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct Cookie {
352    /// Cookie name
353    pub name: String,
354    /// Cookie value
355    pub value: String,
356    /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
357    pub domain: String,
358    /// Cookie path
359    pub path: String,
360    /// Unix timestamp in seconds; -1 for session cookies
361    pub expires: f64,
362    /// HTTP-only flag
363    pub http_only: bool,
364    /// Secure flag
365    pub secure: bool,
366    /// SameSite attribute ("Strict", "Lax", "None")
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub same_site: Option<String>,
369}
370
371/// Local storage item for storage state.
372///
373/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct LocalStorageItem {
376    /// Storage key
377    pub name: String,
378    /// Storage value
379    pub value: String,
380}
381
382/// Origin with local storage items for storage state.
383///
384/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
385#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct Origin {
388    /// Origin URL (e.g., "https://example.com")
389    pub origin: String,
390    /// Local storage items for this origin
391    pub local_storage: Vec<LocalStorageItem>,
392}
393
394/// Storage state containing cookies and local storage.
395///
396/// Used to populate a browser context with saved authentication state,
397/// enabling session persistence across context instances.
398///
399/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct StorageState {
402    /// List of cookies
403    pub cookies: Vec<Cookie>,
404    /// List of origins with local storage
405    pub origins: Vec<Origin>,
406}
407
408/// Options for creating a new browser context.
409///
410/// Allows customizing viewport, user agent, locale, timezone, geolocation,
411/// permissions, and other browser context settings.
412///
413/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
414#[derive(Debug, Clone, Default, Serialize)]
415#[serde(rename_all = "camelCase")]
416pub struct BrowserContextOptions {
417    /// Sets consistent viewport for all pages in the context.
418    /// Set to null via `no_viewport(true)` to disable viewport emulation.
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub viewport: Option<Viewport>,
421
422    /// Disables viewport emulation when set to true.
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub no_viewport: Option<bool>,
425
426    /// Custom user agent string
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub user_agent: Option<String>,
429
430    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub locale: Option<String>,
433
434    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub timezone_id: Option<String>,
437
438    /// Geolocation coordinates
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub geolocation: Option<Geolocation>,
441
442    /// List of permissions to grant (e.g., "geolocation", "notifications")
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub permissions: Option<Vec<String>>,
445
446    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub color_scheme: Option<String>,
449
450    /// Whether the viewport supports touch events
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub has_touch: Option<bool>,
453
454    /// Whether the meta viewport tag is respected
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub is_mobile: Option<bool>,
457
458    /// Whether JavaScript is enabled in the context
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub javascript_enabled: Option<bool>,
461
462    /// Emulates network being offline
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub offline: Option<bool>,
465
466    /// Whether to automatically download attachments
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub accept_downloads: Option<bool>,
469
470    /// Whether to bypass Content-Security-Policy
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub bypass_csp: Option<bool>,
473
474    /// Whether to ignore HTTPS errors
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub ignore_https_errors: Option<bool>,
477
478    /// Device scale factor (default: 1)
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub device_scale_factor: Option<f64>,
481
482    /// Extra HTTP headers to send with every request
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub extra_http_headers: Option<HashMap<String, String>>,
485
486    /// Base URL for relative navigation
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub base_url: Option<String>,
489
490    /// Storage state to populate the context (cookies, localStorage, sessionStorage).
491    /// Can be an inline StorageState object or a file path string.
492    /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub storage_state: Option<StorageState>,
495
496    /// Storage state file path (alternative to inline storage_state).
497    /// This is handled by the builder and converted to storage_state during serialization.
498    #[serde(skip_serializing_if = "Option::is_none")]
499    pub storage_state_path: Option<String>,
500}
501
502impl BrowserContextOptions {
503    /// Creates a new builder for BrowserContextOptions
504    pub fn builder() -> BrowserContextOptionsBuilder {
505        BrowserContextOptionsBuilder::default()
506    }
507}
508
509/// Builder for BrowserContextOptions
510#[derive(Debug, Clone, Default)]
511pub struct BrowserContextOptionsBuilder {
512    viewport: Option<Viewport>,
513    no_viewport: Option<bool>,
514    user_agent: Option<String>,
515    locale: Option<String>,
516    timezone_id: Option<String>,
517    geolocation: Option<Geolocation>,
518    permissions: Option<Vec<String>>,
519    color_scheme: Option<String>,
520    has_touch: Option<bool>,
521    is_mobile: Option<bool>,
522    javascript_enabled: Option<bool>,
523    offline: Option<bool>,
524    accept_downloads: Option<bool>,
525    bypass_csp: Option<bool>,
526    ignore_https_errors: Option<bool>,
527    device_scale_factor: Option<f64>,
528    extra_http_headers: Option<HashMap<String, String>>,
529    base_url: Option<String>,
530    storage_state: Option<StorageState>,
531    storage_state_path: Option<String>,
532}
533
534impl BrowserContextOptionsBuilder {
535    /// Sets the viewport dimensions
536    pub fn viewport(mut self, viewport: Viewport) -> Self {
537        self.viewport = Some(viewport);
538        self.no_viewport = None; // Clear no_viewport if setting viewport
539        self
540    }
541
542    /// Disables viewport emulation
543    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
544        self.no_viewport = Some(no_viewport);
545        if no_viewport {
546            self.viewport = None; // Clear viewport if setting no_viewport
547        }
548        self
549    }
550
551    /// Sets the user agent string
552    pub fn user_agent(mut self, user_agent: String) -> Self {
553        self.user_agent = Some(user_agent);
554        self
555    }
556
557    /// Sets the locale
558    pub fn locale(mut self, locale: String) -> Self {
559        self.locale = Some(locale);
560        self
561    }
562
563    /// Sets the timezone identifier
564    pub fn timezone_id(mut self, timezone_id: String) -> Self {
565        self.timezone_id = Some(timezone_id);
566        self
567    }
568
569    /// Sets the geolocation
570    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
571        self.geolocation = Some(geolocation);
572        self
573    }
574
575    /// Sets the permissions to grant
576    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
577        self.permissions = Some(permissions);
578        self
579    }
580
581    /// Sets the color scheme preference
582    pub fn color_scheme(mut self, color_scheme: String) -> Self {
583        self.color_scheme = Some(color_scheme);
584        self
585    }
586
587    /// Sets whether the viewport supports touch events
588    pub fn has_touch(mut self, has_touch: bool) -> Self {
589        self.has_touch = Some(has_touch);
590        self
591    }
592
593    /// Sets whether this is a mobile viewport
594    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
595        self.is_mobile = Some(is_mobile);
596        self
597    }
598
599    /// Sets whether JavaScript is enabled
600    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
601        self.javascript_enabled = Some(javascript_enabled);
602        self
603    }
604
605    /// Sets whether to emulate offline network
606    pub fn offline(mut self, offline: bool) -> Self {
607        self.offline = Some(offline);
608        self
609    }
610
611    /// Sets whether to automatically download attachments
612    pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
613        self.accept_downloads = Some(accept_downloads);
614        self
615    }
616
617    /// Sets whether to bypass Content-Security-Policy
618    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
619        self.bypass_csp = Some(bypass_csp);
620        self
621    }
622
623    /// Sets whether to ignore HTTPS errors
624    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
625        self.ignore_https_errors = Some(ignore_https_errors);
626        self
627    }
628
629    /// Sets the device scale factor
630    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
631        self.device_scale_factor = Some(device_scale_factor);
632        self
633    }
634
635    /// Sets extra HTTP headers
636    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
637        self.extra_http_headers = Some(extra_http_headers);
638        self
639    }
640
641    /// Sets the base URL for relative navigation
642    pub fn base_url(mut self, base_url: String) -> Self {
643        self.base_url = Some(base_url);
644        self
645    }
646
647    /// Sets the storage state inline (cookies, localStorage).
648    ///
649    /// Populates the browser context with the provided storage state, including
650    /// cookies and local storage. This is useful for initializing a context with
651    /// a saved authentication state.
652    ///
653    /// Mutually exclusive with `storage_state_path()`.
654    ///
655    /// # Example
656    ///
657    /// ```rust
658    /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
659    ///
660    /// let storage_state = StorageState {
661    ///     cookies: vec![Cookie {
662    ///         name: "session_id".to_string(),
663    ///         value: "abc123".to_string(),
664    ///         domain: ".example.com".to_string(),
665    ///         path: "/".to_string(),
666    ///         expires: -1.0,
667    ///         http_only: true,
668    ///         secure: true,
669    ///         same_site: Some("Lax".to_string()),
670    ///     }],
671    ///     origins: vec![Origin {
672    ///         origin: "https://example.com".to_string(),
673    ///         local_storage: vec![LocalStorageItem {
674    ///             name: "user_prefs".to_string(),
675    ///             value: "{\"theme\":\"dark\"}".to_string(),
676    ///         }],
677    ///     }],
678    /// };
679    ///
680    /// let options = BrowserContextOptions::builder()
681    ///     .storage_state(storage_state)
682    ///     .build();
683    /// ```
684    ///
685    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
686    pub fn storage_state(mut self, storage_state: StorageState) -> Self {
687        self.storage_state = Some(storage_state);
688        self.storage_state_path = None; // Clear path if setting inline
689        self
690    }
691
692    /// Sets the storage state from a file path.
693    ///
694    /// The file should contain a JSON representation of StorageState with cookies
695    /// and origins. This is useful for loading authentication state saved from a
696    /// previous session.
697    ///
698    /// Mutually exclusive with `storage_state()`.
699    ///
700    /// # Example
701    ///
702    /// ```rust
703    /// use playwright_rs::protocol::BrowserContextOptions;
704    ///
705    /// let options = BrowserContextOptions::builder()
706    ///     .storage_state_path("auth.json".to_string())
707    ///     .build();
708    /// ```
709    ///
710    /// The file should have this format:
711    /// ```json
712    /// {
713    ///   "cookies": [{
714    ///     "name": "session_id",
715    ///     "value": "abc123",
716    ///     "domain": ".example.com",
717    ///     "path": "/",
718    ///     "expires": -1,
719    ///     "httpOnly": true,
720    ///     "secure": true,
721    ///     "sameSite": "Lax"
722    ///   }],
723    ///   "origins": [{
724    ///     "origin": "https://example.com",
725    ///     "localStorage": [{
726    ///       "name": "user_prefs",
727    ///       "value": "{\"theme\":\"dark\"}"
728    ///     }]
729    ///   }]
730    /// }
731    /// ```
732    ///
733    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
734    pub fn storage_state_path(mut self, path: String) -> Self {
735        self.storage_state_path = Some(path);
736        self.storage_state = None; // Clear inline if setting path
737        self
738    }
739
740    /// Builds the BrowserContextOptions
741    pub fn build(self) -> BrowserContextOptions {
742        BrowserContextOptions {
743            viewport: self.viewport,
744            no_viewport: self.no_viewport,
745            user_agent: self.user_agent,
746            locale: self.locale,
747            timezone_id: self.timezone_id,
748            geolocation: self.geolocation,
749            permissions: self.permissions,
750            color_scheme: self.color_scheme,
751            has_touch: self.has_touch,
752            is_mobile: self.is_mobile,
753            javascript_enabled: self.javascript_enabled,
754            offline: self.offline,
755            accept_downloads: self.accept_downloads,
756            bypass_csp: self.bypass_csp,
757            ignore_https_errors: self.ignore_https_errors,
758            device_scale_factor: self.device_scale_factor,
759            extra_http_headers: self.extra_http_headers,
760            base_url: self.base_url,
761            storage_state: self.storage_state,
762            storage_state_path: self.storage_state_path,
763        }
764    }
765}