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    /// Creates a new page in this browser context.
112    ///
113    /// Pages are isolated tabs/windows within a context. Each page starts
114    /// at "about:blank" and can be navigated independently.
115    ///
116    /// # Errors
117    ///
118    /// Returns error if:
119    /// - Context has been closed
120    /// - Communication with browser process fails
121    ///
122    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
123    pub async fn new_page(&self) -> Result<Page> {
124        // Response contains the GUID of the created Page
125        #[derive(Deserialize)]
126        struct NewPageResponse {
127            page: GuidRef,
128        }
129
130        #[derive(Deserialize)]
131        struct GuidRef {
132            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
133            guid: Arc<str>,
134        }
135
136        // Send newPage RPC to server
137        let response: NewPageResponse = self
138            .channel()
139            .send("newPage", serde_json::json!({}))
140            .await?;
141
142        // Retrieve the Page object from the connection registry
143        let page_arc = self.connection().get_object(&response.page.guid).await?;
144
145        // Downcast to Page
146        let page = page_arc.as_any().downcast_ref::<Page>().ok_or_else(|| {
147            crate::error::Error::ProtocolError(format!(
148                "Expected Page object, got {}",
149                page_arc.type_name()
150            ))
151        })?;
152
153        Ok(page.clone())
154    }
155
156    /// Closes the browser context and all its pages.
157    ///
158    /// This is a graceful operation that sends a close command to the context
159    /// and waits for it to shut down properly.
160    ///
161    /// # Errors
162    ///
163    /// Returns error if:
164    /// - Context has already been closed
165    /// - Communication with browser process fails
166    ///
167    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
168    pub async fn close(&self) -> Result<()> {
169        // Send close RPC to server
170        self.channel()
171            .send_no_result("close", serde_json::json!({}))
172            .await
173    }
174}
175
176impl ChannelOwner for BrowserContext {
177    fn guid(&self) -> &str {
178        self.base.guid()
179    }
180
181    fn type_name(&self) -> &str {
182        self.base.type_name()
183    }
184
185    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
186        self.base.parent()
187    }
188
189    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
190        self.base.connection()
191    }
192
193    fn initializer(&self) -> &Value {
194        self.base.initializer()
195    }
196
197    fn channel(&self) -> &Channel {
198        self.base.channel()
199    }
200
201    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
202        self.base.dispose(reason)
203    }
204
205    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
206        self.base.adopt(child)
207    }
208
209    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
210        self.base.add_child(guid, child)
211    }
212
213    fn remove_child(&self, guid: &str) {
214        self.base.remove_child(guid)
215    }
216
217    fn on_event(&self, method: &str, params: Value) {
218        match method {
219            "dialog" => {
220                // Dialog events come to BrowserContext, need to forward to the associated Page
221                // Event format: {dialog: {guid: "..."}}
222                // The Dialog protocol object has the Page as its parent
223                if let Some(dialog_guid) = params
224                    .get("dialog")
225                    .and_then(|v| v.get("guid"))
226                    .and_then(|v| v.as_str())
227                {
228                    let connection = self.connection();
229                    let dialog_guid_owned = dialog_guid.to_string();
230
231                    tokio::spawn(async move {
232                        // Get the Dialog object
233                        let dialog_arc = match connection.get_object(&dialog_guid_owned).await {
234                            Ok(obj) => obj,
235                            Err(_) => return,
236                        };
237
238                        // Downcast to Dialog
239                        let dialog = match dialog_arc
240                            .as_any()
241                            .downcast_ref::<crate::protocol::Dialog>()
242                        {
243                            Some(d) => d.clone(),
244                            None => return,
245                        };
246
247                        // Get the Page from the Dialog's parent
248                        let page_arc = match dialog_arc.parent() {
249                            Some(parent) => parent,
250                            None => return,
251                        };
252
253                        // Downcast to Page
254                        let page = match page_arc.as_any().downcast_ref::<Page>() {
255                            Some(p) => p.clone(),
256                            None => return,
257                        };
258
259                        // Forward to Page's dialog handlers
260                        page.trigger_dialog_event(dialog).await;
261                    });
262                }
263            }
264            _ => {
265                // Other events will be handled in future phases
266            }
267        }
268    }
269
270    fn was_collected(&self) -> bool {
271        self.base.was_collected()
272    }
273
274    fn as_any(&self) -> &dyn Any {
275        self
276    }
277}
278
279impl std::fmt::Debug for BrowserContext {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        f.debug_struct("BrowserContext")
282            .field("guid", &self.guid())
283            .finish()
284    }
285}
286
287/// Viewport dimensions for browser context.
288///
289/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct Viewport {
292    /// Page width in pixels
293    pub width: u32,
294    /// Page height in pixels
295    pub height: u32,
296}
297
298/// Geolocation coordinates.
299///
300/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct Geolocation {
303    /// Latitude between -90 and 90
304    pub latitude: f64,
305    /// Longitude between -180 and 180
306    pub longitude: f64,
307    /// Optional accuracy in meters (default: 0)
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub accuracy: Option<f64>,
310}
311
312/// Options for creating a new browser context.
313///
314/// Allows customizing viewport, user agent, locale, timezone, geolocation,
315/// permissions, and other browser context settings.
316///
317/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
318#[derive(Debug, Clone, Default, Serialize)]
319#[serde(rename_all = "camelCase")]
320pub struct BrowserContextOptions {
321    /// Sets consistent viewport for all pages in the context.
322    /// Set to null via `no_viewport(true)` to disable viewport emulation.
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub viewport: Option<Viewport>,
325
326    /// Disables viewport emulation when set to true.
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub no_viewport: Option<bool>,
329
330    /// Custom user agent string
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub user_agent: Option<String>,
333
334    /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub locale: Option<String>,
337
338    /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub timezone_id: Option<String>,
341
342    /// Geolocation coordinates
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub geolocation: Option<Geolocation>,
345
346    /// List of permissions to grant (e.g., "geolocation", "notifications")
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub permissions: Option<Vec<String>>,
349
350    /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub color_scheme: Option<String>,
353
354    /// Whether the viewport supports touch events
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub has_touch: Option<bool>,
357
358    /// Whether the meta viewport tag is respected
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub is_mobile: Option<bool>,
361
362    /// Whether JavaScript is enabled in the context
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub javascript_enabled: Option<bool>,
365
366    /// Emulates network being offline
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub offline: Option<bool>,
369
370    /// Whether to automatically download attachments
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub accept_downloads: Option<bool>,
373
374    /// Whether to bypass Content-Security-Policy
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub bypass_csp: Option<bool>,
377
378    /// Whether to ignore HTTPS errors
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub ignore_https_errors: Option<bool>,
381
382    /// Device scale factor (default: 1)
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub device_scale_factor: Option<f64>,
385
386    /// Extra HTTP headers to send with every request
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub extra_http_headers: Option<HashMap<String, String>>,
389
390    /// Base URL for relative navigation
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub base_url: Option<String>,
393}
394
395impl BrowserContextOptions {
396    /// Creates a new builder for BrowserContextOptions
397    pub fn builder() -> BrowserContextOptionsBuilder {
398        BrowserContextOptionsBuilder::default()
399    }
400}
401
402/// Builder for BrowserContextOptions
403#[derive(Debug, Clone, Default)]
404pub struct BrowserContextOptionsBuilder {
405    viewport: Option<Viewport>,
406    no_viewport: Option<bool>,
407    user_agent: Option<String>,
408    locale: Option<String>,
409    timezone_id: Option<String>,
410    geolocation: Option<Geolocation>,
411    permissions: Option<Vec<String>>,
412    color_scheme: Option<String>,
413    has_touch: Option<bool>,
414    is_mobile: Option<bool>,
415    javascript_enabled: Option<bool>,
416    offline: Option<bool>,
417    accept_downloads: Option<bool>,
418    bypass_csp: Option<bool>,
419    ignore_https_errors: Option<bool>,
420    device_scale_factor: Option<f64>,
421    extra_http_headers: Option<HashMap<String, String>>,
422    base_url: Option<String>,
423}
424
425impl BrowserContextOptionsBuilder {
426    /// Sets the viewport dimensions
427    pub fn viewport(mut self, viewport: Viewport) -> Self {
428        self.viewport = Some(viewport);
429        self.no_viewport = None; // Clear no_viewport if setting viewport
430        self
431    }
432
433    /// Disables viewport emulation
434    pub fn no_viewport(mut self, no_viewport: bool) -> Self {
435        self.no_viewport = Some(no_viewport);
436        if no_viewport {
437            self.viewport = None; // Clear viewport if setting no_viewport
438        }
439        self
440    }
441
442    /// Sets the user agent string
443    pub fn user_agent(mut self, user_agent: String) -> Self {
444        self.user_agent = Some(user_agent);
445        self
446    }
447
448    /// Sets the locale
449    pub fn locale(mut self, locale: String) -> Self {
450        self.locale = Some(locale);
451        self
452    }
453
454    /// Sets the timezone identifier
455    pub fn timezone_id(mut self, timezone_id: String) -> Self {
456        self.timezone_id = Some(timezone_id);
457        self
458    }
459
460    /// Sets the geolocation
461    pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
462        self.geolocation = Some(geolocation);
463        self
464    }
465
466    /// Sets the permissions to grant
467    pub fn permissions(mut self, permissions: Vec<String>) -> Self {
468        self.permissions = Some(permissions);
469        self
470    }
471
472    /// Sets the color scheme preference
473    pub fn color_scheme(mut self, color_scheme: String) -> Self {
474        self.color_scheme = Some(color_scheme);
475        self
476    }
477
478    /// Sets whether the viewport supports touch events
479    pub fn has_touch(mut self, has_touch: bool) -> Self {
480        self.has_touch = Some(has_touch);
481        self
482    }
483
484    /// Sets whether this is a mobile viewport
485    pub fn is_mobile(mut self, is_mobile: bool) -> Self {
486        self.is_mobile = Some(is_mobile);
487        self
488    }
489
490    /// Sets whether JavaScript is enabled
491    pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
492        self.javascript_enabled = Some(javascript_enabled);
493        self
494    }
495
496    /// Sets whether to emulate offline network
497    pub fn offline(mut self, offline: bool) -> Self {
498        self.offline = Some(offline);
499        self
500    }
501
502    /// Sets whether to automatically download attachments
503    pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
504        self.accept_downloads = Some(accept_downloads);
505        self
506    }
507
508    /// Sets whether to bypass Content-Security-Policy
509    pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
510        self.bypass_csp = Some(bypass_csp);
511        self
512    }
513
514    /// Sets whether to ignore HTTPS errors
515    pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
516        self.ignore_https_errors = Some(ignore_https_errors);
517        self
518    }
519
520    /// Sets the device scale factor
521    pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
522        self.device_scale_factor = Some(device_scale_factor);
523        self
524    }
525
526    /// Sets extra HTTP headers
527    pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
528        self.extra_http_headers = Some(extra_http_headers);
529        self
530    }
531
532    /// Sets the base URL for relative navigation
533    pub fn base_url(mut self, base_url: String) -> Self {
534        self.base_url = Some(base_url);
535        self
536    }
537
538    /// Builds the BrowserContextOptions
539    pub fn build(self) -> BrowserContextOptions {
540        BrowserContextOptions {
541            viewport: self.viewport,
542            no_viewport: self.no_viewport,
543            user_agent: self.user_agent,
544            locale: self.locale,
545            timezone_id: self.timezone_id,
546            geolocation: self.geolocation,
547            permissions: self.permissions,
548            color_scheme: self.color_scheme,
549            has_touch: self.has_touch,
550            is_mobile: self.is_mobile,
551            javascript_enabled: self.javascript_enabled,
552            offline: self.offline,
553            accept_downloads: self.accept_downloads,
554            bypass_csp: self.bypass_csp,
555            ignore_https_errors: self.ignore_https_errors,
556            device_scale_factor: self.device_scale_factor,
557            extra_http_headers: self.extra_http_headers,
558            base_url: self.base_url,
559        }
560    }
561}