playwright_rs/protocol/browser_type.rs
1// Copyright 2024 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::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 two main launching modes:
29/// 1. **Standard launch**: Creates a browser, then contexts and pages separately
30/// 2. **Persistent context launch**: Creates browser + context together with persistent storage
31///
32/// # Example
33///
34/// ```ignore
35/// # use playwright_rs::protocol::Playwright;
36/// # use playwright_rs::api::LaunchOptions;
37/// # use playwright_rs::protocol::BrowserContextOptions;
38/// # #[tokio::main]
39/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
40/// let playwright = Playwright::launch().await?;
41/// let chromium = playwright.chromium();
42///
43/// // Verify browser type info
44/// assert_eq!(chromium.name(), "chromium");
45/// assert!(!chromium.executable_path().is_empty());
46///
47/// // === Standard Launch ===
48/// // Launch with default options
49/// let browser1 = chromium.launch().await?;
50/// assert_eq!(browser1.name(), "chromium");
51/// assert!(!browser1.version().is_empty());
52/// browser1.close().await?;
53///
54/// // Launch with custom options
55/// let options = LaunchOptions::default()
56/// .headless(true)
57/// .slow_mo(100.0)
58/// .args(vec!["--no-sandbox".to_string()]);
59///
60/// let browser2 = chromium.launch_with_options(options).await?;
61/// assert_eq!(browser2.name(), "chromium");
62/// assert!(!browser2.version().is_empty());
63/// browser2.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 Playwright object
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: Arc<dyn ChannelOwner>,
112 type_name: String,
113 guid: Arc<str>,
114 initializer: Value,
115 ) -> Result<Self> {
116 let base = ChannelOwnerImpl::new(
117 ParentOrConnection::Parent(parent),
118 type_name,
119 guid,
120 initializer.clone(),
121 );
122
123 // Extract fields from initializer
124 let name = initializer["name"]
125 .as_str()
126 .ok_or_else(|| {
127 crate::error::Error::ProtocolError(
128 "BrowserType initializer missing 'name'".to_string(),
129 )
130 })?
131 .to_string();
132
133 let executable_path = initializer["executablePath"]
134 .as_str()
135 .ok_or_else(|| {
136 crate::error::Error::ProtocolError(
137 "BrowserType initializer missing 'executablePath'".to_string(),
138 )
139 })?
140 .to_string();
141
142 Ok(Self {
143 base,
144 name,
145 executable_path,
146 })
147 }
148
149 /// Returns the browser name ("chromium", "firefox", or "webkit").
150 pub fn name(&self) -> &str {
151 &self.name
152 }
153
154 /// Returns the path to the browser executable.
155 pub fn executable_path(&self) -> &str {
156 &self.executable_path
157 }
158
159 /// Launches a browser instance with default options.
160 ///
161 /// This is equivalent to calling `launch_with_options(LaunchOptions::default())`.
162 ///
163 /// # Errors
164 ///
165 /// Returns error if:
166 /// - Browser executable not found
167 /// - Launch timeout (default 30s)
168 /// - Browser process fails to start
169 ///
170 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
171 pub async fn launch(&self) -> Result<Browser> {
172 self.launch_with_options(LaunchOptions::default()).await
173 }
174
175 /// Launches a browser instance with custom options.
176 ///
177 /// # Arguments
178 ///
179 /// * `options` - Launch options (headless, args, etc.)
180 ///
181 /// # Errors
182 ///
183 /// Returns error if:
184 /// - Browser executable not found
185 /// - Launch timeout
186 /// - Invalid options
187 /// - Browser process fails to start
188 ///
189 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
190 pub async fn launch_with_options(&self, options: LaunchOptions) -> Result<Browser> {
191 // Add Windows CI-specific browser args to prevent hanging
192 let options = {
193 #[cfg(windows)]
194 {
195 let mut options = options;
196 // Check if we're in a CI environment (GitHub Actions, Jenkins, etc.)
197 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
198
199 if is_ci {
200 tracing::debug!(
201 "[playwright-rust] Detected Windows CI environment, adding stability flags"
202 );
203
204 // Get existing args or create empty vec
205 let mut args = options.args.unwrap_or_default();
206
207 // Add Windows CI stability flags if not already present
208 let ci_flags = vec![
209 "--no-sandbox", // Disable sandboxing (often problematic in CI)
210 "--disable-dev-shm-usage", // Overcome limited /dev/shm resources
211 "--disable-gpu", // Disable GPU hardware acceleration
212 "--disable-web-security", // Avoid CORS issues in CI
213 "--disable-features=IsolateOrigins,site-per-process", // Reduce process overhead
214 ];
215
216 for flag in ci_flags {
217 if !args.iter().any(|a| a == flag) {
218 args.push(flag.to_string());
219 }
220 }
221
222 // Update options with enhanced args
223 options.args = Some(args);
224
225 // Increase timeout for Windows CI (slower startup)
226 if options.timeout.is_none() {
227 options.timeout = Some(60000.0); // 60 seconds for Windows CI
228 }
229 }
230 options
231 }
232
233 #[cfg(not(windows))]
234 {
235 options
236 }
237 };
238
239 // Normalize options for protocol transmission
240 let params = options.normalize();
241
242 // Send launch RPC to server
243 let response: LaunchResponse = self.base.channel().send("launch", params).await?;
244
245 // Get browser object from registry
246 let browser_arc = self.connection().get_object(&response.browser.guid).await?;
247
248 // Downcast to Browser
249 let browser = browser_arc
250 .as_any()
251 .downcast_ref::<Browser>()
252 .ok_or_else(|| {
253 crate::error::Error::ProtocolError(format!(
254 "Expected Browser object, got {}",
255 browser_arc.type_name()
256 ))
257 })?;
258
259 Ok(browser.clone())
260 }
261
262 /// Launches a browser with persistent storage using default options.
263 ///
264 /// Returns a persistent browser context. Closing this context will automatically
265 /// close the browser.
266 ///
267 /// This method is useful for:
268 /// - Preserving authentication state across sessions
269 /// - Testing with real user profiles
270 /// - Creating standalone applications with app mode
271 /// - Simulating real user behavior with cookies and storage
272 ///
273 /// # Arguments
274 ///
275 /// * `user_data_dir` - Path to a user data directory (stores cookies, local storage)
276 ///
277 /// # Errors
278 ///
279 /// Returns error if:
280 /// - Browser executable not found
281 /// - Launch timeout (default 30s)
282 /// - Browser process fails to start
283 /// - User data directory cannot be created
284 ///
285 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
286 pub async fn launch_persistent_context(
287 &self,
288 user_data_dir: impl Into<String>,
289 ) -> Result<BrowserContext> {
290 self.launch_persistent_context_with_options(user_data_dir, BrowserContextOptions::default())
291 .await
292 }
293
294 /// Launches a browser with persistent storage and custom options.
295 ///
296 /// Returns a persistent browser context with the specified configuration.
297 /// Closing this context will automatically close the browser.
298 ///
299 /// This method accepts both launch options (headless, args, etc.) and context
300 /// options (viewport, locale, etc.) in a single BrowserContextOptions struct.
301 ///
302 /// # Arguments
303 ///
304 /// * `user_data_dir` - Path to a user data directory (stores cookies, local storage)
305 /// * `options` - Combined launch and context options
306 ///
307 /// # Errors
308 ///
309 /// Returns error if:
310 /// - Browser executable not found
311 /// - Launch timeout
312 /// - Invalid options
313 /// - Browser process fails to start
314 /// - User data directory cannot be created
315 ///
316 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
317 pub async fn launch_persistent_context_with_options(
318 &self,
319 user_data_dir: impl Into<String>,
320 mut options: BrowserContextOptions,
321 ) -> Result<BrowserContext> {
322 // Add Windows CI-specific browser args to prevent hanging
323 #[cfg(windows)]
324 {
325 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
326
327 if is_ci {
328 tracing::debug!(
329 "[playwright-rust] Detected Windows CI environment, adding stability flags"
330 );
331
332 // Get existing args or create empty vec
333 let mut args = options.args.unwrap_or_default();
334
335 // Add Windows CI stability flags if not already present
336 let ci_flags = vec![
337 "--no-sandbox", // Disable sandboxing (often problematic in CI)
338 "--disable-dev-shm-usage", // Overcome limited /dev/shm resources
339 "--disable-gpu", // Disable GPU hardware acceleration
340 "--disable-web-security", // Avoid CORS issues in CI
341 "--disable-features=IsolateOrigins,site-per-process", // Reduce process overhead
342 ];
343
344 for flag in ci_flags {
345 if !args.iter().any(|a| a == flag) {
346 args.push(flag.to_string());
347 }
348 }
349
350 // Update options with enhanced args
351 options.args = Some(args);
352
353 // Increase timeout for Windows CI (slower startup)
354 if options.timeout.is_none() {
355 options.timeout = Some(60000.0); // 60 seconds for Windows CI
356 }
357 }
358 }
359
360 // Handle storage_state_path: read file and convert to inline storage_state
361 if let Some(path) = &options.storage_state_path {
362 let file_content = tokio::fs::read_to_string(path).await.map_err(|e| {
363 crate::error::Error::ProtocolError(format!(
364 "Failed to read storage state file '{}': {}",
365 path, e
366 ))
367 })?;
368
369 let storage_state: crate::protocol::StorageState = serde_json::from_str(&file_content)
370 .map_err(|e| {
371 crate::error::Error::ProtocolError(format!(
372 "Failed to parse storage state file '{}': {}",
373 path, e
374 ))
375 })?;
376
377 options.storage_state = Some(storage_state);
378 options.storage_state_path = None; // Clear path since we've converted to inline
379 }
380
381 // Convert options to JSON with userDataDir
382 let mut params = serde_json::to_value(&options).map_err(|e| {
383 crate::error::Error::ProtocolError(format!(
384 "Failed to serialize context options: {}",
385 e
386 ))
387 })?;
388
389 // Add userDataDir to params
390 params["userDataDir"] = serde_json::json!(user_data_dir.into());
391
392 // Set default timeout if not specified (required in Playwright 1.56.1+)
393 if params.get("timeout").is_none() {
394 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
395 }
396
397 // Send launchPersistentContext RPC to server
398 let response: LaunchPersistentContextResponse = self
399 .base
400 .channel()
401 .send("launchPersistentContext", params)
402 .await?;
403
404 // Get context object from registry
405 let context_arc = self.connection().get_object(&response.context.guid).await?;
406
407 // Downcast to BrowserContext
408 let context = context_arc
409 .as_any()
410 .downcast_ref::<BrowserContext>()
411 .ok_or_else(|| {
412 crate::error::Error::ProtocolError(format!(
413 "Expected BrowserContext object, got {}",
414 context_arc.type_name()
415 ))
416 })?;
417
418 Ok(context.clone())
419 }
420}
421
422/// Response from BrowserType.launch() protocol call
423#[derive(Debug, Deserialize, Serialize)]
424struct LaunchResponse {
425 browser: BrowserRef,
426}
427
428/// Response from BrowserType.launchPersistentContext() protocol call
429#[derive(Debug, Deserialize, Serialize)]
430struct LaunchPersistentContextResponse {
431 context: ContextRef,
432}
433
434/// Reference to a Browser object in the protocol
435#[derive(Debug, Deserialize, Serialize)]
436struct BrowserRef {
437 #[serde(
438 serialize_with = "crate::server::connection::serialize_arc_str",
439 deserialize_with = "crate::server::connection::deserialize_arc_str"
440 )]
441 guid: Arc<str>,
442}
443
444/// Reference to a BrowserContext object in the protocol
445#[derive(Debug, Deserialize, Serialize)]
446struct ContextRef {
447 #[serde(
448 serialize_with = "crate::server::connection::serialize_arc_str",
449 deserialize_with = "crate::server::connection::deserialize_arc_str"
450 )]
451 guid: Arc<str>,
452}
453
454impl ChannelOwner for BrowserType {
455 fn guid(&self) -> &str {
456 self.base.guid()
457 }
458
459 fn type_name(&self) -> &str {
460 self.base.type_name()
461 }
462
463 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
464 self.base.parent()
465 }
466
467 fn connection(&self) -> Arc<dyn ConnectionLike> {
468 self.base.connection()
469 }
470
471 fn initializer(&self) -> &Value {
472 self.base.initializer()
473 }
474
475 fn channel(&self) -> &Channel {
476 self.base.channel()
477 }
478
479 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
480 self.base.dispose(reason)
481 }
482
483 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
484 self.base.adopt(child)
485 }
486
487 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
488 self.base.add_child(guid, child)
489 }
490
491 fn remove_child(&self, guid: &str) {
492 self.base.remove_child(guid)
493 }
494
495 fn on_event(&self, method: &str, params: Value) {
496 self.base.on_event(method, params)
497 }
498
499 fn was_collected(&self) -> bool {
500 self.base.was_collected()
501 }
502
503 fn as_any(&self) -> &dyn Any {
504 self
505 }
506}
507
508impl std::fmt::Debug for BrowserType {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 f.debug_struct("BrowserType")
511 .field("guid", &self.guid())
512 .field("name", &self.name)
513 .field("executable_path", &self.executable_path)
514 .finish()
515 }
516}
517
518// Note: BrowserType testing is done via integration tests since it requires:
519// - A real Connection with object registry
520// - Protocol messages from the server
521// See: crates/playwright-core/tests/connection_integration.rs