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 ///
214 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
215 pub async fn new_context_with_options(
216 &self,
217 options: crate::protocol::BrowserContextOptions,
218 ) -> Result<BrowserContext> {
219 // Response contains the GUID of the created BrowserContext
220 #[derive(Deserialize)]
221 struct NewContextResponse {
222 context: GuidRef,
223 }
224
225 #[derive(Deserialize)]
226 struct GuidRef {
227 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
228 guid: Arc<str>,
229 }
230
231 // Convert options to JSON
232 let options_json = serde_json::to_value(options).map_err(|e| {
233 crate::error::Error::ProtocolError(format!(
234 "Failed to serialize context options: {}",
235 e
236 ))
237 })?;
238
239 // Send newContext RPC to server with options
240 let response: NewContextResponse = self.channel().send("newContext", options_json).await?;
241
242 // Retrieve the BrowserContext object from the connection registry
243 let context_arc = self.connection().get_object(&response.context.guid).await?;
244
245 // Downcast to BrowserContext
246 let context = context_arc
247 .as_any()
248 .downcast_ref::<BrowserContext>()
249 .ok_or_else(|| {
250 crate::error::Error::ProtocolError(format!(
251 "Expected BrowserContext object, got {}",
252 context_arc.type_name()
253 ))
254 })?;
255
256 Ok(context.clone())
257 }
258
259 /// Creates a new page in a new browser context.
260 ///
261 /// This is a convenience method that creates a default context and then
262 /// creates a page in it. This is equivalent to calling `browser.new_context().await?.new_page().await?`.
263 ///
264 /// The created context is not directly accessible, but will be cleaned up
265 /// when the page is closed.
266 ///
267 /// # Errors
268 ///
269 /// Returns error if:
270 /// - Browser has been closed
271 /// - Communication with browser process fails
272 ///
273 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-page>
274 pub async fn new_page(&self) -> Result<Page> {
275 // Create a default context and then create a page in it
276 let context = self.new_context().await?;
277 context.new_page().await
278 }
279
280 /// Closes the browser and all of its pages (if any were opened).
281 ///
282 /// This is a graceful operation that sends a close command to the browser
283 /// and waits for it to shut down properly.
284 ///
285 /// # Errors
286 ///
287 /// Returns error if:
288 /// - Browser has already been closed
289 /// - Communication with browser process fails
290 ///
291 /// See: <https://playwright.dev/docs/api/class-browser#browser-close>
292 pub async fn close(&self) -> Result<()> {
293 // Send close RPC to server
294 // The protocol expects an empty object as params
295 let result = self
296 .channel()
297 .send_no_result("close", serde_json::json!({}))
298 .await;
299
300 // Add delay on Windows CI to ensure browser process fully terminates
301 // This prevents subsequent browser launches from hanging
302 #[cfg(windows)]
303 {
304 let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
305 if is_ci {
306 eprintln!("[playwright-rust] Adding Windows CI browser cleanup delay");
307 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
308 }
309 }
310
311 result
312 }
313}
314
315impl ChannelOwner for Browser {
316 fn guid(&self) -> &str {
317 self.base.guid()
318 }
319
320 fn type_name(&self) -> &str {
321 self.base.type_name()
322 }
323
324 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
325 self.base.parent()
326 }
327
328 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
329 self.base.connection()
330 }
331
332 fn initializer(&self) -> &Value {
333 self.base.initializer()
334 }
335
336 fn channel(&self) -> &Channel {
337 self.base.channel()
338 }
339
340 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
341 self.is_connected.store(false, Ordering::SeqCst);
342 self.base.dispose(reason)
343 }
344
345 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
346 self.base.adopt(child)
347 }
348
349 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
350 self.base.add_child(guid, child)
351 }
352
353 fn remove_child(&self, guid: &str) {
354 self.base.remove_child(guid)
355 }
356
357 fn on_event(&self, method: &str, params: Value) {
358 if method == "disconnected" {
359 self.is_connected.store(false, Ordering::SeqCst);
360 }
361 self.base.on_event(method, params)
362 }
363
364 fn was_collected(&self) -> bool {
365 self.base.was_collected()
366 }
367
368 fn as_any(&self) -> &dyn Any {
369 self
370 }
371}
372
373impl std::fmt::Debug for Browser {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 f.debug_struct("Browser")
376 .field("guid", &self.guid())
377 .field("name", &self.name)
378 .field("version", &self.version)
379 .finish()
380 }
381}
382
383// Note: Browser testing is done via integration tests since it requires:
384// - A real Connection with object registry
385// - Protocol messages from the server
386// - BrowserType.launch() to create Browser objects
387// See: crates/playwright-core/tests/browser_launch_integration.rs (Phase 2 Slice 3)