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