Skip to main content

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