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
201impl ChannelOwner for BrowserContext {
202    fn guid(&self) -> &str {
203        self.base.guid()
204    }
205
206    fn type_name(&self) -> &str {
207        self.base.type_name()
208    }
209
210    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
211        self.base.parent()
212    }
213
214    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
215        self.base.connection()
216    }
217
218    fn initializer(&self) -> &Value {
219        self.base.initializer()
220    }
221
222    fn channel(&self) -> &Channel {
223        self.base.channel()
224    }
225
226    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
227        self.base.dispose(reason)
228    }
229
230    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
231        self.base.adopt(child)
232    }
233
234    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
235        self.base.add_child(guid, child)
236    }
237
238    fn remove_child(&self, guid: &str) {
239        self.base.remove_child(guid)
240    }
241
242    fn on_event(&self, method: &str, params: Value) {
243        match method {
244            "dialog" => {
245                // Dialog events come to BrowserContext, need to forward to the associated Page
246                // Event format: {dialog: {guid: "..."}}
247                // The Dialog protocol object has the Page as its parent
248                if let Some(dialog_guid) = params
249                    .get("dialog")
250                    .and_then(|v| v.get("guid"))
251                    .and_then(|v| v.as_str())
252                {
253                    let connection = self.connection();
254                    let dialog_guid_owned = dialog_guid.to_string();
255
256                    tokio::spawn(async move {
257                        // Get the Dialog object
258                        let dialog_arc = match connection.get_object(&dialog_guid_owned).await {
259                            Ok(obj) => obj,
260                            Err(_) => return,
261                        };
262
263                        // Downcast to Dialog
264                        let dialog = match dialog_arc
265                            .as_any()
266                            .downcast_ref::<crate::protocol::Dialog>()
267                        {
268                            Some(d) => d.clone(),
269                            None => return,
270                        };
271
272                        // Get the Page from the Dialog's parent
273                        let page_arc = match dialog_arc.parent() {
274                            Some(parent) => parent,
275                            None => return,
276                        };
277
278                        // Downcast to Page
279                        let page = match page_arc.as_any().downcast_ref::<Page>() {
280                            Some(p) => p.clone(),
281                            None => return,
282                        };
283
284                        // Forward to Page's dialog handlers
285                        page.trigger_dialog_event(dialog).await;
286                    });
287                }
288            }
289            _ => {
290                // Other events will be handled in future phases
291            }
292        }
293    }
294
295    fn was_collected(&self) -> bool {
296        self.base.was_collected()
297    }
298
299    fn as_any(&self) -> &dyn Any {
300        self
301    }
302}
303
304impl std::fmt::Debug for BrowserContext {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.debug_struct("BrowserContext")
307            .field("guid", &self.guid())
308            .finish()
309    }
310}
311
312/// Viewport dimensions for browser context.
313///
314/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct Viewport {
317    /// Page width in pixels
318    pub width: u32,
319    /// Page height in pixels
320    pub height: u32,
321}
322
323/// Geolocation coordinates.
324///
325/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct Geolocation {
328    /// Latitude between -90 and 90
329    pub latitude: f64,
330    /// Longitude between -180 and 180
331    pub longitude: f64,
332    /// Optional accuracy in meters (default: 0)
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub accuracy: Option<f64>,
335}
336
337/// Options for creating a new browser context.
338///
339/// Allows customizing viewport, user agent, locale, timezone, geolocation,
340/// permissions, and other browser context settings.
341///
342/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
343#[derive(Debug, Clone, Default, Serialize)]
344#[serde(rename_all = "camelCase")]
345pub struct BrowserContextOptions {
346    /// Sets consistent viewport for all pages in the context.
347    /// Set to null via `no_viewport(true)` to disable viewport emulation.
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub viewport: Option<Viewport>,
350
351    /// Disables viewport emulation when set to true.
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub no_viewport: Option<bool>,
354
355    /// Custom user agent string
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub user_agent: Option<String>,
358
359    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub locale: Option<String>,
362
363    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub timezone_id: Option<String>,
366
367    /// Geolocation coordinates
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub geolocation: Option<Geolocation>,
370
371    /// List of permissions to grant (e.g., "geolocation", "notifications")
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub permissions: Option<Vec<String>>,
374
375    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub color_scheme: Option<String>,
378
379    /// Whether the viewport supports touch events
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub has_touch: Option<bool>,
382
383    /// Whether the meta viewport tag is respected
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub is_mobile: Option<bool>,
386
387    /// Whether JavaScript is enabled in the context
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub javascript_enabled: Option<bool>,
390
391    /// Emulates network being offline
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub offline: Option<bool>,
394
395    /// Whether to automatically download attachments
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub accept_downloads: Option<bool>,
398
399    /// Whether to bypass Content-Security-Policy
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub bypass_csp: Option<bool>,
402
403    /// Whether to ignore HTTPS errors
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub ignore_https_errors: Option<bool>,
406
407    /// Device scale factor (default: 1)
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub device_scale_factor: Option<f64>,
410
411    /// Extra HTTP headers to send with every request
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub extra_http_headers: Option<HashMap<String, String>>,
414
415    /// Base URL for relative navigation
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub base_url: Option<String>,
418}
419
420impl BrowserContextOptions {
421    /// Creates a new builder for BrowserContextOptions
422    pub fn builder() -> BrowserContextOptionsBuilder {
423        BrowserContextOptionsBuilder::default()
424    }
425}
426
427/// Builder for BrowserContextOptions
428#[derive(Debug, Clone, Default)]
429pub struct BrowserContextOptionsBuilder {
430    viewport: Option<Viewport>,
431    no_viewport: Option<bool>,
432    user_agent: Option<String>,
433    locale: Option<String>,
434    timezone_id: Option<String>,
435    geolocation: Option<Geolocation>,
436    permissions: Option<Vec<String>>,
437    color_scheme: Option<String>,
438    has_touch: Option<bool>,
439    is_mobile: Option<bool>,
440    javascript_enabled: Option<bool>,
441    offline: Option<bool>,
442    accept_downloads: Option<bool>,
443    bypass_csp: Option<bool>,
444    ignore_https_errors: Option<bool>,
445    device_scale_factor: Option<f64>,
446    extra_http_headers: Option<HashMap<String, String>>,
447    base_url: Option<String>,
448}
449
450impl BrowserContextOptionsBuilder {
451    /// Sets the viewport dimensions
452    pub fn viewport(mut self, viewport: Viewport) -> Self {
453        self.viewport = Some(viewport);
454        self.no_viewport = None; // Clear no_viewport if setting viewport
455        self
456    }
457
458    /// Disables viewport emulation
459    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
460        self.no_viewport = Some(no_viewport);
461        if no_viewport {
462            self.viewport = None; // Clear viewport if setting no_viewport
463        }
464        self
465    }
466
467    /// Sets the user agent string
468    pub fn user_agent(mut self, user_agent: String) -> Self {
469        self.user_agent = Some(user_agent);
470        self
471    }
472
473    /// Sets the locale
474    pub fn locale(mut self, locale: String) -> Self {
475        self.locale = Some(locale);
476        self
477    }
478
479    /// Sets the timezone identifier
480    pub fn timezone_id(mut self, timezone_id: String) -> Self {
481        self.timezone_id = Some(timezone_id);
482        self
483    }
484
485    /// Sets the geolocation
486    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
487        self.geolocation = Some(geolocation);
488        self
489    }
490
491    /// Sets the permissions to grant
492    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
493        self.permissions = Some(permissions);
494        self
495    }
496
497    /// Sets the color scheme preference
498    pub fn color_scheme(mut self, color_scheme: String) -> Self {
499        self.color_scheme = Some(color_scheme);
500        self
501    }
502
503    /// Sets whether the viewport supports touch events
504    pub fn has_touch(mut self, has_touch: bool) -> Self {
505        self.has_touch = Some(has_touch);
506        self
507    }
508
509    /// Sets whether this is a mobile viewport
510    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
511        self.is_mobile = Some(is_mobile);
512        self
513    }
514
515    /// Sets whether JavaScript is enabled
516    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
517        self.javascript_enabled = Some(javascript_enabled);
518        self
519    }
520
521    /// Sets whether to emulate offline network
522    pub fn offline(mut self, offline: bool) -> Self {
523        self.offline = Some(offline);
524        self
525    }
526
527    /// Sets whether to automatically download attachments
528    pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
529        self.accept_downloads = Some(accept_downloads);
530        self
531    }
532
533    /// Sets whether to bypass Content-Security-Policy
534    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
535        self.bypass_csp = Some(bypass_csp);
536        self
537    }
538
539    /// Sets whether to ignore HTTPS errors
540    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
541        self.ignore_https_errors = Some(ignore_https_errors);
542        self
543    }
544
545    /// Sets the device scale factor
546    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
547        self.device_scale_factor = Some(device_scale_factor);
548        self
549    }
550
551    /// Sets extra HTTP headers
552    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
553        self.extra_http_headers = Some(extra_http_headers);
554        self
555    }
556
557    /// Sets the base URL for relative navigation
558    pub fn base_url(mut self, base_url: String) -> Self {
559        self.base_url = Some(base_url);
560        self
561    }
562
563    /// Builds the BrowserContextOptions
564    pub fn build(self) -> BrowserContextOptions {
565        BrowserContextOptions {
566            viewport: self.viewport,
567            no_viewport: self.no_viewport,
568            user_agent: self.user_agent,
569            locale: self.locale,
570            timezone_id: self.timezone_id,
571            geolocation: self.geolocation,
572            permissions: self.permissions,
573            color_scheme: self.color_scheme,
574            has_touch: self.has_touch,
575            is_mobile: self.is_mobile,
576            javascript_enabled: self.javascript_enabled,
577            offline: self.offline,
578            accept_downloads: self.accept_downloads,
579            bypass_csp: self.bypass_csp,
580            ignore_https_errors: self.ignore_https_errors,
581            device_scale_factor: self.device_scale_factor,
582            extra_http_headers: self.extra_http_headers,
583            base_url: self.base_url,
584        }
585    }
586}