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