Skip to main content

playwright_rs/protocol/
playwright.rs

1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// Playwright - Root protocol object
5//
6// Reference:
7// - Python: playwright-python/playwright/_impl/_playwright.py
8// - Protocol: protocol.yml (Playwright interface)
9
10use crate::error::Result;
11use crate::protocol::BrowserType;
12use crate::protocol::device::DeviceDescriptor;
13use crate::protocol::selectors::Selectors;
14use crate::server::channel::Channel;
15use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
16use crate::server::connection::{ConnectionExt, ConnectionLike};
17use crate::server::playwright_server::PlaywrightServer;
18use parking_lot::Mutex;
19use serde_json::Value;
20use std::any::Any;
21use std::collections::HashMap;
22use std::sync::Arc;
23
24/// Playwright is the root object that provides access to browser types.
25///
26/// This is the main entry point for the Playwright API. It provides access to
27/// the three browser types (Chromium, Firefox, WebKit) and other top-level services.
28///
29/// # Example
30///
31/// ```ignore
32/// use playwright_rs::protocol::Playwright;
33///
34/// #[tokio::main]
35/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
36///     // Launch Playwright server and initialize
37///     let playwright = Playwright::launch().await?;
38///
39///     // Verify all three browser types are available
40///     let chromium = playwright.chromium();
41///     let firefox = playwright.firefox();
42///     let webkit = playwright.webkit();
43///
44///     assert_eq!(chromium.name(), "chromium");
45///     assert_eq!(firefox.name(), "firefox");
46///     assert_eq!(webkit.name(), "webkit");
47///
48///     // Verify we can launch a browser
49///     let browser = chromium.launch().await?;
50///     assert!(!browser.version().is_empty());
51///     browser.close().await?;
52///
53///     // Shutdown when done
54///     playwright.shutdown().await?;
55///
56///     Ok(())
57/// }
58/// ```
59///
60/// See: <https://playwright.dev/docs/api/class-playwright>
61#[derive(Clone)]
62pub struct Playwright {
63    /// Base ChannelOwner implementation
64    base: ChannelOwnerImpl,
65    /// Chromium browser type
66    chromium: BrowserType,
67    /// Firefox browser type
68    firefox: BrowserType,
69    /// WebKit browser type
70    webkit: BrowserType,
71    /// Playwright server process (for clean shutdown)
72    ///
73    /// Stored as `Option<PlaywrightServer>` wrapped in Arc<Mutex<>> to allow:
74    /// - Sharing across clones (Arc)
75    /// - Taking ownership during shutdown (Option::take)
76    /// - Interior mutability (Mutex)
77    server: Arc<Mutex<Option<PlaywrightServer>>>,
78    /// Device descriptors parsed from the initializer's `deviceDescriptors` array.
79    devices: HashMap<String, DeviceDescriptor>,
80}
81
82impl Playwright {
83    /// Launches Playwright and returns a handle to interact with browser types.
84    ///
85    /// This is the main entry point for the Playwright API. It will:
86    /// 1. Launch the Playwright server process
87    /// 2. Establish a connection via stdio
88    /// 3. Initialize the protocol
89    /// 4. Return a Playwright instance with access to browser types
90    ///
91    /// # Errors
92    ///
93    /// Returns error if:
94    /// - Playwright server is not found or fails to launch
95    /// - Connection to server fails
96    /// - Protocol initialization fails
97    /// - Server doesn't respond within timeout (30s)
98    pub async fn launch() -> Result<Self> {
99        use crate::server::connection::Connection;
100        use crate::server::playwright_server::PlaywrightServer;
101        use crate::server::transport::PipeTransport;
102
103        // 1. Launch Playwright server
104        tracing::debug!("Launching Playwright server");
105        let mut server = PlaywrightServer::launch().await?;
106
107        // 2. Take stdio streams from server process
108        let stdin = server.process.stdin.take().ok_or_else(|| {
109            crate::error::Error::ServerError("Failed to get server stdin".to_string())
110        })?;
111
112        let stdout = server.process.stdout.take().ok_or_else(|| {
113            crate::error::Error::ServerError("Failed to get server stdout".to_string())
114        })?;
115
116        // 3. Create transport and connection
117        tracing::debug!("Creating transport and connection");
118        let (transport, message_rx) = PipeTransport::new(stdin, stdout);
119        let (sender, receiver) = transport.into_parts();
120        let connection: Arc<Connection> = Arc::new(Connection::new(sender, receiver, message_rx));
121
122        // 4. Spawn connection message loop in background
123        let conn_for_loop: Arc<Connection> = Arc::clone(&connection);
124        tokio::spawn(async move {
125            conn_for_loop.run().await;
126        });
127
128        // 5. Initialize Playwright (sends initialize message, waits for Playwright object)
129        tracing::debug!("Initializing Playwright protocol");
130        let playwright_obj = connection.initialize_playwright().await?;
131
132        // 6. Downcast to Playwright type using get_typed
133        let guid = playwright_obj.guid().to_string();
134        let mut playwright: Playwright = connection.get_typed::<Playwright>(&guid).await?;
135
136        // Attach the server for clean shutdown
137        playwright.server = Arc::new(Mutex::new(Some(server)));
138
139        Ok(playwright)
140    }
141
142    /// Creates a new Playwright object from protocol initialization.
143    ///
144    /// Called by the object factory when server sends __create__ message for root object.
145    ///
146    /// # Arguments
147    /// * `connection` - The connection (Playwright is root, so no parent)
148    /// * `type_name` - Protocol type name ("Playwright")
149    /// * `guid` - Unique GUID from server (typically "playwright@1")
150    /// * `initializer` - Initial state with references to browser types
151    ///
152    /// # Initializer Format
153    ///
154    /// The initializer contains GUID references to BrowserType objects:
155    /// ```json
156    /// {
157    ///   "chromium": { "guid": "browserType@chromium" },
158    ///   "firefox": { "guid": "browserType@firefox" },
159    ///   "webkit": { "guid": "browserType@webkit" }
160    /// }
161    /// ```
162    ///
163    /// Note: `Selectors` is a pure client-side coordinator, not a protocol object.
164    /// It is created fresh here rather than looked up from the registry.
165    pub async fn new(
166        connection: Arc<dyn ConnectionLike>,
167        type_name: String,
168        guid: Arc<str>,
169        initializer: Value,
170    ) -> Result<Self> {
171        let base = ChannelOwnerImpl::new(
172            ParentOrConnection::Connection(connection.clone()),
173            type_name,
174            guid,
175            initializer.clone(),
176        );
177
178        // Extract BrowserType GUIDs from initializer
179        let chromium_guid = initializer["chromium"]["guid"].as_str().ok_or_else(|| {
180            crate::error::Error::ProtocolError(
181                "Playwright initializer missing 'chromium.guid'".to_string(),
182            )
183        })?;
184
185        let firefox_guid = initializer["firefox"]["guid"].as_str().ok_or_else(|| {
186            crate::error::Error::ProtocolError(
187                "Playwright initializer missing 'firefox.guid'".to_string(),
188            )
189        })?;
190
191        let webkit_guid = initializer["webkit"]["guid"].as_str().ok_or_else(|| {
192            crate::error::Error::ProtocolError(
193                "Playwright initializer missing 'webkit.guid'".to_string(),
194            )
195        })?;
196
197        // Get BrowserType objects from connection registry and downcast.
198        // Note: These objects should already exist (created by earlier __create__ messages).
199        let chromium: BrowserType = connection.get_typed::<BrowserType>(chromium_guid).await?;
200        let firefox: BrowserType = connection.get_typed::<BrowserType>(firefox_guid).await?;
201        let webkit: BrowserType = connection.get_typed::<BrowserType>(webkit_guid).await?;
202
203        // Selectors is a pure client-side coordinator stored in the connection.
204        // No need to create or store it here; access it via self.connection().selectors().
205
206        // Parse deviceDescriptors from LocalUtils.
207        //
208        // The Playwright initializer has "utils": { "guid": "localUtils" }.
209        // LocalUtils's initializer has "deviceDescriptors": [ { "name": "...", "descriptor": { ... } }, ... ]
210        //
211        // We wrap the inner descriptor fields in a helper struct that matches the
212        // server-side shape: { name, descriptor: { userAgent, viewport, ... } }.
213        #[derive(serde::Deserialize)]
214        struct DeviceEntry {
215            name: String,
216            descriptor: DeviceDescriptor,
217        }
218
219        let local_utils_guid = initializer
220            .get("utils")
221            .and_then(|v| v.get("guid"))
222            .and_then(|v| v.as_str())
223            .unwrap_or("localUtils");
224
225        let devices: HashMap<String, DeviceDescriptor> =
226            if let Ok(lu) = connection.get_object(local_utils_guid).await {
227                lu.initializer()
228                    .get("deviceDescriptors")
229                    .and_then(|v| v.as_array())
230                    .map(|arr| {
231                        arr.iter()
232                            .filter_map(|v| {
233                                serde_json::from_value::<DeviceEntry>(v.clone())
234                                    .ok()
235                                    .map(|e| (e.name.clone(), e.descriptor))
236                            })
237                            .collect()
238                    })
239                    .unwrap_or_default()
240            } else {
241                HashMap::new()
242            };
243
244        Ok(Self {
245            base,
246            chromium,
247            firefox,
248            webkit,
249            server: Arc::new(Mutex::new(None)), // No server for protocol-created objects
250            devices,
251        })
252    }
253
254    /// Returns the Chromium browser type.
255    pub fn chromium(&self) -> &BrowserType {
256        &self.chromium
257    }
258
259    /// Returns the Firefox browser type.
260    pub fn firefox(&self) -> &BrowserType {
261        &self.firefox
262    }
263
264    /// Returns the WebKit browser type.
265    pub fn webkit(&self) -> &BrowserType {
266        &self.webkit
267    }
268
269    /// Returns the Selectors object for registering custom selector engines.
270    ///
271    /// The Selectors instance is shared across all browser contexts created on this
272    /// connection. Register custom selector engines here before creating any pages
273    /// that will use them.
274    ///
275    /// # Example
276    ///
277    /// ```ignore
278    /// # use playwright_rs::protocol::Playwright;
279    /// # #[tokio::main]
280    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
281    /// let playwright = Playwright::launch().await?;
282    /// let selectors = playwright.selectors();
283    /// selectors.set_test_id_attribute("data-custom-id").await?;
284    /// # Ok(())
285    /// # }
286    /// ```
287    ///
288    /// See: <https://playwright.dev/docs/api/class-playwright#playwright-selectors>
289    pub fn selectors(&self) -> std::sync::Arc<Selectors> {
290        self.connection().selectors()
291    }
292
293    /// Returns the device descriptors map for browser emulation.
294    ///
295    /// Each entry maps a device name (e.g., `"iPhone 13"`) to a [`DeviceDescriptor`]
296    /// containing user agent, viewport, and other emulation settings.
297    ///
298    /// # Example
299    ///
300    /// ```ignore
301    /// # use playwright_rs::protocol::Playwright;
302    /// # #[tokio::main]
303    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
304    /// let playwright = Playwright::launch().await?;
305    /// let iphone = &playwright.devices()["iPhone 13"];
306    /// // Use iphone fields to configure BrowserContext...
307    /// # Ok(())
308    /// # }
309    /// ```
310    ///
311    /// See: <https://playwright.dev/docs/api/class-playwright#playwright-devices>
312    pub fn devices(&self) -> &HashMap<String, DeviceDescriptor> {
313        &self.devices
314    }
315
316    /// Shuts down the Playwright server gracefully.
317    ///
318    /// This method should be called when you're done using Playwright to ensure
319    /// the server process is terminated cleanly, especially on Windows.
320    ///
321    /// # Platform-Specific Behavior
322    ///
323    /// **Windows**: Closes stdio pipes before shutting down to prevent hangs.
324    ///
325    /// **Unix**: Standard graceful shutdown.
326    ///
327    /// # Errors
328    ///
329    /// Returns an error if the server shutdown fails.
330    pub async fn shutdown(&self) -> Result<()> {
331        // Take server from mutex without holding the lock across await
332        let server = self.server.lock().take();
333        if let Some(server) = server {
334            tracing::debug!("Shutting down Playwright server");
335            server.shutdown().await?;
336        }
337        Ok(())
338    }
339}
340
341impl ChannelOwner for Playwright {
342    fn guid(&self) -> &str {
343        self.base.guid()
344    }
345
346    fn type_name(&self) -> &str {
347        self.base.type_name()
348    }
349
350    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
351        self.base.parent()
352    }
353
354    fn connection(&self) -> Arc<dyn ConnectionLike> {
355        self.base.connection()
356    }
357
358    fn initializer(&self) -> &Value {
359        self.base.initializer()
360    }
361
362    fn channel(&self) -> &Channel {
363        self.base.channel()
364    }
365
366    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
367        self.base.dispose(reason)
368    }
369
370    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
371        self.base.adopt(child)
372    }
373
374    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
375        self.base.add_child(guid, child)
376    }
377
378    fn remove_child(&self, guid: &str) {
379        self.base.remove_child(guid)
380    }
381
382    fn on_event(&self, method: &str, params: Value) {
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 Drop for Playwright {
396    /// Ensures Playwright server is shut down when Playwright is dropped.
397    ///
398    /// This is critical on Windows to prevent process hangs when tests complete.
399    /// The Drop implementation will attempt to kill the server process synchronously.
400    ///
401    /// Note: For graceful shutdown, prefer calling `playwright.shutdown().await`
402    /// explicitly before dropping.
403    fn drop(&mut self) {
404        if let Some(mut server) = self.server.lock().take() {
405            tracing::debug!("Drop: Force-killing Playwright server");
406
407            // We can't call async shutdown in Drop, so use blocking kill
408            // This is less graceful but ensures the process terminates
409            #[cfg(windows)]
410            {
411                // On Windows: Close stdio pipes before killing
412                drop(server.process.stdin.take());
413                drop(server.process.stdout.take());
414                drop(server.process.stderr.take());
415            }
416
417            // Force kill the process
418            if let Err(e) = server.process.start_kill() {
419                tracing::warn!("Failed to kill Playwright server in Drop: {}", e);
420            }
421        }
422    }
423}
424
425impl std::fmt::Debug for Playwright {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        f.debug_struct("Playwright")
428            .field("guid", &self.guid())
429            .field("chromium", &self.chromium().name())
430            .field("firefox", &self.firefox().name())
431            .field("webkit", &self.webkit().name())
432            .field("selectors", &*self.selectors())
433            .finish()
434    }
435}
436
437// Note: Playwright testing is done via integration tests since it requires:
438// - A real Connection with object registry
439// - BrowserType objects already created and registered
440// - Protocol messages from the server
441// See: crates/playwright-core/tests/connection_integration.rs