playwright_core/protocol/
browser.rs

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