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