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