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::protocol::device::DeviceDescriptor;
13use crate::protocol::selectors::Selectors;
14use crate::server::channel::Channel;
15use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
16use crate::server::connection::{ConnectionExt, ConnectionLike};
17use crate::server::playwright_server::PlaywrightServer;
18use parking_lot::Mutex;
19use serde_json::Value;
20use std::any::Any;
21use std::collections::HashMap;
22use std::sync::Arc;
23
24/// Playwright is the root object that provides access to browser types.
25///
26/// This is the main entry point for the Playwright API. It provides access to
27/// the three browser types (Chromium, Firefox, WebKit) and other top-level services.
28///
29/// # Example
30///
31/// ```ignore
32/// use playwright_rs::protocol::Playwright;
33///
34/// #[tokio::main]
35/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
36/// // Launch Playwright server and initialize
37/// let playwright = Playwright::launch().await?;
38///
39/// // Verify all three browser types are available
40/// let chromium = playwright.chromium();
41/// let firefox = playwright.firefox();
42/// let webkit = playwright.webkit();
43///
44/// assert_eq!(chromium.name(), "chromium");
45/// assert_eq!(firefox.name(), "firefox");
46/// assert_eq!(webkit.name(), "webkit");
47///
48/// // Verify we can launch a browser
49/// let browser = chromium.launch().await?;
50/// assert!(!browser.version().is_empty());
51/// browser.close().await?;
52///
53/// // Shutdown when done
54/// playwright.shutdown().await?;
55///
56/// Ok(())
57/// }
58/// ```
59///
60/// See: <https://playwright.dev/docs/api/class-playwright>
61#[derive(Clone)]
62pub struct Playwright {
63 /// Base ChannelOwner implementation
64 base: ChannelOwnerImpl,
65 /// Chromium browser type
66 chromium: BrowserType,
67 /// Firefox browser type
68 firefox: BrowserType,
69 /// WebKit browser type
70 webkit: BrowserType,
71 /// Playwright server process (for clean shutdown)
72 ///
73 /// Stored as `Option<PlaywrightServer>` wrapped in Arc<Mutex<>> to allow:
74 /// - Sharing across clones (Arc)
75 /// - Taking ownership during shutdown (Option::take)
76 /// - Interior mutability (Mutex)
77 server: Arc<Mutex<Option<PlaywrightServer>>>,
78 /// Device descriptors parsed from the initializer's `deviceDescriptors` array.
79 devices: HashMap<String, DeviceDescriptor>,
80}
81
82impl Playwright {
83 /// Launches Playwright and returns a handle to interact with browser types.
84 ///
85 /// This is the main entry point for the Playwright API. It will:
86 /// 1. Launch the Playwright server process
87 /// 2. Establish a connection via stdio
88 /// 3. Initialize the protocol
89 /// 4. Return a Playwright instance with access to browser types
90 ///
91 /// # Errors
92 ///
93 /// Returns error if:
94 /// - Playwright server is not found or fails to launch
95 /// - Connection to server fails
96 /// - Protocol initialization fails
97 /// - Server doesn't respond within timeout (30s)
98 pub async fn launch() -> Result<Self> {
99 use crate::server::connection::Connection;
100 use crate::server::playwright_server::PlaywrightServer;
101 use crate::server::transport::PipeTransport;
102
103 // 1. Launch Playwright server
104 tracing::debug!("Launching Playwright server");
105 let mut server = PlaywrightServer::launch().await?;
106
107 // 2. Take stdio streams from server process
108 let stdin = server.process.stdin.take().ok_or_else(|| {
109 crate::error::Error::ServerError("Failed to get server stdin".to_string())
110 })?;
111
112 let stdout = server.process.stdout.take().ok_or_else(|| {
113 crate::error::Error::ServerError("Failed to get server stdout".to_string())
114 })?;
115
116 // 3. Create transport and connection
117 tracing::debug!("Creating transport and connection");
118 let (transport, message_rx) = PipeTransport::new(stdin, stdout);
119 let (sender, receiver) = transport.into_parts();
120 let connection: Arc<Connection> = Arc::new(Connection::new(sender, receiver, message_rx));
121
122 // 4. Spawn connection message loop in background
123 let conn_for_loop: Arc<Connection> = Arc::clone(&connection);
124 tokio::spawn(async move {
125 conn_for_loop.run().await;
126 });
127
128 // 5. Initialize Playwright (sends initialize message, waits for Playwright object)
129 tracing::debug!("Initializing Playwright protocol");
130 let playwright_obj = connection.initialize_playwright().await?;
131
132 // 6. Downcast to Playwright type using get_typed
133 let guid = playwright_obj.guid().to_string();
134 let mut playwright: Playwright = connection.get_typed::<Playwright>(&guid).await?;
135
136 // Attach the server for clean shutdown
137 playwright.server = Arc::new(Mutex::new(Some(server)));
138
139 Ok(playwright)
140 }
141
142 /// Creates a new Playwright object from protocol initialization.
143 ///
144 /// Called by the object factory when server sends __create__ message for root object.
145 ///
146 /// # Arguments
147 /// * `connection` - The connection (Playwright is root, so no parent)
148 /// * `type_name` - Protocol type name ("Playwright")
149 /// * `guid` - Unique GUID from server (typically "playwright@1")
150 /// * `initializer` - Initial state with references to browser types
151 ///
152 /// # Initializer Format
153 ///
154 /// The initializer contains GUID references to BrowserType objects:
155 /// ```json
156 /// {
157 /// "chromium": { "guid": "browserType@chromium" },
158 /// "firefox": { "guid": "browserType@firefox" },
159 /// "webkit": { "guid": "browserType@webkit" }
160 /// }
161 /// ```
162 ///
163 /// Note: `Selectors` is a pure client-side coordinator, not a protocol object.
164 /// It is created fresh here rather than looked up from the registry.
165 pub async fn new(
166 connection: Arc<dyn ConnectionLike>,
167 type_name: String,
168 guid: Arc<str>,
169 initializer: Value,
170 ) -> Result<Self> {
171 let base = ChannelOwnerImpl::new(
172 ParentOrConnection::Connection(connection.clone()),
173 type_name,
174 guid,
175 initializer.clone(),
176 );
177
178 // Extract BrowserType GUIDs from initializer
179 let chromium_guid = initializer["chromium"]["guid"].as_str().ok_or_else(|| {
180 crate::error::Error::ProtocolError(
181 "Playwright initializer missing 'chromium.guid'".to_string(),
182 )
183 })?;
184
185 let firefox_guid = initializer["firefox"]["guid"].as_str().ok_or_else(|| {
186 crate::error::Error::ProtocolError(
187 "Playwright initializer missing 'firefox.guid'".to_string(),
188 )
189 })?;
190
191 let webkit_guid = initializer["webkit"]["guid"].as_str().ok_or_else(|| {
192 crate::error::Error::ProtocolError(
193 "Playwright initializer missing 'webkit.guid'".to_string(),
194 )
195 })?;
196
197 // Get BrowserType objects from connection registry and downcast.
198 // Note: These objects should already exist (created by earlier __create__ messages).
199 let chromium: BrowserType = connection.get_typed::<BrowserType>(chromium_guid).await?;
200 let firefox: BrowserType = connection.get_typed::<BrowserType>(firefox_guid).await?;
201 let webkit: BrowserType = connection.get_typed::<BrowserType>(webkit_guid).await?;
202
203 // Selectors is a pure client-side coordinator stored in the connection.
204 // No need to create or store it here; access it via self.connection().selectors().
205
206 // Parse deviceDescriptors from LocalUtils.
207 //
208 // The Playwright initializer has "utils": { "guid": "localUtils" }.
209 // LocalUtils's initializer has "deviceDescriptors": [ { "name": "...", "descriptor": { ... } }, ... ]
210 //
211 // We wrap the inner descriptor fields in a helper struct that matches the
212 // server-side shape: { name, descriptor: { userAgent, viewport, ... } }.
213 #[derive(serde::Deserialize)]
214 struct DeviceEntry {
215 name: String,
216 descriptor: DeviceDescriptor,
217 }
218
219 let local_utils_guid = initializer
220 .get("utils")
221 .and_then(|v| v.get("guid"))
222 .and_then(|v| v.as_str())
223 .unwrap_or("localUtils");
224
225 let devices: HashMap<String, DeviceDescriptor> =
226 if let Ok(lu) = connection.get_object(local_utils_guid).await {
227 lu.initializer()
228 .get("deviceDescriptors")
229 .and_then(|v| v.as_array())
230 .map(|arr| {
231 arr.iter()
232 .filter_map(|v| {
233 serde_json::from_value::<DeviceEntry>(v.clone())
234 .ok()
235 .map(|e| (e.name.clone(), e.descriptor))
236 })
237 .collect()
238 })
239 .unwrap_or_default()
240 } else {
241 HashMap::new()
242 };
243
244 Ok(Self {
245 base,
246 chromium,
247 firefox,
248 webkit,
249 server: Arc::new(Mutex::new(None)), // No server for protocol-created objects
250 devices,
251 })
252 }
253
254 /// Returns the Chromium browser type.
255 pub fn chromium(&self) -> &BrowserType {
256 &self.chromium
257 }
258
259 /// Returns the Firefox browser type.
260 pub fn firefox(&self) -> &BrowserType {
261 &self.firefox
262 }
263
264 /// Returns the WebKit browser type.
265 pub fn webkit(&self) -> &BrowserType {
266 &self.webkit
267 }
268
269 /// Returns an `APIRequest` factory for creating standalone HTTP request contexts.
270 ///
271 /// Use this to perform HTTP requests outside of a browser page, suitable for
272 /// headless API testing.
273 ///
274 /// # Example
275 ///
276 /// ```ignore
277 /// # use playwright_rs::protocol::Playwright;
278 /// # #[tokio::main]
279 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
280 /// let playwright = Playwright::launch().await?;
281 /// let ctx = playwright.request().new_context(None).await?;
282 /// let response = ctx.get("https://httpbin.org/get", None).await?;
283 /// assert!(response.ok());
284 /// ctx.dispose().await?;
285 /// # Ok(())
286 /// # }
287 /// ```
288 ///
289 /// See: <https://playwright.dev/docs/api/class-playwright#playwright-request>
290 pub fn request(&self) -> crate::protocol::api_request_context::APIRequest {
291 crate::protocol::api_request_context::APIRequest::new(
292 self.channel().clone(),
293 self.connection(),
294 )
295 }
296
297 /// Returns the Selectors object for registering custom selector engines.
298 ///
299 /// The Selectors instance is shared across all browser contexts created on this
300 /// connection. Register custom selector engines here before creating any pages
301 /// that will use them.
302 ///
303 /// # Example
304 ///
305 /// ```ignore
306 /// # use playwright_rs::protocol::Playwright;
307 /// # #[tokio::main]
308 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
309 /// let playwright = Playwright::launch().await?;
310 /// let selectors = playwright.selectors();
311 /// selectors.set_test_id_attribute("data-custom-id").await?;
312 /// # Ok(())
313 /// # }
314 /// ```
315 ///
316 /// See: <https://playwright.dev/docs/api/class-playwright#playwright-selectors>
317 pub fn selectors(&self) -> std::sync::Arc<Selectors> {
318 self.connection().selectors()
319 }
320
321 /// Returns the device descriptors map for browser emulation.
322 ///
323 /// Each entry maps a device name (e.g., `"iPhone 13"`) to a [`DeviceDescriptor`]
324 /// containing user agent, viewport, and other emulation settings.
325 ///
326 /// # Example
327 ///
328 /// ```ignore
329 /// # use playwright_rs::protocol::Playwright;
330 /// # #[tokio::main]
331 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
332 /// let playwright = Playwright::launch().await?;
333 /// let iphone = &playwright.devices()["iPhone 13"];
334 /// // Use iphone fields to configure BrowserContext...
335 /// # Ok(())
336 /// # }
337 /// ```
338 ///
339 /// See: <https://playwright.dev/docs/api/class-playwright#playwright-devices>
340 pub fn devices(&self) -> &HashMap<String, DeviceDescriptor> {
341 &self.devices
342 }
343
344 /// Shuts down the Playwright server gracefully.
345 ///
346 /// This method should be called when you're done using Playwright to ensure
347 /// the server process is terminated cleanly, especially on Windows.
348 ///
349 /// # Platform-Specific Behavior
350 ///
351 /// **Windows**: Closes stdio pipes before shutting down to prevent hangs.
352 ///
353 /// **Unix**: Standard graceful shutdown.
354 ///
355 /// # Errors
356 ///
357 /// Returns an error if the server shutdown fails.
358 pub async fn shutdown(&self) -> Result<()> {
359 // Take server from mutex without holding the lock across await
360 let server = self.server.lock().take();
361 if let Some(server) = server {
362 tracing::debug!("Shutting down Playwright server");
363 server.shutdown().await?;
364 }
365 Ok(())
366 }
367}
368
369impl ChannelOwner for Playwright {
370 fn guid(&self) -> &str {
371 self.base.guid()
372 }
373
374 fn type_name(&self) -> &str {
375 self.base.type_name()
376 }
377
378 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
379 self.base.parent()
380 }
381
382 fn connection(&self) -> Arc<dyn ConnectionLike> {
383 self.base.connection()
384 }
385
386 fn initializer(&self) -> &Value {
387 self.base.initializer()
388 }
389
390 fn channel(&self) -> &Channel {
391 self.base.channel()
392 }
393
394 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
395 self.base.dispose(reason)
396 }
397
398 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
399 self.base.adopt(child)
400 }
401
402 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
403 self.base.add_child(guid, child)
404 }
405
406 fn remove_child(&self, guid: &str) {
407 self.base.remove_child(guid)
408 }
409
410 fn on_event(&self, method: &str, params: Value) {
411 self.base.on_event(method, params)
412 }
413
414 fn was_collected(&self) -> bool {
415 self.base.was_collected()
416 }
417
418 fn as_any(&self) -> &dyn Any {
419 self
420 }
421}
422
423impl Drop for Playwright {
424 /// Ensures Playwright server is shut down when Playwright is dropped.
425 ///
426 /// This is critical on Windows to prevent process hangs when tests complete.
427 /// The Drop implementation will attempt to kill the server process synchronously.
428 ///
429 /// Note: For graceful shutdown, prefer calling `playwright.shutdown().await`
430 /// explicitly before dropping.
431 fn drop(&mut self) {
432 if let Some(mut server) = self.server.lock().take() {
433 tracing::debug!("Drop: Force-killing Playwright server");
434
435 // We can't call async shutdown in Drop, so use blocking kill
436 // This is less graceful but ensures the process terminates
437 #[cfg(windows)]
438 {
439 // On Windows: Close stdio pipes before killing
440 drop(server.process.stdin.take());
441 drop(server.process.stdout.take());
442 drop(server.process.stderr.take());
443 }
444
445 // Force kill the process
446 if let Err(e) = server.process.start_kill() {
447 tracing::warn!("Failed to kill Playwright server in Drop: {}", e);
448 }
449 }
450 }
451}
452
453impl std::fmt::Debug for Playwright {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 f.debug_struct("Playwright")
456 .field("guid", &self.guid())
457 .field("chromium", &self.chromium().name())
458 .field("firefox", &self.firefox().name())
459 .field("webkit", &self.webkit().name())
460 .field("selectors", &*self.selectors())
461 .finish()
462 }
463}
464
465// Note: Playwright testing is done via integration tests since it requires:
466// - A real Connection with object registry
467// - BrowserType objects already created and registered
468// - Protocol messages from the server
469// See: crates/playwright-core/tests/connection_integration.rs