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;
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/// # Example
29///
30/// ```ignore
31/// # use playwright_rs::protocol::Playwright;
32/// # use playwright_rs::api::LaunchOptions;
33/// # #[tokio::main]
34/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
35/// let playwright = Playwright::launch().await?;
36/// let chromium = playwright.chromium();
37///
38/// // Verify browser type info
39/// assert_eq!(chromium.name(), "chromium");
40/// assert!(!chromium.executable_path().is_empty());
41///
42/// // Launch with default options
43/// let browser1 = chromium.launch().await?;
44/// assert_eq!(browser1.name(), "chromium");
45/// assert!(!browser1.version().is_empty());
46/// browser1.close().await?;
47///
48/// // Launch with custom options
49/// let options = LaunchOptions::default()
50///     .headless(true)
51///     .slow_mo(100.0)
52///     .args(vec!["--no-sandbox".to_string()]);
53///
54/// let browser2 = chromium.launch_with_options(options).await?;
55/// assert_eq!(browser2.name(), "chromium");
56/// assert!(!browser2.version().is_empty());
57/// browser2.close().await?;
58/// # Ok(())
59/// # }
60/// ```
61///
62/// See: <https://playwright.dev/docs/api/class-browsertype>
63pub struct BrowserType {
64    /// Base ChannelOwner implementation
65    base: ChannelOwnerImpl,
66    /// Browser name ("chromium", "firefox", or "webkit")
67    name: String,
68    /// Path to browser executable
69    executable_path: String,
70}
71
72impl BrowserType {
73    /// Creates a new BrowserType object from protocol initialization.
74    ///
75    /// Called by the object factory when server sends __create__ message.
76    ///
77    /// # Arguments
78    /// * `parent` - Parent Playwright object
79    /// * `type_name` - Protocol type name ("BrowserType")
80    /// * `guid` - Unique GUID from server (e.g., "browserType@chromium")
81    /// * `initializer` - Initial state with name and executablePath
82    pub fn new(
83        parent: Arc<dyn ChannelOwner>,
84        type_name: String,
85        guid: Arc<str>,
86        initializer: Value,
87    ) -> Result<Self> {
88        let base = ChannelOwnerImpl::new(
89            ParentOrConnection::Parent(parent),
90            type_name,
91            guid,
92            initializer.clone(),
93        );
94
95        // Extract fields from initializer
96        let name = initializer["name"]
97            .as_str()
98            .ok_or_else(|| {
99                crate::error::Error::ProtocolError(
100                    "BrowserType initializer missing 'name'".to_string(),
101                )
102            })?
103            .to_string();
104
105        let executable_path = initializer["executablePath"]
106            .as_str()
107            .ok_or_else(|| {
108                crate::error::Error::ProtocolError(
109                    "BrowserType initializer missing 'executablePath'".to_string(),
110                )
111            })?
112            .to_string();
113
114        Ok(Self {
115            base,
116            name,
117            executable_path,
118        })
119    }
120
121    /// Returns the browser name ("chromium", "firefox", or "webkit").
122    pub fn name(&self) -> &str {
123        &self.name
124    }
125
126    /// Returns the path to the browser executable.
127    pub fn executable_path(&self) -> &str {
128        &self.executable_path
129    }
130
131    /// Launches a browser instance with default options.
132    ///
133    /// This is equivalent to calling `launch_with_options(LaunchOptions::default())`.
134    ///
135    /// # Errors
136    ///
137    /// Returns error if:
138    /// - Browser executable not found
139    /// - Launch timeout (default 30s)
140    /// - Browser process fails to start
141    ///
142    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
143    pub async fn launch(&self) -> Result<Browser> {
144        self.launch_with_options(LaunchOptions::default()).await
145    }
146
147    /// Launches a browser instance with custom options.
148    ///
149    /// # Arguments
150    ///
151    /// * `options` - Launch options (headless, args, etc.)
152    ///
153    /// # Errors
154    ///
155    /// Returns error if:
156    /// - Browser executable not found
157    /// - Launch timeout
158    /// - Invalid options
159    /// - Browser process fails to start
160    ///
161    /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch>
162    pub async fn launch_with_options(&self, options: LaunchOptions) -> Result<Browser> {
163        // Add Windows CI-specific browser args to prevent hanging
164        let options = {
165            #[cfg(windows)]
166            {
167                let mut options = options;
168                // Check if we're in a CI environment (GitHub Actions, Jenkins, etc.)
169                let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
170
171                if is_ci {
172                    eprintln!(
173                        "[playwright-rust] Detected Windows CI environment, adding stability flags"
174                    );
175
176                    // Get existing args or create empty vec
177                    let mut args = options.args.unwrap_or_default();
178
179                    // Add Windows CI stability flags if not already present
180                    let ci_flags = vec![
181                        "--no-sandbox",            // Disable sandboxing (often problematic in CI)
182                        "--disable-dev-shm-usage", // Overcome limited /dev/shm resources
183                        "--disable-gpu",           // Disable GPU hardware acceleration
184                        "--disable-web-security",  // Avoid CORS issues in CI
185                        "--disable-features=IsolateOrigins,site-per-process", // Reduce process overhead
186                    ];
187
188                    for flag in ci_flags {
189                        if !args.iter().any(|a| a == flag) {
190                            args.push(flag.to_string());
191                        }
192                    }
193
194                    // Update options with enhanced args
195                    options.args = Some(args);
196
197                    // Increase timeout for Windows CI (slower startup)
198                    if options.timeout.is_none() {
199                        options.timeout = Some(60000.0); // 60 seconds for Windows CI
200                    }
201                }
202                options
203            }
204
205            #[cfg(not(windows))]
206            {
207                options
208            }
209        };
210
211        // Normalize options for protocol transmission
212        let params = options.normalize();
213
214        // Send launch RPC to server
215        let response: LaunchResponse = self.base.channel().send("launch", params).await?;
216
217        // Get browser object from registry
218        let browser_arc = self.connection().get_object(&response.browser.guid).await?;
219
220        // Downcast to Browser
221        let browser = browser_arc
222            .as_any()
223            .downcast_ref::<Browser>()
224            .ok_or_else(|| {
225                crate::error::Error::ProtocolError(format!(
226                    "Expected Browser object, got {}",
227                    browser_arc.type_name()
228                ))
229            })?;
230
231        Ok(browser.clone())
232    }
233}
234
235/// Response from BrowserType.launch() protocol call
236#[derive(Debug, Deserialize, Serialize)]
237struct LaunchResponse {
238    browser: BrowserRef,
239}
240
241/// Reference to a Browser object in the protocol
242#[derive(Debug, Deserialize, Serialize)]
243struct BrowserRef {
244    #[serde(
245        serialize_with = "crate::server::connection::serialize_arc_str",
246        deserialize_with = "crate::server::connection::deserialize_arc_str"
247    )]
248    guid: Arc<str>,
249}
250
251impl ChannelOwner for BrowserType {
252    fn guid(&self) -> &str {
253        self.base.guid()
254    }
255
256    fn type_name(&self) -> &str {
257        self.base.type_name()
258    }
259
260    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
261        self.base.parent()
262    }
263
264    fn connection(&self) -> Arc<dyn ConnectionLike> {
265        self.base.connection()
266    }
267
268    fn initializer(&self) -> &Value {
269        self.base.initializer()
270    }
271
272    fn channel(&self) -> &Channel {
273        self.base.channel()
274    }
275
276    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
277        self.base.dispose(reason)
278    }
279
280    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
281        self.base.adopt(child)
282    }
283
284    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
285        self.base.add_child(guid, child)
286    }
287
288    fn remove_child(&self, guid: &str) {
289        self.base.remove_child(guid)
290    }
291
292    fn on_event(&self, method: &str, params: Value) {
293        self.base.on_event(method, params)
294    }
295
296    fn was_collected(&self) -> bool {
297        self.base.was_collected()
298    }
299
300    fn as_any(&self) -> &dyn Any {
301        self
302    }
303}
304
305impl std::fmt::Debug for BrowserType {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        f.debug_struct("BrowserType")
308            .field("guid", &self.guid())
309            .field("name", &self.name)
310            .field("executable_path", &self.executable_path)
311            .finish()
312    }
313}
314
315// Note: BrowserType testing is done via integration tests since it requires:
316// - A real Connection with object registry
317// - Protocol messages from the server
318// See: crates/playwright-core/tests/connection_integration.rs