Skip to main content

playwright_rs/protocol/
browser.rs

1// Browser protocol object
2//
3// Represents a browser instance created by BrowserType.launch()
4
5use crate::error::Result;
6use crate::protocol::{BrowserContext, BrowserType, Page};
7use crate::server::channel::Channel;
8use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
9use crate::server::connection::ConnectionExt;
10use serde::Deserialize;
11use serde_json::Value;
12use std::any::Any;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16
17use std::sync::Mutex;
18use std::sync::atomic::{AtomicBool, Ordering};
19
20/// Type alias for the future returned by a disconnected handler.
21type DisconnectedHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
22
23/// Type alias for a registered disconnected event handler.
24type DisconnectedHandler = Arc<dyn Fn() -> DisconnectedHandlerFuture + Send + Sync>;
25
26/// Options for `Browser::start_tracing()`.
27///
28/// See: <https://playwright.dev/docs/api/class-browser#browser-start-tracing>
29#[derive(Debug, Default, Clone)]
30pub struct StartTracingOptions {
31    /// If specified, tracing captures screenshots for this page.
32    /// Pass `Some(page)` to associate the trace with a specific page.
33    pub page: Option<Page>,
34    /// Whether to capture screenshots during tracing. Default false.
35    pub screenshots: Option<bool>,
36    /// Trace categories to enable. If omitted, uses a default set.
37    pub categories: Option<Vec<String>>,
38}
39
40/// Browser represents a browser instance.
41///
42/// A Browser is created when you call `BrowserType::launch()`. It provides methods
43/// to create browser contexts and pages.
44///
45/// # Example
46///
47/// ```ignore
48/// use playwright_rs::protocol::Playwright;
49///
50/// #[tokio::main]
51/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
52///     let playwright = Playwright::launch().await?;
53///     let chromium = playwright.chromium();
54///
55///     let browser = chromium.launch().await?;
56///     println!("Browser: {} version {}", browser.name(), browser.version());
57///     assert!(browser.is_connected());
58///
59///     let bt = browser.browser_type();
60///     assert_eq!(bt.name(), "chromium");
61///
62///     let context = browser.new_context().await?;
63///     let _page = context.new_page().await?;
64///     assert_eq!(browser.contexts().len(), 1);
65///
66///     browser.on_disconnected(|| async { Ok(()) }).await?;
67///
68///     browser.start_tracing(None).await?;
69///     let _trace_bytes = browser.stop_tracing().await?;
70///
71///     browser.close().await?;
72///     Ok(())
73/// }
74/// ```
75///
76/// See: <https://playwright.dev/docs/api/class-browser>
77#[derive(Clone)]
78pub struct Browser {
79    base: ChannelOwnerImpl,
80    version: String,
81    name: String,
82    is_connected: Arc<AtomicBool>,
83    /// Registered handlers for the "disconnected" event.
84    disconnected_handlers: Arc<Mutex<Vec<DisconnectedHandler>>>,
85}
86
87impl Browser {
88    /// Creates a new Browser from protocol initialization
89    ///
90    /// This is called by the object factory when the server sends a `__create__` message
91    /// for a Browser object.
92    ///
93    /// # Arguments
94    ///
95    /// * `parent` - The parent BrowserType object
96    /// * `type_name` - The protocol type name ("Browser")
97    /// * `guid` - The unique identifier for this browser instance
98    /// * `initializer` - The initialization data from the server
99    ///
100    /// # Errors
101    ///
102    /// Returns error if initializer is missing required fields (version, name)
103    pub fn new(
104        parent: Arc<dyn ChannelOwner>,
105        type_name: String,
106        guid: Arc<str>,
107        initializer: Value,
108    ) -> Result<Self> {
109        let base = ChannelOwnerImpl::new(
110            ParentOrConnection::Parent(parent),
111            type_name,
112            guid,
113            initializer.clone(),
114        );
115
116        let version = initializer["version"]
117            .as_str()
118            .ok_or_else(|| {
119                crate::error::Error::ProtocolError(
120                    "Browser initializer missing 'version' field".to_string(),
121                )
122            })?
123            .to_string();
124
125        let name = initializer["name"]
126            .as_str()
127            .ok_or_else(|| {
128                crate::error::Error::ProtocolError(
129                    "Browser initializer missing 'name' field".to_string(),
130                )
131            })?
132            .to_string();
133
134        Ok(Self {
135            base,
136            version,
137            name,
138            is_connected: Arc::new(AtomicBool::new(true)),
139            disconnected_handlers: Arc::new(Mutex::new(Vec::new())),
140        })
141    }
142
143    /// Returns the browser version string.
144    ///
145    /// See: <https://playwright.dev/docs/api/class-browser#browser-version>
146    pub fn version(&self) -> &str {
147        &self.version
148    }
149
150    /// Returns the browser name (e.g., "chromium", "firefox", "webkit").
151    ///
152    /// See: <https://playwright.dev/docs/api/class-browser#browser-name>
153    pub fn name(&self) -> &str {
154        &self.name
155    }
156
157    /// Returns true if the browser is connected.
158    ///
159    /// The browser is connected when it is launched and becomes disconnected when:
160    /// - `browser.close()` is called
161    /// - The browser process crashes
162    /// - The browser is closed by the user
163    ///
164    /// See: <https://playwright.dev/docs/api/class-browser#browser-is-connected>
165    pub fn is_connected(&self) -> bool {
166        self.is_connected.load(Ordering::SeqCst)
167    }
168
169    /// Returns the channel for sending protocol messages
170    ///
171    /// Used internally for sending RPC calls to the browser.
172    fn channel(&self) -> &Channel {
173        self.base.channel()
174    }
175
176    /// Creates a new browser context.
177    ///
178    /// A browser context is an isolated session within the browser instance,
179    /// similar to an incognito profile. Each context has its own cookies,
180    /// cache, and local storage.
181    ///
182    /// # Errors
183    ///
184    /// Returns error if:
185    /// - Browser has been closed
186    /// - Communication with browser process fails
187    ///
188    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
189    pub async fn new_context(&self) -> Result<BrowserContext> {
190        #[derive(Deserialize)]
191        struct NewContextResponse {
192            context: GuidRef,
193        }
194
195        #[derive(Deserialize)]
196        struct GuidRef {
197            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
198            guid: Arc<str>,
199        }
200
201        let response: NewContextResponse = self
202            .channel()
203            .send("newContext", serde_json::json!({}))
204            .await?;
205
206        let context: BrowserContext = self
207            .connection()
208            .get_typed::<BrowserContext>(&response.context.guid)
209            .await?;
210
211        let selectors = self.connection().selectors();
212        if let Err(e) = selectors.add_context(context.channel().clone()).await {
213            tracing::warn!("Failed to register BrowserContext with Selectors: {}", e);
214        }
215
216        Ok(context)
217    }
218
219    /// Creates a new browser context with custom options.
220    ///
221    /// A browser context is an isolated session within the browser instance,
222    /// similar to an incognito profile. Each context has its own cookies,
223    /// cache, and local storage.
224    ///
225    /// This method allows customizing viewport, user agent, locale, timezone,
226    /// and other settings.
227    ///
228    /// # Errors
229    ///
230    /// Returns error if:
231    /// - Browser has been closed
232    /// - Communication with browser process fails
233    /// - Invalid options provided
234    /// - Storage state file cannot be read or parsed
235    ///
236    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
237    pub async fn new_context_with_options(
238        &self,
239        mut options: crate::protocol::BrowserContextOptions,
240    ) -> Result<BrowserContext> {
241        // Response contains the GUID of the created BrowserContext
242        #[derive(Deserialize)]
243        struct NewContextResponse {
244            context: GuidRef,
245        }
246
247        #[derive(Deserialize)]
248        struct GuidRef {
249            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
250            guid: Arc<str>,
251        }
252
253        // Handle storage_state_path: read file and convert to inline storage_state
254        if let Some(path) = &options.storage_state_path {
255            let file_content = tokio::fs::read_to_string(path).await.map_err(|e| {
256                crate::error::Error::ProtocolError(format!(
257                    "Failed to read storage state file '{}': {}",
258                    path, e
259                ))
260            })?;
261
262            let storage_state: crate::protocol::StorageState = serde_json::from_str(&file_content)
263                .map_err(|e| {
264                    crate::error::Error::ProtocolError(format!(
265                        "Failed to parse storage state file '{}': {}",
266                        path, e
267                    ))
268                })?;
269
270            options.storage_state = Some(storage_state);
271            options.storage_state_path = None; // Clear path since we've converted to inline
272        }
273
274        // Convert options to JSON
275        let options_json = serde_json::to_value(options).map_err(|e| {
276            crate::error::Error::ProtocolError(format!(
277                "Failed to serialize context options: {}",
278                e
279            ))
280        })?;
281
282        // Send newContext RPC to server with options
283        let response: NewContextResponse = self.channel().send("newContext", options_json).await?;
284
285        // Retrieve and downcast the BrowserContext object from the connection registry
286        let context: BrowserContext = self
287            .connection()
288            .get_typed::<BrowserContext>(&response.context.guid)
289            .await?;
290
291        // Register new context with the Selectors coordinator.
292        let selectors = self.connection().selectors();
293        if let Err(e) = selectors.add_context(context.channel().clone()).await {
294            tracing::warn!("Failed to register BrowserContext with Selectors: {}", e);
295        }
296
297        Ok(context)
298    }
299
300    /// Creates a new page in a new browser context.
301    ///
302    /// This is a convenience method that creates a default context and then
303    /// creates a page in it. This is equivalent to calling `browser.new_context().await?.new_page().await?`.
304    ///
305    /// The created context is not directly accessible, but will be cleaned up
306    /// when the page is closed.
307    ///
308    /// # Errors
309    ///
310    /// Returns error if:
311    /// - Browser has been closed
312    /// - Communication with browser process fails
313    ///
314    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-page>
315    pub async fn new_page(&self) -> Result<Page> {
316        // Create a default context and then create a page in it
317        let context = self.new_context().await?;
318        context.new_page().await
319    }
320
321    /// Returns all open browser contexts.
322    ///
323    /// A new browser starts with no contexts. Contexts are created via
324    /// `new_context()` and cleaned up when they are closed.
325    ///
326    /// See: <https://playwright.dev/docs/api/class-browser#browser-contexts>
327    pub fn contexts(&self) -> Vec<BrowserContext> {
328        let my_guid = self.guid();
329        self.connection()
330            .all_objects_sync()
331            .into_iter()
332            .filter_map(|obj| {
333                let ctx = obj.as_any().downcast_ref::<BrowserContext>()?.clone();
334                let parent_guid = ctx.parent().map(|p| p.guid().to_string());
335                if parent_guid.as_deref() == Some(my_guid) {
336                    Some(ctx)
337                } else {
338                    None
339                }
340            })
341            .collect()
342    }
343
344    /// Returns the `BrowserType` that was used to launch this browser.
345    ///
346    /// See: <https://playwright.dev/docs/api/class-browser#browser-browser-type>
347    pub fn browser_type(&self) -> BrowserType {
348        self.base
349            .parent()
350            .expect("Browser always has a BrowserType parent")
351            .as_any()
352            .downcast_ref::<BrowserType>()
353            .expect("Browser parent is always a BrowserType")
354            .clone()
355    }
356
357    /// Registers a handler that fires when the browser is disconnected.
358    ///
359    /// The browser can become disconnected when it is closed, crashes, or
360    /// the process is killed. The handler is called with no arguments.
361    ///
362    /// # Arguments
363    ///
364    /// * `handler` - Async closure called when the browser disconnects.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error only if the mutex is poisoned (practically never).
369    ///
370    /// Creates a browser-level Chrome DevTools Protocol session.
371    ///
372    /// Unlike [`BrowserContext::new_cdp_session`](crate::protocol::BrowserContext::new_cdp_session)
373    /// which is scoped to a page, this session is attached to the browser itself.
374    /// Chromium only.
375    ///
376    /// See: <https://playwright.dev/docs/api/class-browser#browser-new-browser-cdp-session>
377    pub async fn new_browser_cdp_session(&self) -> Result<crate::protocol::CDPSession> {
378        #[derive(Deserialize)]
379        struct Response {
380            session: GuidRef,
381        }
382        #[derive(Deserialize)]
383        struct GuidRef {
384            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
385            guid: Arc<str>,
386        }
387
388        let response: Response = self
389            .channel()
390            .send("newBrowserCDPSession", serde_json::json!({}))
391            .await?;
392
393        self.connection()
394            .get_typed::<crate::protocol::CDPSession>(&response.session.guid)
395            .await
396    }
397
398    /// See: <https://playwright.dev/docs/api/class-browser#browser-event-disconnected>
399    pub async fn on_disconnected<F, Fut>(&self, handler: F) -> Result<()>
400    where
401        F: Fn() -> Fut + Send + Sync + 'static,
402        Fut: Future<Output = Result<()>> + Send + 'static,
403    {
404        let handler = Arc::new(move || -> DisconnectedHandlerFuture { Box::pin(handler()) });
405        self.disconnected_handlers.lock().unwrap().push(handler);
406        Ok(())
407    }
408
409    /// Starts CDP tracing on this browser (Chromium only).
410    ///
411    /// Only one trace may be active at a time per browser instance.
412    ///
413    /// # Arguments
414    ///
415    /// * `options` - Optional tracing configuration (screenshots, categories, page).
416    ///
417    /// # Errors
418    ///
419    /// Returns error if:
420    /// - Tracing is already active
421    /// - Called on a non-Chromium browser
422    /// - Communication with the browser fails
423    ///
424    /// See: <https://playwright.dev/docs/api/class-browser#browser-start-tracing>
425    pub async fn start_tracing(&self, options: Option<StartTracingOptions>) -> Result<()> {
426        #[derive(serde::Serialize)]
427        struct StartTracingParams {
428            #[serde(skip_serializing_if = "Option::is_none")]
429            page: Option<serde_json::Value>,
430            #[serde(skip_serializing_if = "Option::is_none")]
431            screenshots: Option<bool>,
432            #[serde(skip_serializing_if = "Option::is_none")]
433            categories: Option<Vec<String>>,
434        }
435
436        let opts = options.unwrap_or_default();
437
438        let page_ref = opts
439            .page
440            .as_ref()
441            .map(|p| serde_json::json!({ "guid": p.guid() }));
442
443        let params = StartTracingParams {
444            page: page_ref,
445            screenshots: opts.screenshots,
446            categories: opts.categories,
447        };
448
449        self.channel()
450            .send_no_result(
451                "startTracing",
452                serde_json::to_value(params).map_err(|e| {
453                    crate::error::Error::ProtocolError(format!(
454                        "serialize startTracing params: {e}"
455                    ))
456                })?,
457            )
458            .await
459    }
460
461    /// Stops CDP tracing and returns the raw trace data.
462    ///
463    /// The returned bytes can be written to a `.json` file and loaded in
464    /// `chrome://tracing` or [Perfetto](https://ui.perfetto.dev).
465    ///
466    /// # Errors
467    ///
468    /// Returns error if no tracing was started or communication fails.
469    ///
470    /// See: <https://playwright.dev/docs/api/class-browser#browser-stop-tracing>
471    pub async fn stop_tracing(&self) -> Result<Vec<u8>> {
472        #[derive(Deserialize)]
473        struct StopTracingResponse {
474            artifact: ArtifactRef,
475        }
476
477        #[derive(Deserialize)]
478        struct ArtifactRef {
479            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
480            guid: Arc<str>,
481        }
482
483        let response: StopTracingResponse = self
484            .channel()
485            .send("stopTracing", serde_json::json!({}))
486            .await?;
487
488        // save_as() rather than streaming because Stream protocol is not yet implemented
489        let artifact: crate::protocol::artifact::Artifact = self
490            .connection()
491            .get_typed::<crate::protocol::artifact::Artifact>(&response.artifact.guid)
492            .await?;
493
494        let tmp_path = std::env::temp_dir().join(format!(
495            "playwright-trace-{}.json",
496            response.artifact.guid.replace('@', "-")
497        ));
498        let tmp_str = tmp_path
499            .to_str()
500            .ok_or_else(|| {
501                crate::error::Error::ProtocolError(
502                    "Temporary path contains non-UTF-8 characters".to_string(),
503                )
504            })?
505            .to_string();
506
507        artifact.save_as(&tmp_str).await?;
508
509        let bytes = tokio::fs::read(&tmp_path).await.map_err(|e| {
510            crate::error::Error::ProtocolError(format!(
511                "Failed to read tracing artifact from '{}': {}",
512                tmp_str, e
513            ))
514        })?;
515
516        let _ = tokio::fs::remove_file(&tmp_path).await;
517
518        Ok(bytes)
519    }
520
521    /// Closes the browser and all of its pages (if any were opened).
522    ///
523    /// This is a graceful operation that sends a close command to the browser
524    /// and waits for it to shut down properly.
525    ///
526    /// # Errors
527    ///
528    /// Returns error if:
529    /// - Browser has already been closed
530    /// - Communication with browser process fails
531    ///
532    /// See: <https://playwright.dev/docs/api/class-browser#browser-close>
533    pub async fn close(&self) -> Result<()> {
534        // Send close RPC to server
535        // The protocol expects an empty object as params
536        let result = self
537            .channel()
538            .send_no_result("close", serde_json::json!({}))
539            .await;
540
541        // Add delay on Windows CI to ensure browser process fully terminates
542        // This prevents subsequent browser launches from hanging
543        #[cfg(windows)]
544        {
545            let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
546            if is_ci {
547                tracing::debug!("[playwright-rust] Adding Windows CI browser cleanup delay");
548                tokio::time::sleep(std::time::Duration::from_millis(500)).await;
549            }
550        }
551
552        result
553    }
554}
555
556impl ChannelOwner for Browser {
557    fn guid(&self) -> &str {
558        self.base.guid()
559    }
560
561    fn type_name(&self) -> &str {
562        self.base.type_name()
563    }
564
565    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
566        self.base.parent()
567    }
568
569    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
570        self.base.connection()
571    }
572
573    fn initializer(&self) -> &Value {
574        self.base.initializer()
575    }
576
577    fn channel(&self) -> &Channel {
578        self.base.channel()
579    }
580
581    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
582        // Use compare_exchange so handlers fire exactly once across both the
583        // "disconnected" event path and the __dispose__ path.
584        if self
585            .is_connected
586            .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
587            .is_ok()
588        {
589            let handlers = self.disconnected_handlers.lock().unwrap().clone();
590            tokio::spawn(async move {
591                for handler in handlers {
592                    if let Err(e) = handler().await {
593                        tracing::warn!("Browser disconnected handler error (from dispose): {}", e);
594                    }
595                }
596            });
597        }
598        self.base.dispose(reason)
599    }
600
601    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
602        self.base.adopt(child)
603    }
604
605    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
606        self.base.add_child(guid, child)
607    }
608
609    fn remove_child(&self, guid: &str) {
610        self.base.remove_child(guid)
611    }
612
613    fn on_event(&self, method: &str, params: Value) {
614        if method == "disconnected" {
615            // Use compare_exchange to fire handlers exactly once (guards against
616            // both the "disconnected" event and the __dispose__ path firing them).
617            if self
618                .is_connected
619                .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
620                .is_ok()
621            {
622                let handlers = self.disconnected_handlers.lock().unwrap().clone();
623                tokio::spawn(async move {
624                    for handler in handlers {
625                        if let Err(e) = handler().await {
626                            tracing::warn!("Browser disconnected handler error: {}", e);
627                        }
628                    }
629                });
630            }
631        }
632        self.base.on_event(method, params)
633    }
634
635    fn was_collected(&self) -> bool {
636        self.base.was_collected()
637    }
638
639    fn as_any(&self) -> &dyn Any {
640        self
641    }
642}
643
644impl std::fmt::Debug for Browser {
645    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
646        f.debug_struct("Browser")
647            .field("guid", &self.guid())
648            .field("name", &self.name)
649            .field("version", &self.version)
650            .finish()
651    }
652}
653
654// Note: Browser testing is done via integration tests since it requires:
655// - A real Connection with object registry
656// - Protocol messages from the server
657// - BrowserType.launch() to create Browser objects
658// See: crates/playwright-core/tests/browser_launch_integration.rs (Phase 2 Slice 3)