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 // Snapshot stdin termios and install a SIGINT handler that
104 // restores it before exiting. Defends against subprocesses that
105 // clobber the controlling terminal's mode and don't restore on
106 // abrupt exit (issue #59). Idempotent — multiple Playwright
107 // instances share the snapshot/handler. Opt-out via the
108 // PLAYWRIGHT_NO_SIGNAL_HANDLER env var.
109 crate::tty_guard::save_if_tty();
110 crate::tty_guard::install_signal_handler();
111
112 // 1. Launch Playwright server
113 tracing::debug!("Launching Playwright server");
114 let mut server = PlaywrightServer::launch().await?;
115
116 // 2. Take stdio streams from server process
117 let stdin = server.process.stdin.take().ok_or_else(|| {
118 crate::error::Error::ServerError("Failed to get server stdin".to_string())
119 })?;
120
121 let stdout = server.process.stdout.take().ok_or_else(|| {
122 crate::error::Error::ServerError("Failed to get server stdout".to_string())
123 })?;
124
125 // 3. Create transport and connection
126 tracing::debug!("Creating transport and connection");
127 let (transport, message_rx) = PipeTransport::new(stdin, stdout);
128 let (sender, receiver) = transport.into_parts();
129 let connection: Arc<Connection> = Arc::new(Connection::new(sender, receiver, message_rx));
130
131 // 4. Spawn connection message loop in background
132 let conn_for_loop: Arc<Connection> = Arc::clone(&connection);
133 tokio::spawn(async move {
134 conn_for_loop.run().await;
135 });
136
137 // 5. Initialize Playwright (sends initialize message, waits for Playwright object)
138 tracing::debug!("Initializing Playwright protocol");
139 let playwright_obj = connection.initialize_playwright().await?;
140
141 // 6. Downcast to Playwright type using get_typed
142 let guid = playwright_obj.guid().to_string();
143 let mut playwright: Playwright = connection.get_typed::<Playwright>(&guid).await?;
144
145 // Attach the server for clean shutdown
146 playwright.server = Arc::new(Mutex::new(Some(server)));
147
148 Ok(playwright)
149 }
150
151 /// Creates a new Playwright object from protocol initialization.
152 ///
153 /// Called by the object factory when server sends __create__ message for root object.
154 ///
155 /// # Arguments
156 /// * `connection` - The connection (Playwright is root, so no parent)
157 /// * `type_name` - Protocol type name ("Playwright")
158 /// * `guid` - Unique GUID from server (typically "playwright@1")
159 /// * `initializer` - Initial state with references to browser types
160 ///
161 /// # Initializer Format
162 ///
163 /// The initializer contains GUID references to BrowserType objects:
164 /// ```json
165 /// {
166 /// "chromium": { "guid": "browserType@chromium" },
167 /// "firefox": { "guid": "browserType@firefox" },
168 /// "webkit": { "guid": "browserType@webkit" }
169 /// }
170 /// ```
171 ///
172 /// Note: `Selectors` is a pure client-side coordinator, not a protocol object.
173 /// It is created fresh here rather than looked up from the registry.
174 pub async fn new(
175 connection: Arc<dyn ConnectionLike>,
176 type_name: String,
177 guid: Arc<str>,
178 initializer: Value,
179 ) -> Result<Self> {
180 let base = ChannelOwnerImpl::new(
181 ParentOrConnection::Connection(connection.clone()),
182 type_name,
183 guid,
184 initializer.clone(),
185 );
186
187 // Extract BrowserType GUIDs from initializer
188 let chromium_guid = initializer["chromium"]["guid"].as_str().ok_or_else(|| {
189 crate::error::Error::ProtocolError(
190 "Playwright initializer missing 'chromium.guid'".to_string(),
191 )
192 })?;
193
194 let firefox_guid = initializer["firefox"]["guid"].as_str().ok_or_else(|| {
195 crate::error::Error::ProtocolError(
196 "Playwright initializer missing 'firefox.guid'".to_string(),
197 )
198 })?;
199
200 let webkit_guid = initializer["webkit"]["guid"].as_str().ok_or_else(|| {
201 crate::error::Error::ProtocolError(
202 "Playwright initializer missing 'webkit.guid'".to_string(),
203 )
204 })?;
205
206 // Get BrowserType objects from connection registry and downcast.
207 // Note: These objects should already exist (created by earlier __create__ messages).
208 let chromium: BrowserType = connection.get_typed::<BrowserType>(chromium_guid).await?;
209 let firefox: BrowserType = connection.get_typed::<BrowserType>(firefox_guid).await?;
210 let webkit: BrowserType = connection.get_typed::<BrowserType>(webkit_guid).await?;
211
212 // Selectors is a pure client-side coordinator stored in the connection.
213 // No need to create or store it here; access it via self.connection().selectors().
214
215 // Parse deviceDescriptors from LocalUtils.
216 //
217 // The Playwright initializer has "utils": { "guid": "localUtils" }.
218 // LocalUtils's initializer has "deviceDescriptors": [ { "name": "...", "descriptor": { ... } }, ... ]
219 //
220 // We wrap the inner descriptor fields in a helper struct that matches the
221 // server-side shape: { name, descriptor: { userAgent, viewport, ... } }.
222 #[derive(serde::Deserialize)]
223 struct DeviceEntry {
224 name: String,
225 descriptor: DeviceDescriptor,
226 }
227
228 let local_utils_guid = initializer
229 .get("utils")
230 .and_then(|v| v.get("guid"))
231 .and_then(|v| v.as_str())
232 .unwrap_or("localUtils");
233
234 let devices: HashMap<String, DeviceDescriptor> =
235 if let Ok(lu) = connection.get_object(local_utils_guid).await {
236 lu.initializer()
237 .get("deviceDescriptors")
238 .and_then(|v| v.as_array())
239 .map(|arr| {
240 arr.iter()
241 .filter_map(|v| {
242 serde_json::from_value::<DeviceEntry>(v.clone())
243 .ok()
244 .map(|e| (e.name.clone(), e.descriptor))
245 })
246 .collect()
247 })
248 .unwrap_or_default()
249 } else {
250 HashMap::new()
251 };
252
253 Ok(Self {
254 base,
255 chromium,
256 firefox,
257 webkit,
258 server: Arc::new(Mutex::new(None)), // No server for protocol-created objects
259 devices,
260 })
261 }
262
263 /// Returns the Chromium browser type.
264 pub fn chromium(&self) -> &BrowserType {
265 &self.chromium
266 }
267
268 /// Returns the Firefox browser type.
269 pub fn firefox(&self) -> &BrowserType {
270 &self.firefox
271 }
272
273 /// Returns the WebKit browser type.
274 pub fn webkit(&self) -> &BrowserType {
275 &self.webkit
276 }
277
278 /// Returns an `APIRequest` factory for creating standalone HTTP request contexts.
279 ///
280 /// Use this to perform HTTP requests outside of a browser page, suitable for
281 /// headless API testing.
282 ///
283 /// # Example
284 ///
285 /// ```ignore
286 /// # use playwright_rs::protocol::Playwright;
287 /// # #[tokio::main]
288 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
289 /// let playwright = Playwright::launch().await?;
290 /// let ctx = playwright.request().new_context(None).await?;
291 /// let response = ctx.get("https://httpbin.org/get", None).await?;
292 /// assert!(response.ok());
293 /// ctx.dispose().await?;
294 /// # Ok(())
295 /// # }
296 /// ```
297 ///
298 /// See: <https://playwright.dev/docs/api/class-playwright#playwright-request>
299 pub fn request(&self) -> crate::protocol::api_request_context::APIRequest {
300 crate::protocol::api_request_context::APIRequest::new(
301 self.channel().clone(),
302 self.connection(),
303 )
304 }
305
306 /// Returns the Selectors object for registering custom selector engines.
307 ///
308 /// The Selectors instance is shared across all browser contexts created on this
309 /// connection. Register custom selector engines here before creating any pages
310 /// that will use them.
311 ///
312 /// # Example
313 ///
314 /// ```ignore
315 /// # use playwright_rs::protocol::Playwright;
316 /// # #[tokio::main]
317 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
318 /// let playwright = Playwright::launch().await?;
319 /// let selectors = playwright.selectors();
320 /// selectors.set_test_id_attribute("data-custom-id").await?;
321 /// # Ok(())
322 /// # }
323 /// ```
324 ///
325 /// See: <https://playwright.dev/docs/api/class-playwright#playwright-selectors>
326 pub fn selectors(&self) -> std::sync::Arc<Selectors> {
327 self.connection().selectors()
328 }
329
330 /// Returns the device descriptors map for browser emulation.
331 ///
332 /// Each entry maps a device name (e.g., `"iPhone 13"`) to a [`DeviceDescriptor`]
333 /// containing user agent, viewport, and other emulation settings.
334 ///
335 /// # Example
336 ///
337 /// ```ignore
338 /// # use playwright_rs::protocol::Playwright;
339 /// # #[tokio::main]
340 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
341 /// let playwright = Playwright::launch().await?;
342 /// let iphone = &playwright.devices()["iPhone 13"];
343 /// // Use iphone fields to configure BrowserContext...
344 /// # Ok(())
345 /// # }
346 /// ```
347 ///
348 /// See: <https://playwright.dev/docs/api/class-playwright#playwright-devices>
349 pub fn devices(&self) -> &HashMap<String, DeviceDescriptor> {
350 &self.devices
351 }
352
353 /// Shuts down the Playwright server gracefully.
354 ///
355 /// This method should be called when you're done using Playwright to ensure
356 /// the server process is terminated cleanly, especially on Windows.
357 ///
358 /// # Platform-Specific Behavior
359 ///
360 /// **Windows**: Closes stdio pipes before shutting down to prevent hangs.
361 ///
362 /// **Unix**: Standard graceful shutdown.
363 ///
364 /// # Errors
365 ///
366 /// Returns an error if the server shutdown fails.
367 pub async fn shutdown(&self) -> Result<()> {
368 // Take server from mutex without holding the lock across await
369 let server = self.server.lock().take();
370 if let Some(server) = server {
371 tracing::debug!("Shutting down Playwright server");
372 server.shutdown().await?;
373 }
374 Ok(())
375 }
376}
377
378impl ChannelOwner for Playwright {
379 fn guid(&self) -> &str {
380 self.base.guid()
381 }
382
383 fn type_name(&self) -> &str {
384 self.base.type_name()
385 }
386
387 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
388 self.base.parent()
389 }
390
391 fn connection(&self) -> Arc<dyn ConnectionLike> {
392 self.base.connection()
393 }
394
395 fn initializer(&self) -> &Value {
396 self.base.initializer()
397 }
398
399 fn channel(&self) -> &Channel {
400 self.base.channel()
401 }
402
403 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
404 self.base.dispose(reason)
405 }
406
407 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
408 self.base.adopt(child)
409 }
410
411 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
412 self.base.add_child(guid, child)
413 }
414
415 fn remove_child(&self, guid: &str) {
416 self.base.remove_child(guid)
417 }
418
419 fn on_event(&self, method: &str, params: Value) {
420 self.base.on_event(method, params)
421 }
422
423 fn was_collected(&self) -> bool {
424 self.base.was_collected()
425 }
426
427 fn as_any(&self) -> &dyn Any {
428 self
429 }
430}
431
432impl Drop for Playwright {
433 /// Ensures Playwright server is shut down when Playwright is dropped.
434 ///
435 /// On Unix, the driver's `cli/driver.js` listens for stdin close as a
436 /// graceful-exit signal — closing the pipe lets it tear down browsers
437 /// cleanly instead of leaving them as orphans. We drop stdin first,
438 /// then `start_kill` as a SIGKILL fallback (still issued
439 /// synchronously because we don't want to block the Drop).
440 ///
441 /// On Windows, tokio's blocking stdio threadpool requires we drop
442 /// the pipes before killing or the cleanup hangs.
443 ///
444 /// Also restores any termios snapshot we took at launch time
445 /// (defends against subprocesses that left the tty in raw mode —
446 /// issue #59).
447 ///
448 /// For fully graceful shutdown, prefer calling
449 /// `playwright.shutdown().await` explicitly before dropping.
450 fn drop(&mut self) {
451 if let Some(mut server) = self.server.lock().take() {
452 tracing::debug!("Drop: shutting down Playwright server");
453
454 // Close stdin first — driver.js's `process.stdin.on("close",
455 // gracefullyProcessExitDoNotHang)` triggers cleanup of
456 // browser children. On Windows we drop all stdio handles to
457 // avoid the blocking-threadpool hang.
458 drop(server.process.stdin.take());
459 #[cfg(windows)]
460 {
461 drop(server.process.stdout.take());
462 drop(server.process.stderr.take());
463 }
464
465 // SIGKILL fallback — non-blocking. If the driver was already
466 // exiting cleanly via stdin-close, this is a no-op.
467 if let Err(e) = server.process.start_kill() {
468 tracing::warn!("Failed to kill Playwright server in Drop: {}", e);
469 }
470 }
471
472 crate::tty_guard::restore();
473 }
474}
475
476impl std::fmt::Debug for Playwright {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 f.debug_struct("Playwright")
479 .field("guid", &self.guid())
480 .field("chromium", &self.chromium().name())
481 .field("firefox", &self.firefox().name())
482 .field("webkit", &self.webkit().name())
483 .field("selectors", &*self.selectors())
484 .finish()
485 }
486}
487
488// Note: Playwright testing is done via integration tests since it requires:
489// - A real Connection with object registry
490// - BrowserType objects already created and registered
491// - Protocol messages from the server
492// See: crates/playwright-core/tests/connection_integration.rs