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