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