playwright_rs/protocol/browser.rs
1// Browser protocol object
2//
3// Represents a browser instance created by BrowserType.launch()
4
5use crate::error::Result;
6use crate::protocol::{BrowserContext, Page};
7use crate::server::channel::Channel;
8use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
9use serde::Deserialize;
10use serde_json::Value;
11use std::any::Any;
12use std::sync::Arc;
13
14use std::sync::atomic::{AtomicBool, Ordering};
15
16/// Browser represents a browser instance.
17///
18/// A Browser is created when you call `BrowserType::launch()`. It provides methods
19/// to create browser contexts and pages.
20///
21/// # Example
22///
23/// ```ignore
24/// use playwright_rs::protocol::Playwright;
25///
26/// #[tokio::main]
27/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
28/// let playwright = Playwright::launch().await?;
29/// let chromium = playwright.chromium();
30///
31/// // Launch browser and get info
32/// let browser = chromium.launch().await?;
33/// println!("Browser: {} version {}", browser.name(), browser.version());
34///
35/// // Check connection status
36/// assert!(browser.is_connected());
37///
38/// // Create and use contexts and pages
39/// let context = browser.new_context().await?;
40/// let page = context.new_page().await?;
41///
42/// // Convenience: create page directly (auto-creates default context)
43/// let page2 = browser.new_page().await?;
44///
45/// // Cleanup
46/// browser.close().await?;
47/// assert!(!browser.is_connected());
48/// Ok(())
49/// }
50/// ```
51///
52/// See: <https://playwright.dev/docs/api/class-browser>
53#[derive(Clone)]
54pub struct Browser {
55 base: ChannelOwnerImpl,
56 version: String,
57 name: String,
58 is_connected: Arc<AtomicBool>,
59}
60
61impl Browser {
62 /// Creates a new Browser from protocol initialization
63 ///
64 /// This is called by the object factory when the server sends a `__create__` message
65 /// for a Browser object.
66 ///
67 /// # Arguments
68 ///
69 /// * `parent` - The parent BrowserType object
70 /// * `type_name` - The protocol type name ("Browser")
71 /// * `guid` - The unique identifier for this browser instance
72 /// * `initializer` - The initialization data from the server
73 ///
74 /// # Errors
75 ///
76 /// Returns error if initializer is missing required fields (version, name)
77 pub fn new(
78 parent: Arc<dyn ChannelOwner>,
79 type_name: String,
80 guid: Arc<str>,
81 initializer: Value,
82 ) -> Result<Self> {
83 let base = ChannelOwnerImpl::new(
84 ParentOrConnection::Parent(parent),
85 type_name,
86 guid,
87 initializer.clone(),
88 );
89
90 let version = initializer["version"]
91 .as_str()
92 .ok_or_else(|| {
93 crate::error::Error::ProtocolError(
94 "Browser initializer missing 'version' field".to_string(),
95 )
96 })?
97 .to_string();
98
99 let name = initializer["name"]
100 .as_str()
101 .ok_or_else(|| {
102 crate::error::Error::ProtocolError(
103 "Browser initializer missing 'name' field".to_string(),
104 )
105 })?
106 .to_string();
107
108 Ok(Self {
109 base,
110 version,
111 name,
112 is_connected: Arc::new(AtomicBool::new(true)),
113 })
114 }
115
116 /// Returns the browser version string.
117 ///
118 /// See: <https://playwright.dev/docs/api/class-browser#browser-version>
119 pub fn version(&self) -> &str {
120 &self.version
121 }
122
123 /// Returns the browser name (e.g., "chromium", "firefox", "webkit").
124 ///
125 /// See: <https://playwright.dev/docs/api/class-browser#browser-name>
126 pub fn name(&self) -> &str {
127 &self.name
128 }
129
130 /// Returns true if the browser is connected.
131 ///
132 /// The browser is connected when it is launched and becomes disconnected when:
133 /// - `browser.close()` is called
134 /// - The browser process crashes
135 /// - The browser is closed by the user
136 ///
137 /// See: <https://playwright.dev/docs/api/class-browser#browser-is-connected>
138 pub fn is_connected(&self) -> bool {
139 self.is_connected.load(Ordering::SeqCst)
140 }
141
142 /// Returns the channel for sending protocol messages
143 ///
144 /// Used internally for sending RPC calls to the browser.
145 fn channel(&self) -> &Channel {
146 self.base.channel()
147 }
148
149 /// Creates a new browser context.
150 ///
151 /// A browser context is an isolated session within the browser instance,
152 /// similar to an incognito profile. Each context has its own cookies,
153 /// cache, and local storage.
154 ///
155 /// # Errors
156 ///
157 /// Returns error if:
158 /// - Browser has been closed
159 /// - Communication with browser process fails
160 ///
161 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
162 pub async fn new_context(&self) -> Result<BrowserContext> {
163 // Response contains the GUID of the created BrowserContext
164 #[derive(Deserialize)]
165 struct NewContextResponse {
166 context: GuidRef,
167 }
168
169 #[derive(Deserialize)]
170 struct GuidRef {
171 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
172 guid: Arc<str>,
173 }
174
175 // Send newContext RPC to server with empty options for now
176 let response: NewContextResponse = self
177 .channel()
178 .send("newContext", serde_json::json!({}))
179 .await?;
180
181 // Retrieve the BrowserContext object from the connection registry
182 let context_arc = self.connection().get_object(&response.context.guid).await?;
183
184 // Downcast to BrowserContext
185 let context = context_arc
186 .as_any()
187 .downcast_ref::<BrowserContext>()
188 .ok_or_else(|| {
189 crate::error::Error::ProtocolError(format!(
190 "Expected BrowserContext object, got {}",
191 context_arc.type_name()
192 ))
193 })?;
194
195 Ok(context.clone())
196 }
197
198 /// Creates a new browser context with custom options.
199 ///
200 /// A browser context is an isolated session within the browser instance,
201 /// similar to an incognito profile. Each context has its own cookies,
202 /// cache, and local storage.
203 ///
204 /// This method allows customizing viewport, user agent, locale, timezone,
205 /// and other settings.
206 ///
207 /// # Errors
208 ///
209 /// Returns error if:
210 /// - Browser has been closed
211 /// - Communication with browser process fails
212 /// - Invalid options provided
213 /// - Storage state file cannot be read or parsed
214 ///
215 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
216 pub async fn new_context_with_options(
217 &self,
218 mut options: crate::protocol::BrowserContextOptions,
219 ) -> Result<BrowserContext> {
220 // Response contains the GUID of the created BrowserContext
221 #[derive(Deserialize)]
222 struct NewContextResponse {
223 context: GuidRef,
224 }
225
226 #[derive(Deserialize)]
227 struct GuidRef {
228 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
229 guid: Arc<str>,
230 }
231
232 // Handle storage_state_path: read file and convert to inline storage_state
233 if let Some(path) = &options.storage_state_path {
234 let file_content = tokio::fs::read_to_string(path).await.map_err(|e| {
235 crate::error::Error::ProtocolError(format!(
236 "Failed to read storage state file '{}': {}",
237 path, e
238 ))
239 })?;
240
241 let storage_state: crate::protocol::StorageState = serde_json::from_str(&file_content)
242 .map_err(|e| {
243 crate::error::Error::ProtocolError(format!(
244 "Failed to parse storage state file '{}': {}",
245 path, e
246 ))
247 })?;
248
249 options.storage_state = Some(storage_state);
250 options.storage_state_path = None; // Clear path since we've converted to inline
251 }
252
253 // Convert options to JSON
254 let options_json = serde_json::to_value(options).map_err(|e| {
255 crate::error::Error::ProtocolError(format!(
256 "Failed to serialize context options: {}",
257 e
258 ))
259 })?;
260
261 // Send newContext RPC to server with options
262 let response: NewContextResponse = self.channel().send("newContext", options_json).await?;
263
264 // Retrieve the BrowserContext object from the connection registry
265 let context_arc = self.connection().get_object(&response.context.guid).await?;
266
267 // Downcast to BrowserContext
268 let context = context_arc
269 .as_any()
270 .downcast_ref::<BrowserContext>()
271 .ok_or_else(|| {
272 crate::error::Error::ProtocolError(format!(
273 "Expected BrowserContext object, got {}",
274 context_arc.type_name()
275 ))
276 })?;
277
278 Ok(context.clone())
279 }
280
281 /// Creates a new page in a new browser context.
282 ///
283 /// This is a convenience method that creates a default context and then
284 /// creates a page in it. This is equivalent to calling `browser.new_context().await?.new_page().await?`.
285 ///
286 /// The created context is not directly accessible, but will be cleaned up
287 /// when the page is closed.
288 ///
289 /// # Errors
290 ///
291 /// Returns error if:
292 /// - Browser has been closed
293 /// - Communication with browser process fails
294 ///
295 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-page>
296 pub async fn new_page(&self) -> Result<Page> {
297 // Create a default context and then create a page in it
298 let context = self.new_context().await?;
299 context.new_page().await
300 }
301
302 /// Closes the browser and all of its pages (if any were opened).
303 ///
304 /// This is a graceful operation that sends a close command to the browser
305 /// and waits for it to shut down properly.
306 ///
307 /// # Errors
308 ///
309 /// Returns error if:
310 /// - Browser has already been closed
311 /// - Communication with browser process fails
312 ///
313 /// See: <https://playwright.dev/docs/api/class-browser#browser-close>
314 pub async fn close(&self) -> Result<()> {
315 // Send close RPC to server
316 // The protocol expects an empty object as params
317 let result = self
318 .channel()
319 .send_no_result("close", serde_json::json!({}))
320 .await;
321
322 // Add delay on Windows CI to ensure browser process fully terminates
323 // This prevents subsequent browser launches from hanging
324 #[cfg(windows)]
325 {
326 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
327 if is_ci {
328 tracing::debug!("[playwright-rust] Adding Windows CI browser cleanup delay");
329 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
330 }
331 }
332
333 result
334 }
335}
336
337impl ChannelOwner for Browser {
338 fn guid(&self) -> &str {
339 self.base.guid()
340 }
341
342 fn type_name(&self) -> &str {
343 self.base.type_name()
344 }
345
346 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
347 self.base.parent()
348 }
349
350 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
351 self.base.connection()
352 }
353
354 fn initializer(&self) -> &Value {
355 self.base.initializer()
356 }
357
358 fn channel(&self) -> &Channel {
359 self.base.channel()
360 }
361
362 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
363 self.is_connected.store(false, Ordering::SeqCst);
364 self.base.dispose(reason)
365 }
366
367 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
368 self.base.adopt(child)
369 }
370
371 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
372 self.base.add_child(guid, child)
373 }
374
375 fn remove_child(&self, guid: &str) {
376 self.base.remove_child(guid)
377 }
378
379 fn on_event(&self, method: &str, params: Value) {
380 if method == "disconnected" {
381 self.is_connected.store(false, Ordering::SeqCst);
382 }
383 self.base.on_event(method, params)
384 }
385
386 fn was_collected(&self) -> bool {
387 self.base.was_collected()
388 }
389
390 fn as_any(&self) -> &dyn Any {
391 self
392 }
393}
394
395impl std::fmt::Debug for Browser {
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 f.debug_struct("Browser")
398 .field("guid", &self.guid())
399 .field("name", &self.name)
400 .field("version", &self.version)
401 .finish()
402 }
403}
404
405// Note: Browser testing is done via integration tests since it requires:
406// - A real Connection with object registry
407// - Protocol messages from the server
408// - BrowserType.launch() to create Browser objects
409// See: crates/playwright-core/tests/browser_launch_integration.rs (Phase 2 Slice 3)