Skip to main content

playwright_rs/protocol/browser_context/
lifecycle.rs

1use super::BrowserContext;
2use crate::error::Result;
3use crate::protocol::api_request_context::APIRequestContext;
4use crate::protocol::cdp_session::CDPSession;
5use crate::protocol::event_registry::EventRegistry;
6use crate::protocol::tracing::Tracing;
7use crate::protocol::{Browser, Page};
8use crate::server::channel::Channel;
9use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
10use crate::server::connection::ConnectionExt;
11use serde::Deserialize;
12use serde_json::Value;
13use std::collections::HashMap;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, Mutex};
16
17/// Lifecycle: construction, pages and companion objects, timeouts, close.
18impl BrowserContext {
19    /// Creates a new BrowserContext from protocol initialization
20    ///
21    /// This is called by the object factory when the server sends a `__create__` message
22    /// for a BrowserContext object.
23    ///
24    /// # Arguments
25    ///
26    /// * `parent` - The parent Browser object
27    /// * `type_name` - The protocol type name ("BrowserContext")
28    /// * `guid` - The unique identifier for this context
29    /// * `initializer` - The initialization data from the server
30    ///
31    /// # Errors
32    ///
33    /// Returns error if initializer is malformed
34    pub fn new(
35        parent: Arc<dyn ChannelOwner>,
36        type_name: String,
37        guid: Arc<str>,
38        initializer: Value,
39    ) -> Result<Self> {
40        // Extract APIRequestContext GUID from initializer before moving it
41        let request_context_guid = initializer
42            .get("requestContext")
43            .and_then(|v| v.get("guid"))
44            .and_then(|v| v.as_str())
45            .map(|s| s.to_string());
46
47        // Extract Tracing GUID from initializer before moving it
48        let tracing_guid = initializer
49            .get("tracing")
50            .and_then(|v| v.get("guid"))
51            .and_then(|v| v.as_str())
52            .map(|s| s.to_string());
53
54        // Extract Debugger GUID from initializer before moving it
55        let debugger_guid = initializer
56            .get("debugger")
57            .and_then(|v| v.get("guid"))
58            .and_then(|v| v.as_str())
59            .map(|s| s.to_string());
60
61        let base = ChannelOwnerImpl::new(
62            ParentOrConnection::Parent(parent.clone()),
63            type_name,
64            guid,
65            initializer,
66        );
67
68        // Store browser reference if parent is a Browser
69        // Returns None only for special contexts (Android, Electron) where parent is not a Browser
70        // For both regular contexts and persistent contexts, parent is a Browser instance
71        let browser = parent.as_any().downcast_ref::<Browser>().cloned();
72
73        let context = Self {
74            base,
75            browser,
76            pages: Arc::new(Mutex::new(Vec::new())),
77            route_handlers: Arc::new(Mutex::new(Vec::new())),
78            request_context_guid,
79            tracing_guid,
80            debugger_guid,
81            default_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
82                crate::DEFAULT_TIMEOUT_MS.to_bits(),
83            )),
84            default_navigation_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
85                crate::DEFAULT_TIMEOUT_MS.to_bits(),
86            )),
87            page_events: EventRegistry::new("page"),
88            close_events: EventRegistry::new("close"),
89            request: EventRegistry::new("request"),
90            request_finished: EventRegistry::new("requestFinished"),
91            request_failed: EventRegistry::new("requestFailed"),
92            response: EventRegistry::new("response"),
93            dialog: EventRegistry::new("dialog"),
94            dialog_closed: EventRegistry::new("dialogClosed"),
95            binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
96            console: EventRegistry::new("console"),
97            weberror: EventRegistry::new("weberror"),
98            serviceworker_handlers: Arc::new(Mutex::new(Vec::new())),
99            forwarders: super::events::Forwarders::default(),
100            serviceworker_waiters: Arc::new(Mutex::new(Vec::new())),
101            service_workers_list: Arc::new(Mutex::new(Vec::new())),
102            ws_route_handlers: Arc::new(Mutex::new(Vec::new())),
103            is_closed: Arc::new(AtomicBool::new(false)),
104            option_har: Arc::new(tokio::sync::Mutex::new(None)),
105        };
106
107        // Enable dialog and console event subscriptions eagerly.
108        // Console events must be subscribed to receive them without a registered handler,
109        // enabling the console_messages() and page_errors() passive accumulators on Page.
110        let channel = context.channel().clone();
111        crate::task::spawn(async move {
112            _ = channel.update_subscription("dialog", true).await;
113            _ = channel.update_subscription("console", true).await;
114        });
115
116        // Note: Selectors registration is done by the caller (e.g. Browser::new_context())
117        // after this object is returned, so that add_context() can be awaited properly.
118
119        Ok(context)
120    }
121
122    /// Returns the channel for sending protocol messages
123    ///
124    /// Used internally for sending RPC calls to the context.
125    pub(super) fn channel(&self) -> &Channel {
126        self.base.channel()
127    }
128
129    /// Creates a new page in this browser context.
130    ///
131    /// Pages are isolated tabs/windows within a context. Each page starts
132    /// at "about:blank" and can be navigated independently.
133    ///
134    /// # Errors
135    ///
136    /// Returns error if:
137    /// - Context has been closed
138    /// - Communication with browser process fails
139    ///
140    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
141    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
142    pub async fn new_page(&self) -> Result<Page> {
143        // Response contains the GUID of the created Page
144        #[derive(Deserialize)]
145        struct NewPageResponse {
146            page: GuidRef,
147        }
148
149        #[derive(Deserialize)]
150        struct GuidRef {
151            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
152            guid: Arc<str>,
153        }
154
155        // Send newPage RPC to server
156        let response: NewPageResponse = self
157            .channel()
158            .send("newPage", serde_json::json!({}))
159            .await?;
160
161        // Retrieve and downcast the Page object from the connection registry
162        let page: Page = self
163            .connection()
164            .get_typed::<Page>(&response.page.guid)
165            .await?;
166
167        // Note: Don't track the page here - it will be tracked via the "page" event
168        // that Playwright server sends automatically when a page is created.
169        // Tracking it here would create duplicates.
170
171        // Propagate context-level timeout defaults to the new page
172        let ctx_timeout = self.default_timeout_ms();
173        let ctx_nav_timeout = self.default_navigation_timeout_ms();
174        if ctx_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
175            page.set_default_timeout(ctx_timeout).await;
176        }
177        if ctx_nav_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
178            page.set_default_navigation_timeout(ctx_nav_timeout).await;
179        }
180
181        Ok(page)
182    }
183
184    /// Returns all open pages in the context.
185    ///
186    /// This method provides a snapshot of all currently active pages that belong
187    /// to this browser context instance. Pages created via `new_page()` and popup
188    /// pages opened through user interactions are included.
189    ///
190    /// In persistent contexts launched with `--app=url`, this will include the
191    /// initial page created automatically by Playwright.
192    ///
193    /// # Errors
194    ///
195    /// This method does not return errors. It provides a snapshot of pages at
196    /// the time of invocation.
197    ///
198    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-pages>
199    pub fn pages(&self) -> Vec<Page> {
200        self.pages.lock().unwrap().clone()
201    }
202
203    /// Returns all active service workers registered in this browser context.
204    ///
205    /// Service workers are accumulated as they are registered (`serviceWorker` event).
206    /// Each call returns a snapshot of the current list.
207    ///
208    /// Note: Testing service workers typically requires HTTPS. In plain HTTP or
209    /// `about:blank` contexts this list is empty.
210    ///
211    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-service-workers>
212    pub fn service_workers(&self) -> Vec<crate::protocol::Worker> {
213        self.service_workers_list.lock().unwrap().clone()
214    }
215
216    /// Returns the browser instance that owns this context.
217    ///
218    /// Returns `None` only for contexts created outside of normal browser
219    /// (e.g., Android or Electron contexts). For both regular contexts and
220    /// persistent contexts, this returns the owning Browser instance.
221    ///
222    /// # Errors
223    ///
224    /// This method does not return errors.
225    ///
226    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-browser>
227    pub fn browser(&self) -> Option<Browser> {
228        self.browser.clone()
229    }
230
231    /// Returns the APIRequestContext associated with this context.
232    ///
233    /// The APIRequestContext is created automatically by the server for each
234    /// BrowserContext. It enables performing HTTP requests and is used internally
235    /// by `Route::fetch()`.
236    ///
237    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-request>
238    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
239    pub async fn request(&self) -> Result<APIRequestContext> {
240        let guid = self.request_context_guid.as_ref().ok_or_else(|| {
241            crate::error::Error::ProtocolError(
242                "No APIRequestContext available for this context".to_string(),
243            )
244        })?;
245
246        self.connection().get_typed::<APIRequestContext>(guid).await
247    }
248
249    /// Creates a new Chrome DevTools Protocol session for the given page.
250    ///
251    /// CDPSession provides low-level access to the Chrome DevTools Protocol.
252    /// This method is only available in Chromium-based browsers.
253    ///
254    /// # Arguments
255    ///
256    /// * `page` - The page to create a CDP session for
257    ///
258    /// # Errors
259    ///
260    /// Returns error if:
261    /// - The browser is not Chromium-based
262    /// - Context has been closed
263    /// - Communication with browser process fails
264    ///
265    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-cdp-session>
266    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), page_guid = %page.guid()))]
267    pub async fn new_cdp_session(&self, page: &Page) -> Result<CDPSession> {
268        #[derive(serde::Deserialize)]
269        struct NewCDPSessionResponse {
270            session: GuidRef,
271        }
272
273        #[derive(serde::Deserialize)]
274        struct GuidRef {
275            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
276            guid: Arc<str>,
277        }
278
279        let response: NewCDPSessionResponse = self
280            .channel()
281            .send(
282                "newCDPSession",
283                serde_json::json!({ "page": { "guid": page.guid() } }),
284            )
285            .await?;
286
287        self.connection()
288            .get_typed::<CDPSession>(&response.session.guid)
289            .await
290    }
291
292    /// Returns the Tracing object for this browser context.
293    ///
294    /// The Tracing object is created automatically by the Playwright server for each
295    /// BrowserContext. Use it to start and stop trace recording.
296    ///
297    /// # Errors
298    ///
299    /// Returns error if no Tracing object is available for this context (rare,
300    /// should not happen in normal usage).
301    ///
302    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-tracing>
303    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
304    pub async fn tracing(&self) -> Result<Tracing> {
305        let guid = self.tracing_guid.as_ref().ok_or_else(|| {
306            crate::error::Error::ProtocolError(
307                "No Tracing object available for this context".to_string(),
308            )
309        })?;
310
311        self.connection().get_typed::<Tracing>(guid).await
312    }
313
314    /// Returns the [`Debugger`](crate::protocol::Debugger) for this context.
315    ///
316    /// The Debugger surfaces programmatic control of Playwright Inspector's
317    /// "PAUSED" overlay — `request_pause`, `resume`, `next`, `run_to`, and a
318    /// `pausedStateChanged` event. Used by IDE integrations and
319    /// inspector-style tools.
320    ///
321    /// See: <https://playwright.dev/docs/api/class-debugger>
322    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
323    pub async fn debugger(&self) -> Result<crate::protocol::Debugger> {
324        let guid = self.debugger_guid.as_ref().ok_or_else(|| {
325            crate::error::Error::ProtocolError(
326                "No Debugger object available for this context".to_string(),
327            )
328        })?;
329        self.connection()
330            .get_typed::<crate::protocol::Debugger>(guid)
331            .await
332    }
333
334    /// Returns the Clock object for this browser context.
335    ///
336    /// The Clock object enables fake timer control — install fake timers,
337    /// fast-forward time, pause/resume, and set fixed or system time.
338    ///
339    /// `page.clock()` delegates to this method via the page's parent context.
340    ///
341    /// See: <https://playwright.dev/docs/api/class-clock>
342    pub fn clock(&self) -> crate::protocol::clock::Clock {
343        crate::protocol::clock::Clock::new(self.channel().clone())
344    }
345
346    /// Manage the context's virtual WebAuthn authenticator:
347    /// install it, then register / list / delete passkeys for
348    /// `navigator.credentials` testing.
349    ///
350    /// See: <https://playwright.dev/docs/api/class-credentials>
351    pub fn credentials(&self) -> crate::protocol::Credentials {
352        crate::protocol::Credentials::new(self.channel().clone())
353    }
354
355    /// Starts the HAR recording a `record_har` option asks for, once the
356    /// context exists. [`close`](Self::close) writes it.
357    pub(crate) async fn start_option_har(
358        &self,
359        har: crate::protocol::har_options::OptionHar,
360    ) -> Result<()> {
361        let recording = self
362            .tracing()
363            .await?
364            .begin_har(har.path, har.options)
365            .await?;
366        *self.option_har.lock().await = Some(recording);
367        Ok(())
368    }
369
370    /// Closes the browser context and all its pages.
371    ///
372    /// This is a graceful operation that sends a close command to the context
373    /// and waits for it to shut down properly.
374    ///
375    /// # Errors
376    ///
377    /// Returns error if:
378    /// - Context has already been closed
379    /// - Communication with browser process fails
380    ///
381    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
382    #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
383    pub async fn close(&self) -> Result<()> {
384        // Write the HAR the `record_har` option started, while the context
385        // can still export it. Any failure returns before the context
386        // closes, as in the other language bindings. A failed export keeps
387        // the recording, so a second `close()` tries it again; once exported,
388        // the driver has let the recording go, so a failed write is reported
389        // once and a second `close()` closes the context.
390        let mut option_har = self.option_har.lock().await;
391        if let Some(recording) = option_har.as_ref() {
392            let tracing = self.tracing().await?;
393            let artifact = tracing.har_export(recording).await?;
394            let written = match &artifact {
395                Some(guid) => tracing.write_har(guid, recording).await,
396                None => Ok(()),
397            };
398            *option_har = None;
399            written?;
400        }
401        drop(option_har);
402
403        // Unregister from Selectors coordinator so closed channels are not sent future messages.
404        let selectors = self.connection().selectors();
405        selectors.remove_context(self.channel());
406
407        // Send close RPC to server
408        let result = self
409            .channel()
410            .send_no_result("close", serde_json::json!({}))
411            .await;
412        // Mark as closed regardless of error (best-effort)
413        self.is_closed.store(true, Ordering::Relaxed);
414        result
415    }
416
417    /// Sets the default timeout for all operations in this browser context.
418    ///
419    /// This applies to all pages already open in this context as well as pages
420    /// created subsequently. Pass `0` to disable timeouts.
421    ///
422    /// # Arguments
423    ///
424    /// * `timeout` - Timeout in milliseconds
425    ///
426    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout>
427    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
428    pub async fn set_default_timeout(&self, timeout: f64) {
429        self.default_timeout_ms
430            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
431        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
432        for page in pages {
433            page.set_default_timeout(timeout).await;
434        }
435    }
436
437    /// Sets the default timeout for navigation operations in this browser context.
438    ///
439    /// This applies to all pages already open in this context as well as pages
440    /// created subsequently. Pass `0` to disable timeouts.
441    ///
442    /// # Arguments
443    ///
444    /// * `timeout` - Timeout in milliseconds
445    ///
446    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout>
447    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
448    pub async fn set_default_navigation_timeout(&self, timeout: f64) {
449        self.default_navigation_timeout_ms
450            .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
451        let pages: Vec<Page> = self.pages.lock().unwrap().clone();
452        for page in pages {
453            page.set_default_navigation_timeout(timeout).await;
454        }
455    }
456
457    /// Returns the context's current default action timeout in milliseconds.
458    fn default_timeout_ms(&self) -> f64 {
459        f64::from_bits(
460            self.default_timeout_ms
461                .load(std::sync::atomic::Ordering::Relaxed),
462        )
463    }
464
465    /// Returns the context's current default navigation timeout in milliseconds.
466    fn default_navigation_timeout_ms(&self) -> f64 {
467        f64::from_bits(
468            self.default_navigation_timeout_ms
469                .load(std::sync::atomic::Ordering::Relaxed),
470        )
471    }
472
473    /// Pauses the browser context.
474    ///
475    /// This pauses the execution of all pages in the context.
476    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
477    pub async fn pause(&self) -> Result<()> {
478        self.channel()
479            .send_no_result("pause", serde_json::Value::Null)
480            .await
481    }
482
483    /// Returns whether this browser context has been closed.
484    ///
485    /// Returns `true` after [`close()`](Self::close) has been called on this context, or after the
486    /// context receives a close event from the server (e.g. when the browser is closed).
487    ///
488    /// Note: this reflects eventual state. If the context was closed by a server-initiated
489    /// event, `is_closed()` becomes `true` only after the "close" event has been received
490    /// and processed.
491    ///
492    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-is-closed>
493    pub fn is_closed(&self) -> bool {
494        self.is_closed.load(Ordering::Relaxed)
495    }
496}