playwright_core/protocol/
playwright.rs

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