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::server::channel::Channel;
13use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
14use crate::server::connection::{ConnectionExt, ConnectionLike};
15use crate::server::playwright_server::PlaywrightServer;
16use parking_lot::Mutex;
17use serde_json::Value;
18use std::any::Any;
19use std::sync::Arc;
20
21/// Playwright is the root object that provides access to browser types.
22///
23/// This is the main entry point for the Playwright API. It provides access to
24/// the three browser types (Chromium, Firefox, WebKit) and other top-level services.
25///
26/// # Example
27///
28/// ```ignore
29/// use playwright_rs::protocol::Playwright;
30///
31/// #[tokio::main]
32/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
33///     // Launch Playwright server and initialize
34///     let playwright = Playwright::launch().await?;
35///
36///     // Verify all three browser types are available
37///     let chromium = playwright.chromium();
38///     let firefox = playwright.firefox();
39///     let webkit = playwright.webkit();
40///
41///     assert_eq!(chromium.name(), "chromium");
42///     assert_eq!(firefox.name(), "firefox");
43///     assert_eq!(webkit.name(), "webkit");
44///
45///     // Verify we can launch a browser
46///     let browser = chromium.launch().await?;
47///     assert!(!browser.version().is_empty());
48///     browser.close().await?;
49///
50///     // Shutdown when done
51///     playwright.shutdown().await?;
52///
53///     Ok(())
54/// }
55/// ```
56///
57/// See: <https://playwright.dev/docs/api/class-playwright>
58#[derive(Clone)]
59pub struct Playwright {
60    /// Base ChannelOwner implementation
61    base: ChannelOwnerImpl,
62    /// Chromium browser type
63    chromium: BrowserType,
64    /// Firefox browser type
65    firefox: BrowserType,
66    /// WebKit browser type
67    webkit: BrowserType,
68    /// Playwright server process (for clean shutdown)
69    ///
70    /// Stored as `Option<PlaywrightServer>` wrapped in Arc<Mutex<>> to allow:
71    /// - Sharing across clones (Arc)
72    /// - Taking ownership during shutdown (Option::take)
73    /// - Interior mutability (Mutex)
74    server: Arc<Mutex<Option<PlaywrightServer>>>,
75}
76
77impl Playwright {
78    /// Launches Playwright and returns a handle to interact with browser types.
79    ///
80    /// This is the main entry point for the Playwright API. It will:
81    /// 1. Launch the Playwright server process
82    /// 2. Establish a connection via stdio
83    /// 3. Initialize the protocol
84    /// 4. Return a Playwright instance with access to browser types
85    ///
86    /// # Errors
87    ///
88    /// Returns error if:
89    /// - Playwright server is not found or fails to launch
90    /// - Connection to server fails
91    /// - Protocol initialization fails
92    /// - Server doesn't respond within timeout (30s)
93    pub async fn launch() -> Result<Self> {
94        use crate::server::connection::Connection;
95        use crate::server::playwright_server::PlaywrightServer;
96        use crate::server::transport::PipeTransport;
97
98        // 1. Launch Playwright server
99        tracing::debug!("Launching Playwright server");
100        let mut server = PlaywrightServer::launch().await?;
101
102        // 2. Take stdio streams from server process
103        let stdin = server.process.stdin.take().ok_or_else(|| {
104            crate::error::Error::ServerError("Failed to get server stdin".to_string())
105        })?;
106
107        let stdout = server.process.stdout.take().ok_or_else(|| {
108            crate::error::Error::ServerError("Failed to get server stdout".to_string())
109        })?;
110
111        // 3. Create transport and connection
112        tracing::debug!("Creating transport and connection");
113        let (transport, message_rx) = PipeTransport::new(stdin, stdout);
114        let (sender, receiver) = transport.into_parts();
115        let connection: Arc<Connection> = Arc::new(Connection::new(sender, receiver, message_rx));
116
117        // 4. Spawn connection message loop in background
118        let conn_for_loop: Arc<Connection> = Arc::clone(&connection);
119        tokio::spawn(async move {
120            conn_for_loop.run().await;
121        });
122
123        // 5. Initialize Playwright (sends initialize message, waits for Playwright object)
124        tracing::debug!("Initializing Playwright protocol");
125        let playwright_obj = connection.initialize_playwright().await?;
126
127        // 6. Downcast to Playwright type using get_typed
128        let guid = playwright_obj.guid().to_string();
129        let mut playwright: Playwright = connection.get_typed::<Playwright>(&guid).await?;
130
131        // Attach the server for clean shutdown
132        playwright.server = Arc::new(Mutex::new(Some(server)));
133
134        Ok(playwright)
135    }
136
137    /// Creates a new Playwright object from protocol initialization.
138    ///
139    /// Called by the object factory when server sends __create__ message for root object.
140    ///
141    /// # Arguments
142    /// * `connection` - The connection (Playwright is root, so no parent)
143    /// * `type_name` - Protocol type name ("Playwright")
144    /// * `guid` - Unique GUID from server (typically "playwright@1")
145    /// * `initializer` - Initial state with references to browser types
146    ///
147    /// # Initializer Format
148    ///
149    /// The initializer contains GUID references to BrowserType objects:
150    /// ```json
151    /// {
152    ///   "chromium": { "guid": "browserType@chromium" },
153    ///   "firefox": { "guid": "browserType@firefox" },
154    ///   "webkit": { "guid": "browserType@webkit" }
155    /// }
156    /// ```
157    pub async fn new(
158        connection: Arc<dyn ConnectionLike>,
159        type_name: String,
160        guid: Arc<str>,
161        initializer: Value,
162    ) -> Result<Self> {
163        let base = ChannelOwnerImpl::new(
164            ParentOrConnection::Connection(connection.clone()),
165            type_name,
166            guid,
167            initializer.clone(),
168        );
169
170        // Extract BrowserType GUIDs from initializer
171        let chromium_guid = initializer["chromium"]["guid"].as_str().ok_or_else(|| {
172            crate::error::Error::ProtocolError(
173                "Playwright initializer missing 'chromium.guid'".to_string(),
174            )
175        })?;
176
177        let firefox_guid = initializer["firefox"]["guid"].as_str().ok_or_else(|| {
178            crate::error::Error::ProtocolError(
179                "Playwright initializer missing 'firefox.guid'".to_string(),
180            )
181        })?;
182
183        let webkit_guid = initializer["webkit"]["guid"].as_str().ok_or_else(|| {
184            crate::error::Error::ProtocolError(
185                "Playwright initializer missing 'webkit.guid'".to_string(),
186            )
187        })?;
188
189        // Get BrowserType objects from connection registry and downcast
190        // Note: These objects should already exist (created by earlier __create__ messages)
191        let chromium: BrowserType = connection.get_typed::<BrowserType>(chromium_guid).await?;
192        let firefox: BrowserType = connection.get_typed::<BrowserType>(firefox_guid).await?;
193        let webkit: BrowserType = connection.get_typed::<BrowserType>(webkit_guid).await?;
194
195        Ok(Self {
196            base,
197            chromium,
198            firefox,
199            webkit,
200            server: Arc::new(Mutex::new(None)), // No server for protocol-created objects
201        })
202    }
203
204    /// Returns the Chromium browser type.
205    pub fn chromium(&self) -> &BrowserType {
206        &self.chromium
207    }
208
209    /// Returns the Firefox browser type.
210    pub fn firefox(&self) -> &BrowserType {
211        &self.firefox
212    }
213
214    /// Returns the WebKit browser type.
215    pub fn webkit(&self) -> &BrowserType {
216        &self.webkit
217    }
218
219    /// Shuts down the Playwright server gracefully.
220    ///
221    /// This method should be called when you're done using Playwright to ensure
222    /// the server process is terminated cleanly, especially on Windows.
223    ///
224    /// # Platform-Specific Behavior
225    ///
226    /// **Windows**: Closes stdio pipes before shutting down to prevent hangs.
227    ///
228    /// **Unix**: Standard graceful shutdown.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if the server shutdown fails.
233    pub async fn shutdown(&self) -> Result<()> {
234        // Take server from mutex without holding the lock across await
235        let server = self.server.lock().take();
236        if let Some(server) = server {
237            tracing::debug!("Shutting down Playwright server");
238            server.shutdown().await?;
239        }
240        Ok(())
241    }
242}
243
244impl ChannelOwner for Playwright {
245    fn guid(&self) -> &str {
246        self.base.guid()
247    }
248
249    fn type_name(&self) -> &str {
250        self.base.type_name()
251    }
252
253    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
254        self.base.parent()
255    }
256
257    fn connection(&self) -> Arc<dyn ConnectionLike> {
258        self.base.connection()
259    }
260
261    fn initializer(&self) -> &Value {
262        self.base.initializer()
263    }
264
265    fn channel(&self) -> &Channel {
266        self.base.channel()
267    }
268
269    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
270        self.base.dispose(reason)
271    }
272
273    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
274        self.base.adopt(child)
275    }
276
277    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
278        self.base.add_child(guid, child)
279    }
280
281    fn remove_child(&self, guid: &str) {
282        self.base.remove_child(guid)
283    }
284
285    fn on_event(&self, method: &str, params: Value) {
286        self.base.on_event(method, params)
287    }
288
289    fn was_collected(&self) -> bool {
290        self.base.was_collected()
291    }
292
293    fn as_any(&self) -> &dyn Any {
294        self
295    }
296}
297
298impl Drop for Playwright {
299    /// Ensures Playwright server is shut down when Playwright is dropped.
300    ///
301    /// This is critical on Windows to prevent process hangs when tests complete.
302    /// The Drop implementation will attempt to kill the server process synchronously.
303    ///
304    /// Note: For graceful shutdown, prefer calling `playwright.shutdown().await`
305    /// explicitly before dropping.
306    fn drop(&mut self) {
307        if let Some(mut server) = self.server.lock().take() {
308            tracing::debug!("Drop: Force-killing Playwright server");
309
310            // We can't call async shutdown in Drop, so use blocking kill
311            // This is less graceful but ensures the process terminates
312            #[cfg(windows)]
313            {
314                // On Windows: Close stdio pipes before killing
315                drop(server.process.stdin.take());
316                drop(server.process.stdout.take());
317                drop(server.process.stderr.take());
318            }
319
320            // Force kill the process
321            if let Err(e) = server.process.start_kill() {
322                tracing::warn!("Failed to kill Playwright server in Drop: {}", e);
323            }
324        }
325    }
326}
327
328impl std::fmt::Debug for Playwright {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        f.debug_struct("Playwright")
331            .field("guid", &self.guid())
332            .field("chromium", &self.chromium().name())
333            .field("firefox", &self.firefox().name())
334            .field("webkit", &self.webkit().name())
335            .finish()
336    }
337}
338
339// Note: Playwright testing is done via integration tests since it requires:
340// - A real Connection with object registry
341// - BrowserType objects already created and registered
342// - Protocol messages from the server
343// See: crates/playwright-core/tests/connection_integration.rs