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