playwright_rs/protocol/browser_type.rs
1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// BrowserType - Represents a browser type (Chromium, Firefox, WebKit)
5//
6// Reference:
7// - Python: playwright-python/playwright/_impl/_browser_type.py
8// - Protocol: protocol.yml (BrowserType interface)
9
10use crate::api::{ConnectOptions, ConnectOverCdpOptions, LaunchOptions};
11use crate::error::Result;
12use crate::protocol::{Browser, BrowserContext, BrowserContextOptions};
13use crate::server::channel::Channel;
14use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
15use crate::server::connection::{ConnectionExt, ConnectionLike};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::any::Any;
19use std::sync::Arc;
20use tracing::Instrument;
21
22/// BrowserType represents a browser engine (Chromium, Firefox, or WebKit).
23///
24/// Each Playwright instance provides three BrowserType objects accessible via:
25/// - `playwright.chromium()`
26/// - `playwright.firefox()`
27/// - `playwright.webkit()`
28///
29/// BrowserType provides three main modes:
30/// 1. **Launch**: Creates a new browser instance
31/// 2. **Launch Persistent Context**: Creates browser + context with persistent storage
32/// 3. **Connect**: Connects to an existing remote browser instance
33///
34/// # Example
35///
36/// ```ignore
37/// # use playwright_rs::protocol::Playwright;
38/// # use playwright_rs::api::LaunchOptions;
39/// # use playwright_rs::protocol::BrowserContextOptions;
40/// # #[tokio::main]
41/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
42/// let playwright = Playwright::launch().await?;
43/// let chromium = playwright.chromium();
44///
45/// // Verify browser type info
46/// assert_eq!(chromium.name(), "chromium");
47/// assert!(!chromium.executable_path().is_empty());
48///
49/// // === Standard Launch ===
50/// // Launch with default options
51/// let browser1 = chromium.launch().await?;
52/// assert_eq!(browser1.name(), "chromium");
53/// assert!(!browser1.version().is_empty());
54/// browser1.close().await?;
55///
56/// // === Remote Connection ===
57/// // Connect to a remote browser (e.g., started with `npx playwright launch-server`)
58/// // let browser3 = chromium.connect("ws://localhost:3000", None).await?;
59/// // browser3.close().await?;
60///
61/// // === CDP Connection (Chromium only) ===
62/// // Connect to a Chrome instance with remote debugging enabled
63/// // let browser4 = chromium.connect_over_cdp("http://localhost:9222", None).await?;
64/// // browser4.close().await?;
65///
66/// // === Persistent Context Launch ===
67/// // Launch with persistent storage (cookies, local storage, etc.)
68/// let context = chromium
69/// .launch_persistent_context("/tmp/user-data")
70/// .await?;
71/// let page = context.new_page().await?;
72/// page.goto("https://example.com", None).await?;
73/// context.close().await?; // Closes browser too
74///
75/// // === App Mode (Standalone Window) ===
76/// // Launch as a standalone application window
77/// let app_options = BrowserContextOptions::builder()
78/// .args(vec!["--app=https://example.com".to_string()])
79/// .headless(true) // Set to true for CI, but app mode is typically headed
80/// .build();
81///
82/// let app_context = chromium
83/// .launch_persistent_context_with_options("/tmp/app-data", app_options)
84/// .await?;
85/// // Browser opens directly to URL without address bar
86/// app_context.close().await?;
87/// # Ok(())
88/// # }
89/// ```
90///
91/// See: <https://playwright.dev/docs/api/class-browsertype>
92#[derive(Clone)]
93pub struct BrowserType {
94 /// Base ChannelOwner implementation
95 base: ChannelOwnerImpl,
96 /// Browser name ("chromium", "firefox", or "webkit")
97 name: String,
98 /// Path to browser executable
99 executable_path: String,
100}
101
102impl BrowserType {
103 /// Creates a new BrowserType object from protocol initialization.
104 ///
105 /// Called by the object factory when server sends __create__ message.
106 ///
107 /// # Arguments
108 /// * `parent` - Parent (Connection for root objects, or another ChannelOwner)
109 /// * `type_name` - Protocol type name ("BrowserType")
110 /// * `guid` - Unique GUID from server (e.g., "browserType@chromium")
111 /// * `initializer` - Initial state with name and executablePath
112 pub fn new(
113 parent: ParentOrConnection,
114 type_name: String,
115 guid: Arc<str>,
116 initializer: Value,
117 ) -> Result<Self> {
118 let base = ChannelOwnerImpl::new(parent, type_name, guid, initializer.clone());
119
120 // Extract fields from initializer
121 let name = initializer["name"]
122 .as_str()
123 .ok_or_else(|| {
124 crate::error::Error::ProtocolError(
125 "BrowserType initializer missing 'name'".to_string(),
126 )
127 })?
128 .to_string();
129
130 let executable_path = initializer["executablePath"]
131 .as_str()
132 .unwrap_or_default() // executablePath might be optional/empty for remote connection objects
133 .to_string();
134
135 Ok(Self {
136 base,
137 name,
138 executable_path,
139 })
140 }
141
142 /// Returns the browser name ("chromium", "firefox", or "webkit").
143 pub fn name(&self) -> &str {
144 &self.name
145 }
146
147 /// Returns the path to the browser executable.
148 pub fn executable_path(&self) -> &str {
149 &self.executable_path
150 }
151
152 /// Launches a browser instance with default options.
153 ///
154 /// This is equivalent to calling `launch_with_options(LaunchOptions::default())`.
155 ///
156 /// # Errors
157 ///
158 /// Returns error if:
159 /// - Browser executable not found
160 /// - Launch timeout (default 30s)
161 /// - Browser process fails to start
162 ///
163 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
164 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
165 pub async fn launch(&self) -> Result<Browser> {
166 self.launch_with_options(LaunchOptions::default()).await
167 }
168
169 /// Launches a browser instance with custom options.
170 ///
171 /// # Arguments
172 ///
173 /// * `options` - Launch options (headless, args, etc.)
174 ///
175 /// # Errors
176 ///
177 /// Returns error if:
178 /// - Browser executable not found
179 /// - Launch timeout
180 /// - Invalid options
181 /// - Browser process fails to start
182 ///
183 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
184 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
185 pub async fn launch_with_options(&self, options: LaunchOptions) -> Result<Browser> {
186 // Add Windows CI-specific browser args to prevent hanging
187 let options = {
188 #[cfg(windows)]
189 {
190 let mut options = options;
191 // Check if we're in a CI environment (GitHub Actions, Jenkins, etc.)
192 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
193
194 if is_ci {
195 tracing::debug!(
196 "[playwright-rust] Detected Windows CI environment, adding stability flags"
197 );
198
199 // Get existing args or create empty vec
200 let mut args = options.args.unwrap_or_default();
201
202 // Add Windows CI stability flags if not already present
203 let ci_flags = vec![
204 "--no-sandbox", // Disable sandboxing (often problematic in CI)
205 "--disable-dev-shm-usage", // Overcome limited /dev/shm resources
206 "--disable-gpu", // Disable GPU hardware acceleration
207 "--disable-web-security", // Avoid CORS issues in CI
208 "--disable-features=IsolateOrigins,site-per-process", // Reduce process overhead
209 ];
210
211 for flag in ci_flags {
212 if !args.iter().any(|a| a == flag) {
213 args.push(flag.to_string());
214 }
215 }
216
217 // Update options with enhanced args
218 options.args = Some(args);
219
220 // Increase timeout for Windows CI (slower startup)
221 if options.timeout.is_none() {
222 options.timeout = Some(60000.0); // 60 seconds for Windows CI
223 }
224 }
225 options
226 }
227
228 #[cfg(not(windows))]
229 {
230 options
231 }
232 };
233
234 // Normalize options for protocol transmission
235 let params = options.normalize();
236
237 // Send launch RPC to server
238 let response: LaunchResponse = self.base.channel().send("launch", params).await?;
239
240 // Get browser object from registry and downcast
241 let browser: Browser = self
242 .connection()
243 .get_typed::<Browser>(&response.browser.guid)
244 .await?;
245
246 Ok(browser)
247 }
248
249 /// Launches a browser with persistent storage using default options.
250 ///
251 /// Returns a persistent browser context. Closing this context will automatically
252 /// close the browser.
253 ///
254 /// This method is useful for:
255 /// - Preserving authentication state across sessions
256 /// - Testing with real user profiles
257 /// - Creating standalone applications with app mode
258 /// - Simulating real user behavior with cookies and storage
259 ///
260 /// # Arguments
261 ///
262 /// * `user_data_dir` - Path to a user data directory (stores cookies, local storage)
263 ///
264 /// # Errors
265 ///
266 /// Returns error if:
267 /// - Browser executable not found
268 /// - Launch timeout (default 30s)
269 /// - Browser process fails to start
270 /// - User data directory cannot be created
271 ///
272 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
273 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
274 pub async fn launch_persistent_context(
275 &self,
276 user_data_dir: impl Into<String>,
277 ) -> Result<BrowserContext> {
278 self.launch_persistent_context_with_options(user_data_dir, BrowserContextOptions::default())
279 .await
280 }
281
282 /// Launches a browser with persistent storage and custom options.
283 ///
284 /// Returns a persistent browser context with the specified configuration.
285 /// Closing this context will automatically close the browser.
286 ///
287 /// This method accepts both launch options (headless, args, etc.) and context
288 /// options (viewport, locale, etc.) in a single BrowserContextOptions struct.
289 ///
290 /// # Arguments
291 ///
292 /// * `user_data_dir` - Path to a user data directory (stores cookies, local storage)
293 /// * `options` - Combined launch and context options
294 ///
295 /// # Errors
296 ///
297 /// Returns error if:
298 /// - Browser executable not found
299 /// - Launch timeout
300 /// - Invalid options
301 /// - Browser process fails to start
302 /// - User data directory cannot be created
303 ///
304 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
305 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name()))]
306 pub async fn launch_persistent_context_with_options(
307 &self,
308 user_data_dir: impl Into<String>,
309 mut options: BrowserContextOptions,
310 ) -> Result<BrowserContext> {
311 // Add Windows CI-specific browser args to prevent hanging
312 #[cfg(windows)]
313 {
314 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
315
316 if is_ci {
317 tracing::debug!(
318 "[playwright-rust] Detected Windows CI environment, adding stability flags"
319 );
320
321 // Get existing args or create empty vec
322 let mut args = options.args.unwrap_or_default();
323
324 // Add Windows CI stability flags if not already present
325 let ci_flags = vec![
326 "--no-sandbox", // Disable sandboxing (often problematic in CI)
327 "--disable-dev-shm-usage", // Overcome limited /dev/shm resources
328 "--disable-gpu", // Disable GPU hardware acceleration
329 "--disable-web-security", // Avoid CORS issues in CI
330 "--disable-features=IsolateOrigins,site-per-process", // Reduce process overhead
331 ];
332
333 for flag in ci_flags {
334 if !args.iter().any(|a| a == flag) {
335 args.push(flag.to_string());
336 }
337 }
338
339 // Update options with enhanced args
340 options.args = Some(args);
341
342 // Increase timeout for Windows CI (slower startup)
343 if options.timeout.is_none() {
344 options.timeout = Some(60000.0); // 60 seconds for Windows CI
345 }
346 }
347 }
348
349 // Handle storage_state_path: read file and convert to inline storage_state
350 if let Some(path) = &options.storage_state_path {
351 let file_content = tokio::fs::read_to_string(path).await.map_err(|e| {
352 crate::error::Error::ProtocolError(format!(
353 "Failed to read storage state file '{}': {}",
354 path, e
355 ))
356 })?;
357
358 let storage_state: crate::protocol::StorageState = serde_json::from_str(&file_content)
359 .map_err(|e| {
360 crate::error::Error::ProtocolError(format!(
361 "Failed to parse storage state file '{}': {}",
362 path, e
363 ))
364 })?;
365
366 options.storage_state = Some(storage_state);
367 options.storage_state_path = None; // Clear path since we've converted to inline
368 }
369
370 // Convert options to JSON with userDataDir
371 let mut params = serde_json::to_value(&options).map_err(|e| {
372 crate::error::Error::ProtocolError(format!(
373 "Failed to serialize context options: {}",
374 e
375 ))
376 })?;
377
378 // Add userDataDir to params
379 params["userDataDir"] = serde_json::json!(user_data_dir.into());
380
381 // Set default timeout if not specified (required in Playwright 1.56.1+)
382 if params.get("timeout").is_none() {
383 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
384 }
385
386 // Convert bool ignoreDefaultArgs to ignoreAllDefaultArgs
387 // (matches playwright-python's parameter normalization)
388 if let Some(ignore) = params.get("ignoreDefaultArgs")
389 && let Some(b) = ignore.as_bool()
390 {
391 if b {
392 params["ignoreAllDefaultArgs"] = serde_json::json!(true);
393 }
394 params
395 .as_object_mut()
396 .expect("params is a JSON object")
397 .remove("ignoreDefaultArgs");
398 }
399
400 // Send launchPersistentContext RPC to server
401 let response: LaunchPersistentContextResponse = self
402 .base
403 .channel()
404 .send("launchPersistentContext", params)
405 .await?;
406
407 // Get context object from registry and downcast
408 let context: BrowserContext = self
409 .connection()
410 .get_typed::<BrowserContext>(&response.context.guid)
411 .await?;
412
413 // Register with Selectors coordinator
414 let selectors = self.connection().selectors();
415 if let Err(e) = selectors.add_context(context.channel().clone()).await {
416 tracing::warn!("Failed to register BrowserContext with Selectors: {}", e);
417 }
418
419 Ok(context)
420 }
421 /// Connects to an existing browser instance.
422 ///
423 /// # Arguments
424 /// * `ws_endpoint` - A WebSocket endpoint to connect to.
425 /// * `options` - Connection options.
426 ///
427 /// # Errors
428 /// Returns error if connection fails or handshake fails.
429 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name(), url = %ws_endpoint))]
430 pub async fn connect(
431 &self,
432 ws_endpoint: &str,
433 options: Option<ConnectOptions>,
434 ) -> Result<Browser> {
435 use crate::server::connection::Connection;
436 use crate::server::transport::WebSocketTransport;
437
438 let options = options.unwrap_or_default();
439
440 // Get timeout (default 30 seconds, 0 = no timeout)
441 let timeout_ms = options.timeout.unwrap_or(30000.0);
442
443 // 1. Connect to WebSocket
444 tracing::debug!("Connecting to remote browser at {}", ws_endpoint);
445
446 let connect_future = WebSocketTransport::connect(ws_endpoint, options.headers);
447 let (transport, message_rx) = if timeout_ms > 0.0 {
448 let timeout = std::time::Duration::from_millis(timeout_ms as u64);
449 tokio::time::timeout(timeout, connect_future)
450 .await
451 .map_err(|_| {
452 crate::error::Error::Timeout(format!(
453 "Connection to {} timed out after {} ms",
454 ws_endpoint, timeout_ms
455 ))
456 })??
457 } else {
458 connect_future.await?
459 };
460 let (sender, receiver) = transport.into_parts();
461
462 // 2. Create Connection
463 let connection = Arc::new(Connection::new(sender, receiver, message_rx));
464
465 // 3. Start message loop
466 let conn_for_loop = Arc::clone(&connection);
467 tokio::spawn(
468 async move {
469 conn_for_loop.run().await;
470 }
471 .in_current_span(),
472 );
473
474 // 4. Initialize Playwright
475 // This exchanges the "initialize" message and returns the root Playwright object
476 let playwright_obj = connection.initialize_playwright().await?;
477
478 // 5. Get pre-launched browser from initializer
479 // The server sends a "preLaunchedBrowser" field in the Playwright object's initializer
480 let initializer = playwright_obj.initializer();
481
482 let browser_guid = initializer["preLaunchedBrowser"]["guid"]
483 .as_str()
484 .ok_or_else(|| {
485 crate::error::Error::ProtocolError(
486 "Remote server did not return a pre-launched browser. Ensure server was launched in server mode.".to_string()
487 )
488 })?;
489
490 // 6. Get the existing Browser object and downcast
491 let browser: Browser = connection.get_typed::<Browser>(browser_guid).await?;
492
493 Ok(browser)
494 }
495
496 /// Connects to a browser over the Chrome DevTools Protocol.
497 ///
498 /// This method is only supported for Chromium. It connects to an existing Chrome
499 /// instance that exposes a CDP endpoint (e.g., `--remote-debugging-port`), or to
500 /// CDP-compatible services like browserless.
501 ///
502 /// Unlike `connect()`, which uses Playwright's proprietary WebSocket protocol,
503 /// this method connects directly via CDP. The Playwright server manages the CDP
504 /// connection internally.
505 ///
506 /// # Arguments
507 /// * `endpoint_url` - A CDP endpoint URL (e.g., `http://localhost:9222` or
508 /// `ws://localhost:9222/devtools/browser/...`)
509 /// * `options` - Optional connection options.
510 ///
511 /// # Errors
512 /// Returns error if:
513 /// - Called on a non-Chromium browser type
514 /// - Connection to the CDP endpoint fails
515 /// - Connection timeout
516 ///
517 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp>
518 #[tracing::instrument(level = "info", skip_all, fields(name = %self.name(), url = %endpoint_url))]
519 pub async fn connect_over_cdp(
520 &self,
521 endpoint_url: &str,
522 options: Option<ConnectOverCdpOptions>,
523 ) -> Result<Browser> {
524 // connect_over_cdp is Chromium-only
525 if self.name() != "chromium" {
526 return Err(crate::error::Error::ProtocolError(
527 "Connecting over CDP is only supported in Chromium.".to_string(),
528 ));
529 }
530
531 let options = options.unwrap_or_default();
532
533 // Convert headers from HashMap to array of {name, value} objects
534 let headers_array = options.headers.map(|h| {
535 h.into_iter()
536 .map(|(name, value)| HeaderEntry { name, value })
537 .collect::<Vec<_>>()
538 });
539
540 let params = ConnectOverCdpParams {
541 endpoint_url: endpoint_url.to_string(),
542 headers: headers_array,
543 slow_mo: options.slow_mo,
544 timeout: options.timeout.unwrap_or(crate::DEFAULT_TIMEOUT_MS),
545 };
546
547 // Send connectOverCDP RPC to the local Playwright server
548 let response: ConnectOverCdpResponse =
549 self.base.channel().send("connectOverCDP", params).await?;
550
551 // Get browser object from registry and downcast
552 let browser: Browser = self
553 .connection()
554 .get_typed::<Browser>(&response.browser.guid)
555 .await?;
556
557 Ok(browser)
558 }
559}
560
561/// Response from BrowserType.launch() protocol call
562#[derive(Debug, Deserialize, Serialize)]
563struct LaunchResponse {
564 browser: BrowserRef,
565}
566
567/// Response from BrowserType.launchPersistentContext() protocol call
568#[derive(Debug, Deserialize, Serialize)]
569struct LaunchPersistentContextResponse {
570 context: ContextRef,
571}
572
573/// Reference to a Browser object in the protocol
574#[derive(Debug, Deserialize, Serialize)]
575struct BrowserRef {
576 #[serde(
577 serialize_with = "crate::server::connection::serialize_arc_str",
578 deserialize_with = "crate::server::connection::deserialize_arc_str"
579 )]
580 guid: Arc<str>,
581}
582
583/// Reference to a BrowserContext object in the protocol
584#[derive(Debug, Deserialize, Serialize)]
585struct ContextRef {
586 #[serde(
587 serialize_with = "crate::server::connection::serialize_arc_str",
588 deserialize_with = "crate::server::connection::deserialize_arc_str"
589 )]
590 guid: Arc<str>,
591}
592
593/// Parameters for BrowserType.connectOverCDP() protocol call
594#[derive(Debug, Serialize)]
595struct ConnectOverCdpParams {
596 #[serde(rename = "endpointURL")]
597 endpoint_url: String,
598 #[serde(skip_serializing_if = "Option::is_none")]
599 headers: Option<Vec<HeaderEntry>>,
600 #[serde(rename = "slowMo", skip_serializing_if = "Option::is_none")]
601 slow_mo: Option<f64>,
602 timeout: f64,
603}
604
605/// A single HTTP header as {name, value} for the connectOverCDP protocol
606#[derive(Debug, Serialize)]
607struct HeaderEntry {
608 name: String,
609 value: String,
610}
611
612/// Response from BrowserType.connectOverCDP() protocol call
613#[derive(Debug, Deserialize)]
614struct ConnectOverCdpResponse {
615 browser: BrowserRef,
616 #[serde(rename = "defaultContext")]
617 #[allow(dead_code)]
618 default_context: Option<ContextRef>,
619}
620
621impl ChannelOwner for BrowserType {
622 fn guid(&self) -> &str {
623 self.base.guid()
624 }
625
626 fn type_name(&self) -> &str {
627 self.base.type_name()
628 }
629
630 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
631 self.base.parent()
632 }
633
634 fn connection(&self) -> Arc<dyn ConnectionLike> {
635 self.base.connection()
636 }
637
638 fn initializer(&self) -> &Value {
639 self.base.initializer()
640 }
641
642 fn channel(&self) -> &Channel {
643 self.base.channel()
644 }
645
646 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
647 self.base.dispose(reason)
648 }
649
650 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
651 self.base.adopt(child)
652 }
653
654 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
655 self.base.add_child(guid, child)
656 }
657
658 fn remove_child(&self, guid: &str) {
659 self.base.remove_child(guid)
660 }
661
662 fn on_event(&self, method: &str, params: Value) {
663 self.base.on_event(method, params)
664 }
665
666 fn was_collected(&self) -> bool {
667 self.base.was_collected()
668 }
669
670 fn as_any(&self) -> &dyn Any {
671 self
672 }
673}
674
675impl std::fmt::Debug for BrowserType {
676 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677 f.debug_struct("BrowserType")
678 .field("guid", &self.guid())
679 .field("name", &self.name)
680 .field("executable_path", &self.executable_path)
681 .finish()
682 }
683}
684
685// Note: BrowserType testing is done via integration tests since it requires:
686// - A real Connection with object registry
687// - Protocol messages from the server
688// See: crates/playwright-core/tests/connection_integration.rs