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}