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