Skip to main content

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