Skip to main content

playwright_rs/protocol/
debugger.rs

1//! Debugger — programmatic control of the Playwright Inspector "PAUSED" overlay.
2//!
3//! Available on every [`BrowserContext`](crate::protocol::BrowserContext) via
4//! [`debugger()`](crate::protocol::BrowserContext::debugger). Used by IDE
5//! integrations and inspector-style tools to pause execution at the next
6//! action call, then resume / step / run-to-location under programmatic
7//! control. Distinct from the MCP / agent codegen path.
8//!
9//! # Example
10//!
11//! ```no_run
12//! use playwright_rs::Playwright;
13//!
14//! #[tokio::main]
15//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//!     let pw = Playwright::launch().await?;
17//!     let browser = pw.chromium().launch().await?;
18//!     let context = browser.new_context().await?;
19//!     let dbg = context.debugger().await?;
20//!
21//!     // Ask Playwright to pause before the next action runs.
22//!     dbg.request_pause().await?;
23//!
24//!     // ... your IDE / tool decides when to step the user through ...
25//!
26//!     dbg.resume().await?;
27//!     browser.close().await?;
28//!     Ok(())
29//! }
30//! ```
31//!
32//! See: <https://playwright.dev/docs/api/class-debugger>
33
34use crate::error::Result;
35use crate::server::channel::Channel;
36use crate::server::channel_owner::{
37    ChannelOwner, ChannelOwnerImpl, DisposeReason, ParentOrConnection,
38};
39use crate::server::connection::ConnectionLike;
40use parking_lot::Mutex;
41use serde_json::Value;
42use std::any::Any;
43use std::future::Future;
44use std::pin::Pin;
45use std::sync::Arc;
46
47type PausedHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>>;
48type PausedHandler =
49    Arc<dyn Fn(Option<PausedDetails>) -> PausedHandlerFuture + Send + Sync + 'static>;
50
51/// Details of the currently paused execution, surfaced via the
52/// `pausedStateChanged` event when the inspector overlay is active.
53#[derive(Debug, Clone, PartialEq, Eq)]
54#[non_exhaustive]
55pub struct PausedDetails {
56    /// Source location the engine paused at, when available.
57    pub location: PausedLocation,
58    /// Title shown on the overlay (typically the action name).
59    pub title: String,
60    /// Stack trace as a string, when the server provides one.
61    pub stack: Option<String>,
62}
63
64/// Source location for [`PausedDetails`].
65#[derive(Debug, Clone, PartialEq, Eq)]
66#[non_exhaustive]
67pub struct PausedLocation {
68    pub file: String,
69    pub line: Option<i64>,
70    pub column: Option<i64>,
71}
72
73/// Programmatic interface to Playwright Inspector's pause / resume /
74/// step controls. See the module docs.
75#[derive(Clone)]
76pub struct Debugger {
77    base: ChannelOwnerImpl,
78    paused_details: Arc<Mutex<Option<PausedDetails>>>,
79    paused_handlers: Arc<Mutex<Vec<PausedHandler>>>,
80}
81
82impl Debugger {
83    pub fn new(
84        parent: ParentOrConnection,
85        type_name: String,
86        guid: Arc<str>,
87        initializer: Value,
88    ) -> Result<Self> {
89        Ok(Self {
90            base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
91            paused_details: Arc::new(Mutex::new(None)),
92            paused_handlers: Arc::new(Mutex::new(Vec::new())),
93        })
94    }
95
96    /// Asks Playwright to pause before the next action runs. The pause
97    /// is surfaced via the `pausedStateChanged` event (register a
98    /// handler with [`Debugger::on_paused_state_changed`]).
99    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
100    pub async fn request_pause(&self) -> Result<()> {
101        self.channel()
102            .send_no_result("requestPause", serde_json::json!({}))
103            .await
104    }
105
106    /// Resume execution from a paused state.
107    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
108    pub async fn resume(&self) -> Result<()> {
109        self.channel()
110            .send_no_result("resume", serde_json::json!({}))
111            .await
112    }
113
114    /// Step to the next action call, then pause again.
115    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
116    pub async fn next(&self) -> Result<()> {
117        self.channel()
118            .send_no_result("next", serde_json::json!({}))
119            .await
120    }
121
122    /// Run to a specific source location, then pause.
123    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
124    pub async fn run_to(&self, location: PausedLocation) -> Result<()> {
125        let mut loc = serde_json::json!({ "file": location.file });
126        if let Some(line) = location.line {
127            loc["line"] = serde_json::json!(line);
128        }
129        if let Some(column) = location.column {
130            loc["column"] = serde_json::json!(column);
131        }
132        self.channel()
133            .send_no_result("runTo", serde_json::json!({ "location": loc }))
134            .await
135    }
136
137    /// Returns the current paused-state details, or `None` if execution
138    /// is not currently paused. Updated as `pausedStateChanged` events
139    /// arrive.
140    pub fn paused_details(&self) -> Option<PausedDetails> {
141        self.paused_details.lock().clone()
142    }
143
144    /// Convenience: `paused_details().is_some()`.
145    pub fn is_paused(&self) -> bool {
146        self.paused_details.lock().is_some()
147    }
148
149    /// Register a handler for the `pausedStateChanged` event. The
150    /// handler receives `Some(details)` when execution becomes paused
151    /// and `None` when it resumes.
152    pub fn on_paused_state_changed<F, Fut>(&self, handler: F)
153    where
154        F: Fn(Option<PausedDetails>) -> Fut + Send + Sync + 'static,
155        Fut: Future<Output = Result<()>> + Send + 'static,
156    {
157        let h: PausedHandler = Arc::new(move |d| -> PausedHandlerFuture { Box::pin(handler(d)) });
158        self.paused_handlers.lock().push(h);
159    }
160}
161
162fn parse_paused_details(params: &Value) -> Option<PausedDetails> {
163    let pd = params.get("pausedDetails")?;
164    if pd.is_null() {
165        return None;
166    }
167    let location = pd.get("location")?;
168    let file = location.get("file")?.as_str()?.to_string();
169    let line = location.get("line").and_then(|v| v.as_i64());
170    let column = location.get("column").and_then(|v| v.as_i64());
171    let title = pd.get("title")?.as_str()?.to_string();
172    let stack = pd.get("stack").and_then(|v| v.as_str()).map(String::from);
173    Some(PausedDetails {
174        location: PausedLocation { file, line, column },
175        title,
176        stack,
177    })
178}
179
180impl ChannelOwner for Debugger {
181    fn guid(&self) -> &str {
182        self.base.guid()
183    }
184    fn type_name(&self) -> &str {
185        self.base.type_name()
186    }
187    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
188        self.base.parent()
189    }
190    fn connection(&self) -> Arc<dyn ConnectionLike> {
191        self.base.connection()
192    }
193    fn initializer(&self) -> &Value {
194        self.base.initializer()
195    }
196    fn channel(&self) -> &Channel {
197        self.base.channel()
198    }
199    fn dispose(&self, reason: DisposeReason) {
200        self.base.dispose(reason)
201    }
202    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
203        self.base.adopt(child)
204    }
205    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
206        self.base.add_child(guid, child)
207    }
208    fn remove_child(&self, guid: &str) {
209        self.base.remove_child(guid)
210    }
211
212    fn on_event(&self, method: &str, params: Value) {
213        if method == "pausedStateChanged" {
214            let details = parse_paused_details(&params);
215            *self.paused_details.lock() = details.clone();
216            let handlers = self.paused_handlers.lock().clone();
217            for h in handlers {
218                let d = details.clone();
219                tokio::spawn(async move {
220                    if let Err(e) = h(d).await {
221                        tracing::warn!("Debugger paused-state handler error: {}", e);
222                    }
223                });
224            }
225        }
226        self.base.on_event(method, params);
227    }
228
229    fn was_collected(&self) -> bool {
230        self.base.was_collected()
231    }
232
233    fn as_any(&self) -> &dyn Any {
234        self
235    }
236}
237
238impl std::fmt::Debug for Debugger {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        f.debug_struct("Debugger")
241            .field("guid", &self.guid())
242            .field("paused", &self.is_paused())
243            .finish()
244    }
245}