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)