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