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//! ```ignore
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)]
54pub struct PausedDetails {
55    /// Source location the engine paused at, when available.
56    pub location: PausedLocation,
57    /// Title shown on the overlay (typically the action name).
58    pub title: String,
59    /// Stack trace as a string, when the server provides one.
60    pub stack: Option<String>,
61}
62
63/// Source location for [`PausedDetails`].
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct PausedLocation {
66    pub file: String,
67    pub line: Option<i64>,
68    pub column: Option<i64>,
69}
70
71/// Programmatic interface to Playwright Inspector's pause / resume /
72/// step controls. See the module docs.
73#[derive(Clone)]
74pub struct Debugger {
75    base: ChannelOwnerImpl,
76    paused_details: Arc<Mutex<Option<PausedDetails>>>,
77    paused_handlers: Arc<Mutex<Vec<PausedHandler>>>,
78}
79
80impl Debugger {
81    pub fn new(
82        parent: ParentOrConnection,
83        type_name: String,
84        guid: Arc<str>,
85        initializer: Value,
86    ) -> Result<Self> {
87        Ok(Self {
88            base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
89            paused_details: Arc::new(Mutex::new(None)),
90            paused_handlers: Arc::new(Mutex::new(Vec::new())),
91        })
92    }
93
94    /// Asks Playwright to pause before the next action runs. The pause
95    /// is surfaced via the `pausedStateChanged` event (register a
96    /// handler with [`Debugger::on_paused_state_changed`]).
97    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
98    pub async fn request_pause(&self) -> Result<()> {
99        self.channel()
100            .send_no_result("requestPause", serde_json::json!({}))
101            .await
102    }
103
104    /// Resume execution from a paused state.
105    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
106    pub async fn resume(&self) -> Result<()> {
107        self.channel()
108            .send_no_result("resume", serde_json::json!({}))
109            .await
110    }
111
112    /// Step to the next action call, then pause again.
113    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
114    pub async fn next(&self) -> Result<()> {
115        self.channel()
116            .send_no_result("next", serde_json::json!({}))
117            .await
118    }
119
120    /// Run to a specific source location, then pause.
121    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
122    pub async fn run_to(&self, location: PausedLocation) -> Result<()> {
123        let mut loc = serde_json::json!({ "file": location.file });
124        if let Some(line) = location.line {
125            loc["line"] = serde_json::json!(line);
126        }
127        if let Some(column) = location.column {
128            loc["column"] = serde_json::json!(column);
129        }
130        self.channel()
131            .send_no_result("runTo", serde_json::json!({ "location": loc }))
132            .await
133    }
134
135    /// Returns the current paused-state details, or `None` if execution
136    /// is not currently paused. Updated as `pausedStateChanged` events
137    /// arrive.
138    pub fn paused_details(&self) -> Option<PausedDetails> {
139        self.paused_details.lock().clone()
140    }
141
142    /// Convenience: `paused_details().is_some()`.
143    pub fn is_paused(&self) -> bool {
144        self.paused_details.lock().is_some()
145    }
146
147    /// Register a handler for the `pausedStateChanged` event. The
148    /// handler receives `Some(details)` when execution becomes paused
149    /// and `None` when it resumes.
150    pub fn on_paused_state_changed<F, Fut>(&self, handler: F)
151    where
152        F: Fn(Option<PausedDetails>) -> Fut + Send + Sync + 'static,
153        Fut: Future<Output = Result<()>> + Send + 'static,
154    {
155        let h: PausedHandler = Arc::new(move |d| -> PausedHandlerFuture { Box::pin(handler(d)) });
156        self.paused_handlers.lock().push(h);
157    }
158}
159
160fn parse_paused_details(params: &Value) -> Option<PausedDetails> {
161    let pd = params.get("pausedDetails")?;
162    if pd.is_null() {
163        return None;
164    }
165    let location = pd.get("location")?;
166    let file = location.get("file")?.as_str()?.to_string();
167    let line = location.get("line").and_then(|v| v.as_i64());
168    let column = location.get("column").and_then(|v| v.as_i64());
169    let title = pd.get("title")?.as_str()?.to_string();
170    let stack = pd.get("stack").and_then(|v| v.as_str()).map(String::from);
171    Some(PausedDetails {
172        location: PausedLocation { file, line, column },
173        title,
174        stack,
175    })
176}
177
178impl ChannelOwner for Debugger {
179    fn guid(&self) -> &str {
180        self.base.guid()
181    }
182    fn type_name(&self) -> &str {
183        self.base.type_name()
184    }
185    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
186        self.base.parent()
187    }
188    fn connection(&self) -> Arc<dyn ConnectionLike> {
189        self.base.connection()
190    }
191    fn initializer(&self) -> &Value {
192        self.base.initializer()
193    }
194    fn channel(&self) -> &Channel {
195        self.base.channel()
196    }
197    fn dispose(&self, reason: DisposeReason) {
198        self.base.dispose(reason)
199    }
200    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
201        self.base.adopt(child)
202    }
203    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
204        self.base.add_child(guid, child)
205    }
206    fn remove_child(&self, guid: &str) {
207        self.base.remove_child(guid)
208    }
209
210    fn on_event(&self, method: &str, params: Value) {
211        if method == "pausedStateChanged" {
212            let details = parse_paused_details(&params);
213            *self.paused_details.lock() = details.clone();
214            let handlers = self.paused_handlers.lock().clone();
215            for h in handlers {
216                let d = details.clone();
217                tokio::spawn(async move {
218                    if let Err(e) = h(d).await {
219                        tracing::warn!("Debugger paused-state handler error: {}", e);
220                    }
221                });
222            }
223        }
224        self.base.on_event(method, params);
225    }
226
227    fn was_collected(&self) -> bool {
228        self.base.was_collected()
229    }
230
231    fn as_any(&self) -> &dyn Any {
232        self
233    }
234}
235
236impl std::fmt::Debug for Debugger {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        f.debug_struct("Debugger")
239            .field("guid", &self.guid())
240            .field("paused", &self.is_paused())
241            .finish()
242    }
243}