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 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)