Skip to main content

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)