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