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