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