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 an `APIRequest` factory for creating standalone HTTP request contexts.
270    ///
271    /// Use this to perform HTTP requests outside of a browser page, suitable for
272    /// headless API testing.
273    ///
274    /// # Example
275    ///
276    /// ```ignore
277    /// # use playwright_rs::protocol::Playwright;
278    /// # #[tokio::main]
279    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
280    /// let playwright = Playwright::launch().await?;
281    /// let ctx = playwright.request().new_context(None).await?;
282    /// let response = ctx.get("https://httpbin.org/get", None).await?;
283    /// assert!(response.ok());
284    /// ctx.dispose().await?;
285    /// # Ok(())
286    /// # }
287    /// ```
288    ///
289    /// See: <https://playwright.dev/docs/api/class-playwright#playwright-request>
290    pub fn request(&self) -> crate::protocol::api_request_context::APIRequest {
291        crate::protocol::api_request_context::APIRequest::new(
292            self.channel().clone(),
293            self.connection(),
294        )
295    }
296
297    /// Returns the Selectors object for registering custom selector engines.
298    ///
299    /// The Selectors instance is shared across all browser contexts created on this
300    /// connection. Register custom selector engines here before creating any pages
301    /// that will use them.
302    ///
303    /// # Example
304    ///
305    /// ```ignore
306    /// # use playwright_rs::protocol::Playwright;
307    /// # #[tokio::main]
308    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
309    /// let playwright = Playwright::launch().await?;
310    /// let selectors = playwright.selectors();
311    /// selectors.set_test_id_attribute("data-custom-id").await?;
312    /// # Ok(())
313    /// # }
314    /// ```
315    ///
316    /// See: <https://playwright.dev/docs/api/class-playwright#playwright-selectors>
317    pub fn selectors(&self) -> std::sync::Arc<Selectors> {
318        self.connection().selectors()
319    }
320
321    /// Returns the device descriptors map for browser emulation.
322    ///
323    /// Each entry maps a device name (e.g., `"iPhone 13"`) to a [`DeviceDescriptor`]
324    /// containing user agent, viewport, and other emulation settings.
325    ///
326    /// # Example
327    ///
328    /// ```ignore
329    /// # use playwright_rs::protocol::Playwright;
330    /// # #[tokio::main]
331    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
332    /// let playwright = Playwright::launch().await?;
333    /// let iphone = &playwright.devices()["iPhone 13"];
334    /// // Use iphone fields to configure BrowserContext...
335    /// # Ok(())
336    /// # }
337    /// ```
338    ///
339    /// See: <https://playwright.dev/docs/api/class-playwright#playwright-devices>
340    pub fn devices(&self) -> &HashMap<String, DeviceDescriptor> {
341        &self.devices
342    }
343
344    /// Shuts down the Playwright server gracefully.
345    ///
346    /// This method should be called when you're done using Playwright to ensure
347    /// the server process is terminated cleanly, especially on Windows.
348    ///
349    /// # Platform-Specific Behavior
350    ///
351    /// **Windows**: Closes stdio pipes before shutting down to prevent hangs.
352    ///
353    /// **Unix**: Standard graceful shutdown.
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if the server shutdown fails.
358    pub async fn shutdown(&self) -> Result<()> {
359        // Take server from mutex without holding the lock across await
360        let server = self.server.lock().take();
361        if let Some(server) = server {
362            tracing::debug!("Shutting down Playwright server");
363            server.shutdown().await?;
364        }
365        Ok(())
366    }
367}
368
369impl ChannelOwner for Playwright {
370    fn guid(&self) -> &str {
371        self.base.guid()
372    }
373
374    fn type_name(&self) -> &str {
375        self.base.type_name()
376    }
377
378    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
379        self.base.parent()
380    }
381
382    fn connection(&self) -> Arc<dyn ConnectionLike> {
383        self.base.connection()
384    }
385
386    fn initializer(&self) -> &Value {
387        self.base.initializer()
388    }
389
390    fn channel(&self) -> &Channel {
391        self.base.channel()
392    }
393
394    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
395        self.base.dispose(reason)
396    }
397
398    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
399        self.base.adopt(child)
400    }
401
402    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
403        self.base.add_child(guid, child)
404    }
405
406    fn remove_child(&self, guid: &str) {
407        self.base.remove_child(guid)
408    }
409
410    fn on_event(&self, method: &str, params: Value) {
411        self.base.on_event(method, params)
412    }
413
414    fn was_collected(&self) -> bool {
415        self.base.was_collected()
416    }
417
418    fn as_any(&self) -> &dyn Any {
419        self
420    }
421}
422
423impl Drop for Playwright {
424    /// Ensures Playwright server is shut down when Playwright is dropped.
425    ///
426    /// This is critical on Windows to prevent process hangs when tests complete.
427    /// The Drop implementation will attempt to kill the server process synchronously.
428    ///
429    /// Note: For graceful shutdown, prefer calling `playwright.shutdown().await`
430    /// explicitly before dropping.
431    fn drop(&mut self) {
432        if let Some(mut server) = self.server.lock().take() {
433            tracing::debug!("Drop: Force-killing Playwright server");
434
435            // We can't call async shutdown in Drop, so use blocking kill
436            // This is less graceful but ensures the process terminates
437            #[cfg(windows)]
438            {
439                // On Windows: Close stdio pipes before killing
440                drop(server.process.stdin.take());
441                drop(server.process.stdout.take());
442                drop(server.process.stderr.take());
443            }
444
445            // Force kill the process
446            if let Err(e) = server.process.start_kill() {
447                tracing::warn!("Failed to kill Playwright server in Drop: {}", e);
448            }
449        }
450    }
451}
452
453impl std::fmt::Debug for Playwright {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        f.debug_struct("Playwright")
456            .field("guid", &self.guid())
457            .field("chromium", &self.chromium().name())
458            .field("firefox", &self.firefox().name())
459            .field("webkit", &self.webkit().name())
460            .field("selectors", &*self.selectors())
461            .finish()
462    }
463}
464
465// Note: Playwright testing is done via integration tests since it requires:
466// - A real Connection with object registry
467// - BrowserType objects already created and registered
468// - Protocol messages from the server
469// See: crates/playwright-core/tests/connection_integration.rs