Skip to main content

playwright_cdp/
page.rs

1//! `Page` — a browser tab. Tracks its main execution context, drives
2//! navigation, evaluation, screenshots, and produces locators.
3
4use crate::accessibility::Accessibility;
5use crate::browser::Browser;
6use crate::api_request::APIRequestContext;
7use crate::cdp::session::CdpSession;
8use crate::clock::Clock;
9use crate::coverage::Coverage;
10use crate::js_handle::JSHandle;
11use crate::video::Video;
12use crate::web_socket::{WebSocket, WebSocketRegistry};
13use crate::web_storage::WebStorage;
14use crate::download::{Download, DownloadState, DownloadStateCell};
15use crate::element_handle::ElementHandle;
16use crate::error::{Error, Result};
17use crate::file_chooser::FileChooser;
18use crate::frame::Frame;
19use crate::frame_locator::FrameLocator;
20use crate::keyboard::Keyboard;
21use crate::locator::Locator;
22use crate::mouse::Mouse;
23use crate::network::{network_tracker, NetworkStore, RequestHandler, ResponseHandler};
24use crate::options::{
25    DragToOptions, EmulateMediaOptions, GotoOptions, ScreenshotOptions, WaitForFunctionOptions, WaitUntil,
26};
27use crate::request::Request;
28use crate::response::Response;
29use crate::route::{route_listener, Route, RouteEntry};
30use crate::selectors;
31use crate::touchscreen::Touchscreen;
32use crate::types::{AriaRole, ConsoleMessage, Headers, Viewport};
33use crate::worker::Worker;
34use base64::Engine;
35use parking_lot::Mutex;
36use serde::de::DeserializeOwned;
37use serde_json::{json, Value};
38use std::collections::HashMap;
39use std::future::Future;
40use std::path::PathBuf;
41use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
42use std::sync::Arc;
43use std::time::Duration;
44use tokio::sync::broadcast;
45use tokio::sync::oneshot;
46
47/// A page (tab) in a browser.
48#[derive(Clone)]
49pub struct Page {
50    inner: Arc<PageInner>,
51}
52
53type CloseHandler =
54    Arc<dyn Fn() -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
55
56/// An erased popup handler: takes the popup [`Page`] and resolves when done.
57type PopupHandler =
58    Arc<dyn Fn(Page) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
59
60/// An erased, callable binding: takes the JS args and returns a JSON value.
61type ExposedBindingHandler =
62    Arc<dyn Fn(Vec<Value>) -> std::pin::Pin<Box<dyn Future<Output = Value> + Send>> + Send + Sync>;
63
64struct PageInner {
65    browser: Browser,
66    session: Arc<CdpSession>,
67    target_id: String,
68    main_world_ctx: Arc<Mutex<Option<i64>>>,
69    /// Per-frame main-world execution contexts: `frame_id → context id`.
70    /// Populated by the `context_tracker` from
71    /// `Runtime.executionContextCreated { auxData.frameId }`. Used to scope
72    /// `Frame` evaluate/locator resolution to a child frame's own context.
73    frame_contexts: Arc<Mutex<HashMap<String, i64>>>,
74    default_timeout_ms: AtomicU64,
75    default_navigation_timeout_ms: AtomicU64,
76    viewport: Mutex<Option<Viewport>>,
77    mouse_pos: Arc<Mutex<(f64, f64)>>,
78    network_store: Arc<NetworkStore>,
79    on_request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
80    on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
81    on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
82    frames: Arc<Mutex<HashMap<String, FrameData>>>,
83    main_frame_id: Arc<Mutex<Option<String>>>,
84    route_handlers: Arc<Mutex<Vec<RouteEntry>>>,
85    route_started: AtomicBool,
86    on_close_handlers: Mutex<Vec<CloseHandler>>,
87    // Popup (window.open) capture: erased handlers + a one-shot flag for
88    // enabling page-session child-target auto-attach.
89    on_popup_handlers: Arc<Mutex<Vec<PopupHandler>>>,
90    popup_started: AtomicBool,
91    closed: Mutex<bool>,
92    // Download capture: the temp dir (kept alive for the page's lifetime), its
93    // path, per-guid progress state, and a one-shot flag for behavior setup.
94    download_dir: Mutex<Option<Arc<tempfile::TempDir>>>,
95    download_path: Mutex<Option<PathBuf>>,
96    download_states: Arc<Mutex<HashMap<String, DownloadStateCell>>>,
97    download_started: AtomicBool,
98    // File-chooser interception: a one-shot flag for enabling interception.
99    filechooser_started: AtomicBool,
100    // Worker capture: a one-shot flag for enabling page-session auto-attach.
101    worker_started: AtomicBool,
102    // Touchscreen: a one-shot flag for enabling touch emulation. Arc-wrapped so
103    // it can be shared across `Touchscreen` clones produced from this page.
104    touch_emulation_started: Arc<AtomicBool>,
105    // WebSocket capture: a registry that subscribes to the page session and
106    // accumulates one `WebSocket` per `Network.webSocket*` connection. Attached
107    // in `Page::attach` BEFORE `Network.enable` so no event is missed.
108    web_socket_registry: Arc<WebSocketRegistry>,
109    // Optional pre-configured video output path (Playwright configures the video
110    // path up front at context/page creation time).
111    video_path: Mutex<Option<PathBuf>>,
112}
113
114/// Cached frame-tree data for one frame.
115#[derive(Clone)]
116pub(crate) struct FrameData {
117    pub url: String,
118    pub name: String,
119    pub parent_id: Option<String>,
120    pub detached: bool,
121}
122
123impl Page {
124    /// Attach to an existing target by session/target id. Enables domains,
125    /// injects the selector engine, and applies context defaults.
126    #[allow(clippy::too_many_arguments)]
127    pub(crate) async fn attach(
128        browser: Browser,
129        session_id: String,
130        target_id: String,
131        init_scripts: &[String],
132        default_timeout_ms: u64,
133        extra_headers: Option<&Headers>,
134        user_agent: Option<&str>,
135        viewport: Option<Viewport>,
136    ) -> Result<Page> {
137        let session = Arc::new(CdpSession::target(
138            browser.connection().clone(),
139            session_id,
140        ));
141        session.set_default_timeout_ms(default_timeout_ms);
142
143        let main_world_ctx: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
144        let frame_contexts: Arc<Mutex<HashMap<String, i64>>> =
145            Arc::new(Mutex::new(HashMap::new()));
146        // The main frame id and frame store are shared with the context tracker
147        // (so it can tell the main frame's default context apart from a child
148        // frame's) and the frame tracker.
149        let frames_store: Arc<Mutex<HashMap<String, FrameData>>> =
150            Arc::new(Mutex::new(HashMap::new()));
151        let main_frame_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
152        let ctx_rx = session.subscribe();
153        // Context-tracking + engine re-injection task. Subscribe before
154        // enabling Runtime so the initial executionContextCreated is captured.
155        {
156            let session_for_task = Arc::clone(&session);
157            let ctx_cell = Arc::clone(&main_world_ctx);
158            let frame_ctx_cell = Arc::clone(&frame_contexts);
159            let main_id_cell = Arc::clone(&main_frame_id);
160            tokio::spawn(async move {
161                context_tracker(
162                    ctx_rx,
163                    session_for_task,
164                    ctx_cell,
165                    frame_ctx_cell,
166                    main_id_cell,
167                )
168                .await;
169            });
170        }
171
172        let network_store = NetworkStore::new();
173        // Handler lists shared between the page and the network tracker task.
174        let on_request_handlers: Arc<Mutex<Vec<RequestHandler>>> = Arc::new(Mutex::new(Vec::new()));
175        let on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>> =
176            Arc::new(Mutex::new(Vec::new()));
177        let on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>> =
178            Arc::new(Mutex::new(Vec::new()));
179
180        // Network capture task. Subscribe before Network.enable so no event is missed.
181        {
182            let net_rx = session.subscribe();
183            let session_for_task = Arc::clone(&session);
184            let store_for_task = Arc::clone(&network_store);
185            let on_request = Arc::clone(&on_request_handlers);
186            let on_response = Arc::clone(&on_response_handlers);
187            let on_failed = Arc::clone(&on_requestfailed_handlers);
188            tokio::spawn(async move {
189                network_tracker(
190                    net_rx,
191                    session_for_task,
192                    store_for_task,
193                    on_request,
194                    on_response,
195                    on_failed,
196                )
197                .await;
198            });
199        }
200
201        let page = Page {
202            inner: Arc::new(PageInner {
203                browser,
204                session: Arc::clone(&session),
205                target_id,
206                main_world_ctx: Arc::clone(&main_world_ctx),
207                frame_contexts: Arc::clone(&frame_contexts),
208                default_timeout_ms: AtomicU64::new(default_timeout_ms),
209                default_navigation_timeout_ms: AtomicU64::new(default_timeout_ms),
210                viewport: Mutex::new(viewport),
211                mouse_pos: Arc::new(Mutex::new((0.0, 0.0))),
212                network_store,
213                on_request_handlers,
214                on_response_handlers,
215                on_requestfailed_handlers,
216                frames: Arc::clone(&frames_store),
217                main_frame_id: Arc::clone(&main_frame_id),
218                route_handlers: Arc::new(Mutex::new(Vec::new())),
219                route_started: AtomicBool::new(false),
220                on_close_handlers: Mutex::new(Vec::new()),
221                on_popup_handlers: Arc::new(Mutex::new(Vec::new())),
222                popup_started: AtomicBool::new(false),
223                closed: Mutex::new(false),
224                download_dir: Mutex::new(None),
225                download_path: Mutex::new(None),
226                download_states: Arc::new(Mutex::new(HashMap::new())),
227                download_started: AtomicBool::new(false),
228                filechooser_started: AtomicBool::new(false),
229                worker_started: AtomicBool::new(false),
230                touch_emulation_started: Arc::new(AtomicBool::new(false)),
231                web_socket_registry: Arc::new(WebSocketRegistry::new()),
232                video_path: Mutex::new(None),
233            }),
234        };
235
236        // WebSocket capture: attach the registry's subscriber BEFORE
237        // Network.enable is sent so no `Network.webSocket*` event is missed.
238        page.inner.web_socket_registry.attach(&page.inner.session);
239
240        // Live frame-tree tracking (urls/names/children) for `Frame`.
241        {
242            let rx = session.subscribe();
243            let frames_store = Arc::clone(&page.inner.frames);
244            let main_id = Arc::clone(&page.inner.main_frame_id);
245            tokio::spawn(async move {
246                frame_tracker(rx, frames_store, main_id).await;
247            });
248        }
249        // Populate the frame tree eagerly so `main_frame()` is always valid.
250        let _ = page.refresh_frame_tree().await;
251
252        // Domain enablement. Order matters only in that Runtime must be last
253        // (after the context receiver exists) so contexts are captured.
254        let _ = session.send("Page.enable", json!({})).await;
255        let _ = session.send("Runtime.enable", json!({})).await;
256        let _ = session.send("Network.enable", json!({})).await;
257        let _ = session.send("Log.enable", json!({})).await;
258
259        // Inject selector engine + user init scripts for all future documents.
260        let mut source = String::from(selectors::INJECTED_SCRIPT);
261        for s in init_scripts {
262            source.push_str("\n");
263            source.push_str(s);
264        }
265        let _ = session
266            .send(
267                "Page.addScriptToEvaluateOnNewDocument",
268                json!({ "source": source }),
269            )
270            .await;
271
272        // Ensure the engine is present in whatever context exists right now.
273        page.ensure_engine_in_current_context().await;
274
275        if let Some(headers) = extra_headers {
276            let _ = page.set_extra_http_headers(headers.clone()).await;
277        }
278        if let Some(ua) = user_agent {
279            let _ = session
280                .send("Emulation.setUserAgentOverride", json!({ "userAgent": ua }))
281                .await;
282        }
283        // Drop the mutex guard before the `.await` below so the (non-`Send`)
284        // `parking_lot::MutexGuard` is not held across an await point — this
285        // keeps `Page::attach`'s returned future `Send`, which `tokio::spawn`
286        // callers (e.g. `on_popup`) require.
287        let vp = page.inner.viewport.lock().as_ref().copied();
288        if let Some(vp) = vp {
289            let _ = page.set_viewport_size(vp).await;
290        }
291
292        Ok(page)
293    }
294
295    // --- accessors ---
296
297    pub(crate) fn session(&self) -> &CdpSession {
298        &self.inner.session
299    }
300
301    /// The owning CDP session as a cloned `Arc`, for building a [`JSHandle`]
302    /// (which retains its own `Arc<CdpSession>`).
303    pub(crate) fn session_arc(&self) -> Arc<CdpSession> {
304        Arc::clone(&self.inner.session)
305    }
306
307    pub(crate) fn context_id(&self) -> Option<i64> {
308        self.inner.main_world_ctx.lock().as_ref().copied()
309    }
310
311    /// The execution context to use for page-level evaluation.
312    ///
313    /// Returns `None` intentionally: omitting `contextId` from
314    /// `Runtime.evaluate` lets CDP resolve the page's default (main-world)
315    /// context itself, which is always fresh and avoids the stale-context race
316    /// that occurs right after a navigation (when the old context is destroyed
317    /// before the new one is reported and our tracker has not caught up).
318    ///
319    /// The tracked id ([`context_id`](Self::context_id)) is still maintained
320    /// for callers that need a concrete handle (e.g. [`JSHandle`] context
321    /// tagging) and to seed per-frame lookups.
322    pub(crate) async fn ctx(&self) -> Option<i64> {
323        None
324    }
325
326    /// The execution-context id to use when targeting `frame_id`.
327    ///
328    /// Returns `None` for the page's main frame (so its ops run in CDP's
329    /// default, race-free context) and `Some(child_ctx)` for a child frame's
330    /// own context. If a child frame's context has not been reported yet,
331    /// returns `None` as a safe fallback (default context).
332    pub(crate) fn context_for_frame(&self, frame_id: &str) -> Option<i64> {
333        // The main frame uses the default context (no id) to stay race-free.
334        if self
335            .inner
336            .main_frame_id
337            .lock()
338            .as_deref()
339            .map(|m| m == frame_id)
340            .unwrap_or(false)
341        {
342            return None;
343        }
344        self.inner
345            .frame_contexts
346            .lock()
347            .get(frame_id)
348            .copied()
349            // Unknown child frame: fall back to the default context.
350            .or(None)
351    }
352
353    /// Like [`context_for_frame`](Self::context_for_frame), but waits briefly
354    /// for a child frame's context to arrive (the context-tracker is
355    /// asynchronous and may lag a freshly-attached frame). The main frame
356    /// returns `None` immediately.
357    pub(crate) async fn ctx_for_frame(&self, frame_id: &str) -> Option<i64> {
358        // Main frame: default context, no waiting.
359        if self
360            .inner
361            .main_frame_id
362            .lock()
363            .as_deref()
364            .map(|m| m == frame_id)
365            .unwrap_or(false)
366        {
367            return None;
368        }
369        let deadline = tokio::time::Instant::now() + Duration::from_millis(2000);
370        loop {
371            if let Some(id) = self.inner.frame_contexts.lock().get(frame_id).copied() {
372                return Some(id);
373            }
374            if tokio::time::Instant::now() >= deadline {
375                // Unknown child frame: fall back to the default context rather
376                // than erroring.
377                return None;
378            }
379            tokio::time::sleep(Duration::from_millis(20)).await;
380        }
381    }
382
383    pub fn browser(&self) -> Browser {
384        self.inner.browser.clone()
385    }
386
387    pub fn target_id(&self) -> &str {
388        &self.inner.target_id
389    }
390
391    pub fn is_closed(&self) -> bool {
392        *self.inner.closed.lock()
393    }
394
395    /// Set the default timeout (ms) for all actions on this page, mirroring
396    /// Playwright's `page.setDefaultTimeout`. The value is stored on the page
397    /// ([`PageInner::default_timeout_ms`]) and propagated to the underlying
398    /// CDP session's command timeout.
399    pub fn set_default_timeout(&self, ms: u64) {
400        self.inner.default_timeout_ms.store(ms, Ordering::Relaxed);
401        self.inner.session.set_default_timeout_ms(ms);
402    }
403
404    pub(crate) fn default_timeout(&self) -> Duration {
405        Duration::from_millis(self.inner.default_timeout_ms.load(Ordering::Relaxed))
406    }
407
408    pub(crate) fn default_navigation_timeout(&self) -> Duration {
409        Duration::from_millis(self.inner.default_navigation_timeout_ms.load(Ordering::Relaxed))
410    }
411
412    async fn ensure_engine_in_current_context(&self) {
413        // Idempotent: the bundle early-returns if already installed.
414        let _ = selectors::eval_context(
415            &self.inner.session,
416            self.ctx().await,
417            "(arg) => { void arg; }",
418            Value::Null,
419        )
420        .await;
421    }
422
423    // --- navigation ---
424
425    /// Navigate to `url`.
426    pub async fn goto(
427        &self,
428        url: &str,
429        opts: Option<GotoOptions>,
430    ) -> Result<Option<Response>> {
431        let opts = opts.unwrap_or_default();
432        let wait = opts.wait_until_or_default();
433
434        // Subscribe before navigating so lifecycle events aren't missed.
435        let mut rx = self.inner.session.subscribe();
436        let mut params = json!({ "url": url });
437        if let Some(referer) = &opts.referer {
438            params["referer"] = json!(referer);
439        }
440        let nav = self.inner.session.send("Page.navigate", params).await?;
441        if let Some(err) = nav.get("errorText").and_then(|v| v.as_str()) {
442            return Err(Error::ProtocolError(format!("navigation to {url} failed: {err}")));
443        }
444        let loader_id = nav.get("loaderId").and_then(|v| v.as_str()).map(String::from);
445
446        let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
447        self.wait_lifecycle(&mut rx, loader_id.as_deref(), wait, timeout, url)
448            .await?;
449
450        // Correlate the main-frame document response for this navigation.
451        let response = match &loader_id {
452            Some(lid) => self.wait_for_nav_response(lid).await,
453            None => None,
454        };
455        Ok(response)
456    }
457
458    /// Poll the network store briefly for the document response of `loader_id`.
459    async fn wait_for_nav_response(&self, loader_id: &str) -> Option<Response> {
460        let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
461        loop {
462            if let Some(r) = self.inner.network_store.response_for_loader(loader_id) {
463                return Some(r);
464            }
465            if tokio::time::Instant::now() >= deadline {
466                return None;
467            }
468            tokio::time::sleep(Duration::from_millis(50)).await;
469        }
470    }
471
472    /// Wait until the in-flight request count stays at 0 for ~500ms (or deadline).
473    async fn wait_network_idle(&self, deadline: tokio::time::Instant) {
474        let window = Duration::from_millis(500);
475        loop {
476            if self.inner.network_store.inflight() == 0 {
477                let settled_at = tokio::time::Instant::now();
478                loop {
479                    if self.inner.network_store.inflight() != 0 {
480                        break; // a new request started; restart
481                    }
482                    if tokio::time::Instant::now().duration_since(settled_at) >= window {
483                        return; // stayed idle for the window
484                    }
485                    if tokio::time::Instant::now() >= deadline {
486                        return; // overall nav timeout
487                    }
488                    tokio::time::sleep(Duration::from_millis(50)).await;
489                }
490            }
491            if tokio::time::Instant::now() >= deadline {
492                return;
493            }
494            tokio::time::sleep(Duration::from_millis(50)).await;
495        }
496    }
497
498    async fn wait_lifecycle(
499        &self,
500        rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
501        loader_id: Option<&str>,
502        wait: WaitUntil,
503        timeout: Duration,
504        url: &str,
505    ) -> Result<()> {
506        if matches!(wait, WaitUntil::Commit) {
507            return Ok(()); // Page.navigate returned => committed.
508        }
509        let target_name = match wait {
510            WaitUntil::Load => "load",
511            WaitUntil::DomContentLoaded => "DOMContentLoaded",
512            WaitUntil::NetworkIdle => "load", // then settle below
513            WaitUntil::Commit => "load",
514        };
515
516        let deadline = tokio::time::Instant::now() + timeout;
517        loop {
518            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
519            match tokio::time::timeout(remaining, rx.recv()).await {
520                Err(_) => {
521                    return Err(Error::NavigationTimeout {
522                        url: url.to_string(),
523                        duration_ms: timeout.as_millis() as u64,
524                    })
525                }
526                Ok(Err(broadcast::error::RecvError::Closed)) => {
527                    return Err(Error::ChannelClosed);
528                }
529                Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
530                Ok(Ok(ev)) => {
531                    // Some Chrome builds emit modern `Page.lifecycleEvent`
532                    // (carrying `name`), others emit the legacy discrete events
533                    // (`Page.loadEventFired`, `Page.domContentEventFired`).
534                    // Handle both.
535                    let matched = match ev.method.as_str() {
536                        "Page.lifecycleEvent" => {
537                            let same_loader = loader_id
538                                .map(|l| {
539                                    ev.params.get("loaderId").and_then(|v| v.as_str()) == Some(l)
540                                })
541                                .unwrap_or(true);
542                            let name = ev.params.get("name").and_then(|v| v.as_str()).unwrap_or("");
543                            same_loader && name == target_name
544                        }
545                        "Page.loadEventFired" => target_name == "load",
546                        "Page.domContentEventFired" => target_name == "DOMContentLoaded",
547                        _ => false,
548                    };
549                    if matched {
550                        if matches!(wait, WaitUntil::NetworkIdle) {
551                            // Settle: wait until in-flight requests hit 0 and stay there
552                            // for ~500ms (Playwright's networkidle window).
553                            self.wait_network_idle(deadline).await;
554                        }
555                        return Ok(());
556                    }
557                }
558            }
559        }
560    }
561
562    /// Reload the page.
563    pub async fn reload(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
564        let opts = opts.unwrap_or_default();
565        let wait = opts.wait_until_or_default();
566        let mut rx = self.inner.session.subscribe();
567        let _ = self.inner.session.send("Page.reload", json!({})).await?;
568        let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
569        self.wait_lifecycle(&mut rx, None, wait, timeout, "reload").await?;
570        Ok(None)
571    }
572
573    /// Wait for a given document lifecycle state.
574    pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
575        let state = state.unwrap_or_default();
576        let mut rx = self.inner.session.subscribe();
577        self.wait_lifecycle(&mut rx, None, state, self.default_timeout(), "wait_for_load_state")
578            .await
579    }
580
581    /// Navigate one entry back in history (best-effort; returns no response).
582    pub async fn go_back(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
583        self.history_nav("history.back()", opts).await
584    }
585
586    /// Navigate one entry forward in history (best-effort; returns no response).
587    pub async fn go_forward(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
588        self.history_nav("history.forward()", opts).await
589    }
590
591    async fn history_nav(
592        &self,
593        expr: &str,
594        opts: Option<GotoOptions>,
595    ) -> Result<Option<Response>> {
596        let opts = opts.unwrap_or_default();
597        // `history.back()/forward()` returns true iff a navigation occurred.
598        let navigated: bool = self.evaluate(expr).await.unwrap_or(false);
599        if navigated {
600            let wait = opts.wait_until_or_default();
601            let mut rx = self.inner.session.subscribe();
602            let timeout = opts
603                .timeout
604                .unwrap_or_else(|| self.default_navigation_timeout());
605            let _ = self
606                .wait_lifecycle(&mut rx, None, wait, timeout, "history navigation")
607                .await;
608        }
609        Ok(None)
610    }
611
612    /// Wait until the page URL matches `url` (exact, or `*` glob), then optionally
613    /// wait for a lifecycle state.
614    pub async fn wait_for_url(&self, url: &str, opts: Option<GotoOptions>) -> Result<()> {
615        let opts = opts.unwrap_or_default();
616        let timeout = opts
617            .timeout
618            .unwrap_or_else(|| self.default_navigation_timeout());
619        let deadline = tokio::time::Instant::now() + timeout;
620        loop {
621            let cur = self.url().await.unwrap_or_default();
622            if glob_matches(url, &cur) {
623                break;
624            }
625            if tokio::time::Instant::now() >= deadline {
626                return Err(Error::Timeout(format!(
627                    "wait_for_url '{url}' timed out after {}ms (last: '{cur}')",
628                    timeout.as_millis()
629                )));
630            }
631            tokio::time::sleep(Duration::from_millis(100)).await;
632        }
633        if let Some(state) = opts.wait_until {
634            self.wait_for_load_state(Some(state)).await?;
635        }
636        Ok(())
637    }
638
639    /// Set the default navigation timeout (ms), separate from action timeout.
640    pub fn set_default_navigation_timeout(&self, ms: u64) {
641        self.inner
642            .default_navigation_timeout_ms
643            .store(ms, Ordering::Relaxed);
644    }
645
646    /// Emulate CSS media (media / color-scheme / reduced-motion).
647    pub async fn emulate_media(&self, opts: Option<EmulateMediaOptions>) -> Result<()> {
648        use crate::options::{ColorScheme, Media, ReducedMotion};
649        let opts = opts.unwrap_or_default();
650        let mut params = json!({});
651        if let Some(m) = opts.media {
652            params["media"] = json!(match m {
653                Media::Screen => "screen",
654                Media::Print => "print",
655            });
656        }
657        let mut features = Vec::new();
658        if let Some(cs) = opts.color_scheme {
659            features.push(json!({
660                "name": "prefers-color-scheme",
661                "value": match cs {
662                    ColorScheme::Light => "light",
663                    ColorScheme::Dark => "dark",
664                    ColorScheme::NoPreference => "no-preference",
665                }
666            }));
667        }
668        if let Some(rm) = opts.reduced_motion {
669            features.push(json!({
670                "name": "prefers-reduced-motion",
671                "value": match rm {
672                    ReducedMotion::Reduce => "reduce",
673                    ReducedMotion::NoPreference => "no-preference",
674                }
675            }));
676        }
677        if !features.is_empty() {
678            params["features"] = json!(features);
679        }
680        self.inner
681            .session
682            .send("Emulation.setEmulatedMedia", params)
683            .await
684            .map(|_: Value| ())
685    }
686
687    /// Toggle network offline emulation.
688    pub async fn set_offline(&self, offline: bool) -> Result<()> {
689        let params = if offline {
690            json!({ "offline": true, "latency": 0, "downloadThroughput": 0, "uploadThroughput": 0 })
691        } else {
692            json!({ "offline": false, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1 })
693        };
694        self.inner
695            .session
696            .send("Network.emulateNetworkConditions", params)
697            .await
698            .map(|_: Value| ())
699    }
700
701    /// Drag the element matched by `source` onto the element matched by `target`.
702    pub async fn drag_and_drop(
703        &self,
704        source: &str,
705        target: &str,
706        options: Option<DragToOptions>,
707    ) -> Result<()> {
708        let src = self.locator(source);
709        let tgt = self.locator(target);
710        src.drag_to(&tgt, options).await
711    }
712
713    // --- content & evaluation ---
714
715    pub async fn url(&self) -> Result<String> {
716        self.evaluate::<String>("location.href").await
717    }
718
719    pub async fn title(&self) -> Result<String> {
720        self.evaluate::<String>("document.title").await
721    }
722
723    pub async fn content(&self) -> Result<String> {
724        self.evaluate::<String>("document.documentElement.outerHTML").await
725    }
726
727    pub async fn set_content(&self, html: &str) -> Result<()> {
728        let _ = selectors::eval_context(
729            &self.inner.session,
730            self.ctx().await,
731            "(html) => { document.open(); document.write(html); document.close(); }",
732            json!(html),
733        )
734        .await?;
735        Ok(())
736    }
737
738    /// Evaluate a JS expression in the main world, returning a typed result.
739    ///
740    /// `expression` is wrapped as `(arg) => { return (<expression>); }`.
741    pub async fn evaluate<R: DeserializeOwned>(&self, expression: &str) -> Result<R> {
742        let function = format!("(arg) => {{ return ({expression}); }}");
743        let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, Value::Null)
744            .await?;
745        serde_json::from_value::<R>(v).map_err(Error::from)
746    }
747
748    /// Evaluate a JS expression in the main world, returning a [`JSHandle`] to
749    /// the result (returned by reference, not by value). Mirrors Playwright's
750    /// `page.evaluateHandle`.
751    ///
752    /// Like [`evaluate`](Self::evaluate), `expression` is wrapped as
753    /// `(arg) => { return (<expression>); }` and run against the page's main
754    /// world. The returned `Runtime.RemoteObjectId` is captured and wrapped in
755    /// a [`JSHandle`]. Returns an error if the evaluation yields `null`/
756    /// `undefined` (no remote object to reference).
757    pub async fn evaluate_handle(&self, expression: &str) -> Result<JSHandle> {
758        let function = format!("(arg) => {{ return ({expression}); }}");
759        let object_id = selectors::eval_context_handle(
760            &self.inner.session,
761            self.ctx().await,
762            &function,
763            Value::Null,
764        )
765        .await?
766        .ok_or_else(|| {
767            Error::ProtocolError(
768                "evaluate_handle returned no remote object (null/undefined result)".into(),
769            )
770        })?;
771        Ok(JSHandle::with_context(
772            Arc::clone(&self.inner.session),
773            object_id,
774            self.context_id(),
775        ))
776    }
777
778    /// Evaluate a JS expression with a JSON-serializable argument.
779    pub async fn evaluate_with_arg<R: DeserializeOwned, T: serde::Serialize>(
780        &self,
781        expression: &str,
782        arg: &T,
783    ) -> Result<R> {
784        let function = format!("(arg) => {{ return ({expression}); }}");
785        let arg_val = serde_json::to_value(arg)?;
786        let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val)
787            .await?;
788        serde_json::from_value::<R>(v).map_err(Error::from)
789    }
790
791    /// Poll `expression` until it evaluates to a truthy value, then return the
792    /// deserialized result. Mirrors Playwright's `page.waitForFunction`.
793    ///
794    /// `expression` is wrapped as `(arg) => { return (<expression>); }` (the
795    /// same form as [`Page::evaluate`]). A value is considered truthy unless it
796    /// is `null`, `false`, `0`, or `""`. On timeout (default 30s) this returns
797    /// [`Error::Timeout`]. Polls every `polling_interval` ms (default 100).
798    pub async fn wait_for_function<R: DeserializeOwned>(
799        &self,
800        expression: &str,
801        arg: Option<Value>,
802        options: Option<WaitForFunctionOptions>,
803    ) -> Result<R> {
804        let opts = options.unwrap_or_default();
805        let timeout = Duration::from_millis(opts.timeout.unwrap_or_else(|| self.default_timeout().as_millis() as f64) as u64);
806        let poll = Duration::from_millis(opts.polling_interval.unwrap_or(100.0) as u64);
807        let function = format!("(arg) => {{ return ({expression}); }}");
808        let arg_val = arg.unwrap_or(Value::Null);
809
810        let deadline = tokio::time::Instant::now() + timeout;
811        loop {
812            let v =
813                selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val.clone())
814                    .await?;
815            if is_truthy(&v) {
816                return serde_json::from_value::<R>(v).map_err(Error::from);
817            }
818            if tokio::time::Instant::now() >= deadline {
819                return Err(Error::Timeout(format!(
820                    "wait_for_function timed out after {}ms (expression: {expression})",
821                    timeout.as_millis()
822                )));
823            }
824            tokio::time::sleep(poll).await;
825        }
826    }
827
828    /// Evaluate `expression` against the first element matching `selector`.
829    pub async fn eval_on_selector<R: DeserializeOwned>(
830        &self,
831        selector: &str,
832        expression: &str,
833    ) -> Result<R> {
834        let object_id = self
835            .resolve_strict(selector, Some(0))
836            .await?
837            .ok_or_else(|| Error::ElementNotFound(selector.to_string()))?;
838        let function = format!("(el) => {{ return ({expression}); }}");
839        let v = selectors::eval_object(&self.inner.session, &object_id, &function, Value::Null)
840            .await?;
841        self.release_object(&object_id).await;
842        serde_json::from_value::<R>(v).map_err(Error::from)
843    }
844
845    /// Evaluate `expression` against all elements matching `selector`.
846    pub async fn eval_on_selector_all<R: DeserializeOwned>(
847        &self,
848        selector: &str,
849        expression: &str,
850    ) -> Result<Vec<R>> {
851        let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
852        let mut out = Vec::with_capacity(n);
853        for i in 0..n {
854            if let Some(oid) = selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await? {
855                let function = format!("(el) => {{ return ({expression}); }}");
856                let v = selectors::eval_object(&self.inner.session, &oid, &function, Value::Null).await?;
857                self.release_object(&oid).await;
858                out.push(serde_json::from_value::<R>(v).map_err(Error::from)?);
859            }
860        }
861        Ok(out)
862    }
863
864    async fn release_object(&self, object_id: &str) {
865        let _ = self
866            .inner
867            .session
868            .send("Runtime.releaseObject", json!({ "objectId": object_id }))
869            .await;
870    }
871
872    // --- viewport / screenshot ---
873
874    pub async fn set_viewport_size(&self, viewport: Viewport) -> Result<()> {
875        *self.inner.viewport.lock() = Some(viewport);
876        self.inner
877            .session
878            .send(
879                "Emulation.setDeviceMetricsOverride",
880                json!({
881                    "width": viewport.width,
882                    "height": viewport.height,
883                    "deviceScaleFactor": 1,
884                    "mobile": false,
885                }),
886            )
887            .await?;
888        Ok(())
889    }
890
891    pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
892        let opts = opts.unwrap_or_default();
893        let format = match opts.r#type.unwrap_or_default() {
894            crate::types::ScreenshotType::Png => "png",
895            crate::types::ScreenshotType::Jpeg => "jpeg",
896            crate::types::ScreenshotType::Webp => "webp",
897        };
898        let mut params = json!({ "format": format });
899        if opts.full_page.unwrap_or(false) {
900            params["captureBeyondViewport"] = json!(true);
901        }
902        if opts.omit_background.unwrap_or(false) && format == "png" {
903            params["omitBackground"] = json!(true);
904        }
905        let resp = self
906            .inner
907            .session
908            .send("Page.captureScreenshot", params)
909            .await?;
910        let data = resp
911            .get("data")
912            .and_then(|v| v.as_str())
913            .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
914        let bytes = base64::engine::general_purpose::STANDARD
915            .decode(data)
916            .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
917        if let Some(path) = &opts.path {
918            tokio::fs::write(path, &bytes).await?;
919        }
920        Ok(bytes)
921    }
922
923    // --- headers / init scripts ---
924
925    /// Inject a `<script>` tag into the page (inline `content` and/or `url`).
926    pub async fn add_script_tag(
927        &self,
928        content: Option<&str>,
929        url: Option<&str>,
930    ) -> Result<()> {
931        let arg = json!({ "content": content, "url": url });
932        let _ = selectors::eval_context(
933            &self.inner.session,
934            self.ctx().await,
935            "(a) => { const s = document.createElement('script'); if (a.url) s.src = a.url; if (a.content) s.textContent = a.content; document.head.appendChild(s); }",
936            arg,
937        )
938        .await?;
939        Ok(())
940    }
941
942    /// Inject a `<style>` tag into the page.
943    pub async fn add_style_tag(&self, content: &str) -> Result<()> {
944        let _ = selectors::eval_context(
945            &self.inner.session,
946            self.ctx().await,
947            "(a) => { const s = document.createElement('style'); s.textContent = a; document.head.appendChild(s); }",
948            json!(content),
949        )
950        .await?;
951        Ok(())
952    }
953
954    /// Enable/disable the HTTP cache for this page.
955    pub async fn set_cache_disabled(&self, disabled: bool) -> Result<()> {
956        self.inner
957            .session
958            .send("Network.setCacheDisabled", json!({ "cacheDisabled": disabled }))
959            .await
960            .map(|_: Value| ())
961    }
962
963    /// Bring the page to the front (focus).
964    pub async fn bring_to_front(&self) -> Result<()> {
965        self.inner
966            .session
967            .send("Page.bringToFront", json!({}))
968            .await
969            .map(|_: Value| ())
970    }
971
972    /// Override geolocation `(latitude, longitude)` (or clear with `None`).
973    /// Requires a granted `geolocation` permission.
974    pub async fn set_geolocation(&self, geolocation: Option<(f64, f64)>) -> Result<()> {
975        let params = match geolocation {
976            Some((lat, lon)) => json!({ "latitude": lat, "longitude": lon }),
977            None => json!({}),
978        };
979        self.inner
980            .session
981            .send("Emulation.setGeolocationOverride", params)
982            .await
983            .map(|_: Value| ())
984    }
985
986    /// Return a standalone HTTP client ([`APIRequestContext`]) tied to this
987    /// page. The client shares no state with the browser tab — it is a direct
988    /// HTTP client useful for API testing.
989    ///
990    /// Note: the page does not retain its `extra_http_headers` in Rust (they
991    /// are applied directly to CDP), so the returned context starts with an
992    /// empty default header set. Use [`BrowserContext::request`] to seed
993    /// defaults from the context.
994    pub fn request(&self) -> APIRequestContext {
995        APIRequestContext::new(Headers::new())
996    }
997
998    pub async fn set_extra_http_headers(&self, headers: Headers) -> Result<()> {
999        // CDP wants an array of {name, value}.
1000        let list: Vec<Value> = headers
1001            .iter()
1002            .map(|(k, v)| json!({ "name": k, "value": v }))
1003            .collect();
1004        self.inner
1005            .session
1006            .send("Network.setExtraHTTPHeaders", json!({ "headers": list }))
1007            .await?;
1008        Ok(())
1009    }
1010
1011    pub async fn add_init_script(&self, script: &str) -> Result<()> {
1012        let _ = self
1013            .inner
1014            .session
1015            .send(
1016                "Page.addScriptToEvaluateOnNewDocument",
1017                json!({ "source": script }),
1018            )
1019            .await;
1020        // Also run once in the current context.
1021        let _ = selectors::eval_context(
1022            &self.inner.session,
1023            self.ctx().await,
1024            "(s) => { (0, eval)(s); }",
1025            json!(script),
1026        )
1027        .await;
1028        Ok(())
1029    }
1030
1031    /// Expose a Rust function to the page as `window[name]`.
1032    ///
1033    /// After this returns, page JS can call `await window.<name>(...args)` and
1034    /// receive the Rust `callback`'s return value (serialized as JSON). Mirrors
1035    /// Playwright's `page.exposeFunction`.
1036    ///
1037    /// The `callback` receives the JS arguments as a `Vec<serde_json::Value>`
1038    /// and returns a `serde_json::Value`. If the callback panics or its result
1039    /// fails to serialize, the JS promise resolves with
1040    /// `{ "__error": "<message>" }` rather than rejecting.
1041    ///
1042    /// # Mechanism
1043    /// A CDP binding is registered under the internal name `__pwcdpInvoke_<name>`
1044    /// via `Runtime.addBinding`. A JS wrapper turns `window[name]` into a
1045    /// Promise-returning function that forwards `{ id, args }` to that binding
1046    /// (as a JSON string). A per-registration listener handles
1047    /// `Runtime.bindingCalled`, invokes the callback on its own task, and
1048    /// resolves the Promise by evaluating against the pending-callback map.
1049    ///
1050    /// The wrapper is installed for future documents via
1051    /// `Page.addScriptToEvaluateOnNewDocument` and once in the current context.
1052    /// Bindings are per-execution-context and reset on navigation: the wrapper
1053    /// is re-installed on every new document, but `Runtime.addBinding` is only
1054    /// re-asserted lazily if needed (it persists across same-origin navigations
1055    /// on a live page session; call `expose_function` again after a cross-origin
1056    /// navigation if the binding goes missing).
1057    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1058    where
1059        F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
1060        Fut: Future<Output = Value> + Send + 'static,
1061    {
1062        // Wrap the typed callback in an erased `Arc<dyn Fn>` so the listener
1063        // task can invoke it without carrying generics.
1064        let handler: ExposedBindingHandler =
1065            Arc::new(move |args| Box::pin(callback(args)));
1066
1067        // Register the CDP binding under a distinct internal name so it does not
1068        // clash with the Promise-returning `window[name]` wrapper.
1069        let binding_name = internal_binding_name(name);
1070        let _ = self
1071            .inner
1072            .session
1073            .send("Runtime.addBinding", json!({ "name": binding_name }))
1074            .await;
1075
1076        // Install the JS wrapper for future documents...
1077        let wrapper = binding_wrapper_source(name);
1078        let _ = self
1079            .inner
1080            .session
1081            .send(
1082                "Page.addScriptToEvaluateOnNewDocument",
1083                json!({ "source": wrapper }),
1084            )
1085            .await;
1086        // ...and once in the current context (idempotent).
1087        let _ = selectors::eval_context(
1088            &self.inner.session,
1089            self.ctx().await,
1090            "(s) => { (0, eval)(s); }",
1091            json!(wrapper),
1092        )
1093        .await;
1094
1095        // Per-registration listener: dispatch bindingCalled -> callback -> resolve.
1096        let mut rx = self.inner.session.subscribe();
1097        let session = Arc::clone(&self.inner.session);
1098        let name_owned = name.to_string();
1099        tokio::spawn(async move {
1100            binding_listener(&mut rx, &session, &name_owned, &handler).await;
1101        });
1102
1103        Ok(())
1104    }
1105
1106    // --- locators ---
1107
1108    pub fn locator(&self, selector: impl Into<String>) -> Locator {
1109        Locator::new(self.clone(), selector.into(), true, None)
1110    }
1111
1112    /// Return a [`FrameLocator`] scoped to the same-origin `<iframe>` matched
1113    /// by `selector`. Element queries on the returned locator resolve inside
1114    /// the iframe's `contentDocument`.
1115    ///
1116    /// **Same-origin only.** Cross-origin iframes cannot be reached from the
1117    /// page's main world; their `contentDocument` reads as `null`, and queries
1118    /// against them will report zero matches. `srcdoc` iframes and same-origin
1119    /// `src` iframes are supported.
1120    pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator {
1121        FrameLocator::new(self.clone(), selector.into())
1122    }
1123
1124    pub fn get_by_text(&self, text: &str, _exact: bool) -> Locator {
1125        // Minimal engine treats `text=` as case-insensitive substring.
1126        self.locator(format!("text={text}"))
1127    }
1128
1129    pub fn get_by_label(&self, text: &str) -> Locator {
1130        self.locator(format!("[aria-label=\"{}\"]", attr_escape(text)))
1131    }
1132
1133    pub fn get_by_placeholder(&self, text: &str) -> Locator {
1134        self.locator(format!("[placeholder=\"{}\"]", attr_escape(text)))
1135    }
1136
1137    pub fn get_by_alt_text(&self, text: &str) -> Locator {
1138        self.locator(format!("[alt=\"{}\"]", attr_escape(text)))
1139    }
1140
1141    pub fn get_by_title(&self, text: &str) -> Locator {
1142        self.locator(format!("[title=\"{}\"]", attr_escape(text)))
1143    }
1144
1145    pub fn get_by_test_id(&self, text: &str) -> Locator {
1146        self.locator(format!("[data-testid=\"{}\"]", attr_escape(text)))
1147    }
1148
1149    pub fn get_by_role(&self, role: AriaRole, opts: Option<crate::options::GetByRoleOptions>) -> Locator {
1150        let opts = opts.unwrap_or_default();
1151        let mut sel = format!("role={}", role.as_str());
1152        if let Some(name) = &opts.name {
1153            sel.push_str(&format!("[name=\"{name}\"]"));
1154        }
1155        if opts.exact == Some(true) {
1156            sel.push_str("[exact=\"true\"]");
1157        }
1158        Locator::new(self.clone(), sel, true, None)
1159    }
1160
1161    /// (Internal) resolve a selector to a single element RemoteObjectId,
1162    /// enforcing strict mode unless `index` was explicitly chosen.
1163    pub(crate) async fn resolve_strict(
1164        &self,
1165        selector: &str,
1166        forced_index: Option<usize>,
1167    ) -> Result<Option<String>> {
1168        let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1169        if n == 0 {
1170            return Ok(None);
1171        }
1172        let index = forced_index.unwrap_or_else(|| {
1173            // Strict callers (no forced index) pick 0; strict-violation is the
1174            // caller's responsibility. See Locator::resolve.
1175            0
1176        });
1177        selectors::element_at(&self.inner.session, self.ctx().await, selector, index).await
1178    }
1179
1180    /// The first element matching `selector`, if any.
1181    pub async fn query_selector(&self, selector: &str) -> Result<Option<ElementHandle>> {
1182        let oid = self.resolve_strict(selector, Some(0)).await?;
1183        Ok(oid.map(|oid| ElementHandle::new(self.clone(), oid)))
1184    }
1185
1186    /// All elements matching `selector`.
1187    pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementHandle>> {
1188        let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1189        let mut out = Vec::with_capacity(n);
1190        for i in 0..n {
1191            if let Some(oid) =
1192                selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await?
1193            {
1194                out.push(ElementHandle::new(self.clone(), oid));
1195            }
1196        }
1197        Ok(out)
1198    }
1199
1200    // --- input devices & frames ---
1201
1202    pub fn keyboard(&self) -> Keyboard {
1203        Keyboard::new(self.clone())
1204    }
1205
1206    pub fn mouse(&self) -> Mouse {
1207        Mouse::with_pos(self.clone(), Arc::clone(&self.inner.mouse_pos))
1208    }
1209
1210    pub fn touchscreen(&self) -> Touchscreen {
1211        Touchscreen::with_flag(self.clone(), Arc::clone(&self.inner.touch_emulation_started))
1212    }
1213
1214    // --- feature handles (wave-1 modules) ---
1215
1216    /// The page video capture handle, mirroring Playwright's `page.video()`.
1217    ///
1218    /// If a video output path was configured for the page, it is pre-recorded on
1219    /// the handle (via [`Video::with_path`]); otherwise the handle starts with
1220    /// no path and one can be supplied at [`Video::start`].
1221    pub fn video(&self) -> Video {
1222        let session = Arc::clone(&self.inner.session);
1223        match self.inner.video_path.lock().clone() {
1224            Some(path) => Video::with_path(session, path),
1225            None => Video::new(session),
1226        }
1227    }
1228
1229    /// The page accessibility-tree handle, mirroring Playwright's
1230    /// `page.accessibility()`.
1231    pub fn accessibility(&self) -> Accessibility {
1232        Accessibility::new(self.target_session())
1233    }
1234
1235    /// The page code-coverage handle, mirroring Playwright's
1236    /// `page.coverage()`.
1237    pub fn coverage(&self) -> Coverage {
1238        Coverage::new(self.target_session())
1239    }
1240
1241    /// The page fake-timer handle, mirroring Playwright's `page.clock()`.
1242    pub fn clock(&self) -> Clock {
1243        Clock::new(self.target_session())
1244    }
1245
1246    /// The page's `localStorage` for its current security origin, mirroring
1247    /// Playwright's `page.evaluate(() => localStorage)`-backed access but
1248    /// speaking CDP's `DOMStorage` domain directly.
1249    ///
1250    /// The origin is derived from the current `location.href` (scheme://host
1251    /// with port). Returns a best-effort handle; if the URL has no parseable
1252    /// origin (e.g. `about:blank`), the empty string is used as the origin.
1253    pub async fn web_storage(&self) -> WebStorage {
1254        let origin = self.security_origin().await;
1255        WebStorage::local_storage(self.target_session(), origin)
1256    }
1257
1258    /// The page's `localStorage` for its current security origin.
1259    pub async fn local_storage(&self) -> WebStorage {
1260        let origin = self.security_origin().await;
1261        WebStorage::local_storage(self.target_session(), origin)
1262    }
1263
1264    /// The page's `sessionStorage` for its current security origin.
1265    pub async fn session_storage(&self) -> WebStorage {
1266        let origin = self.security_origin().await;
1267        WebStorage::session_storage(self.target_session(), origin)
1268    }
1269
1270    /// Build a by-value [`CdpSession`] for this page's target.
1271    ///
1272    /// The wave-1 feature modules ([`Coverage`], [`Clock`], [`WebStorage`],
1273    /// [`Accessibility`]) own their session by value, but the page holds its
1274    /// session behind an `Arc`. We rebuild an equivalent target-level session
1275    /// (same connection + session id) and re-apply the page's default timeout
1276    /// so command timeouts match the page's configured value.
1277    fn target_session(&self) -> CdpSession {
1278        let conn = self.inner.session.connection().clone();
1279        let session_id = self
1280            .inner
1281            .session
1282            .session_id()
1283            .map(|s| s.to_string())
1284            .unwrap_or_default();
1285        let session = CdpSession::target(conn, session_id);
1286        session.set_default_timeout_ms(self.inner.default_timeout_ms.load(Ordering::Relaxed));
1287        session
1288    }
1289
1290    /// Derive the security origin (`scheme://host[:port]`) from the current page
1291    /// URL. Falls back to an empty origin for opaque URLs like `about:blank`.
1292    async fn security_origin(&self) -> String {
1293        let url = self.url().await.unwrap_or_default();
1294        derive_security_origin(&url)
1295    }
1296
1297    pub fn main_frame(&self) -> Frame {
1298        Frame::main(self.clone())
1299    }
1300
1301    /// All frames in the page (refreshed from the browser).
1302    pub async fn frames(&self) -> Result<Vec<Frame>> {
1303        self.refresh_frame_tree().await?;
1304        let ids: Vec<String> = self.inner.frames.lock().keys().cloned().collect();
1305        Ok(ids.into_iter().map(|id| Frame::new(self.clone(), id)).collect())
1306    }
1307
1308    /// Refresh the cached frame tree from `Page.getFrameTree`.
1309    pub(crate) async fn refresh_frame_tree(&self) -> Result<()> {
1310        let resp = self
1311            .inner
1312            .session
1313            .send("Page.getFrameTree", json!({}))
1314            .await?;
1315        if let Some(tree) = resp.get("frameTree") {
1316            let mut frames = self.inner.frames.lock();
1317            frames.clear();
1318            walk_frame_tree(tree, &mut frames);
1319            if let Some(id) = tree
1320                .get("frame")
1321                .and_then(|f| f.get("id"))
1322                .and_then(|v| v.as_str())
1323            {
1324                *self.inner.main_frame_id.lock() = Some(id.to_string());
1325            }
1326        }
1327        Ok(())
1328    }
1329
1330    pub(crate) fn main_frame_id(&self) -> Option<String> {
1331        self.inner.main_frame_id.lock().clone()
1332    }
1333
1334    pub(crate) fn frame_data(&self, frame_id: &str) -> Option<FrameData> {
1335        self.inner.frames.lock().get(frame_id).cloned()
1336    }
1337
1338    pub(crate) fn frame_ids_with_parent(&self, parent: &str) -> Vec<String> {
1339        self.inner
1340            .frames
1341            .lock()
1342            .iter()
1343            .filter(|(_, d)| d.parent_id.as_deref() == Some(parent))
1344            .map(|(k, _)| k.clone())
1345            .collect()
1346    }
1347
1348    // --- events ---
1349
1350    pub fn on_console<F, Fut>(&self, handler: F)
1351    where
1352        F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
1353        Fut: std::future::Future<Output = ()> + Send + 'static,
1354    {
1355        let mut rx = self.inner.session.subscribe();
1356        tokio::spawn(async move {
1357            while let Ok(ev) = rx.recv().await {
1358                if ev.method == "Runtime.consoleAPICalled" {
1359                    let msg = parse_console(&ev.params);
1360                    handler(msg).await;
1361                }
1362            }
1363        });
1364    }
1365
1366    /// Register a handler invoked when the page fires its `load` event
1367    /// (`Page.loadEventFired`), mirroring Playwright's `page.on('load')`.
1368    pub fn on_load<F, Fut>(&self, handler: F)
1369    where
1370        F: Fn(()) -> Fut + Send + Sync + 'static,
1371        Fut: std::future::Future<Output = ()> + Send + 'static,
1372    {
1373        let mut rx = self.inner.session.subscribe();
1374        tokio::spawn(async move {
1375            while let Ok(ev) = rx.recv().await {
1376                if ev.method == "Page.loadEventFired" {
1377                    handler(()).await;
1378                }
1379            }
1380        });
1381    }
1382
1383    /// Register a handler invoked when the page crashes (`Inspector.targetCrashed`),
1384    /// mirroring Playwright's `page.on('crash')`.
1385    pub fn on_crash<F, Fut>(&self, handler: F)
1386    where
1387        F: Fn(()) -> Fut + Send + Sync + 'static,
1388        Fut: std::future::Future<Output = ()> + Send + 'static,
1389    {
1390        let mut rx = self.inner.session.subscribe();
1391        tokio::spawn(async move {
1392            while let Ok(ev) = rx.recv().await {
1393                if ev.method == "Inspector.targetCrashed" {
1394                    handler(()).await;
1395                }
1396            }
1397        });
1398    }
1399
1400    /// Register a handler invoked when a frame is attached to the page
1401    /// (`Page.frameAttached`), mirroring Playwright's `page.on('frameattached')`.
1402    pub fn on_frameattached<F, Fut>(&self, handler: F)
1403    where
1404        F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1405        Fut: std::future::Future<Output = ()> + Send + 'static,
1406    {
1407        let mut rx = self.inner.session.subscribe();
1408        tokio::spawn(async move {
1409            while let Ok(ev) = rx.recv().await {
1410                if ev.method == "Page.frameAttached" {
1411                    if let Some(fe) = FrameEvent::from_attached(&ev.params) {
1412                        handler(fe).await;
1413                    }
1414                }
1415            }
1416        });
1417    }
1418
1419    /// Register a handler invoked when a frame is detached from the page
1420    /// (`Page.frameDetached`), mirroring Playwright's `page.on('framedetached')`.
1421    pub fn on_framedetached<F, Fut>(&self, handler: F)
1422    where
1423        F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1424        Fut: std::future::Future<Output = ()> + Send + 'static,
1425    {
1426        let mut rx = self.inner.session.subscribe();
1427        tokio::spawn(async move {
1428            while let Ok(ev) = rx.recv().await {
1429                if ev.method == "Page.frameDetached" {
1430                    if let Some(fe) = FrameEvent::from_detached(&ev.params) {
1431                        handler(fe).await;
1432                    }
1433                }
1434            }
1435        });
1436    }
1437
1438    /// Register a handler invoked when a frame is navigated to a new URL
1439    /// (`Page.frameNavigated`), mirroring Playwright's `page.on('framenavigated')`.
1440    pub fn on_framenavigated<F, Fut>(&self, handler: F)
1441    where
1442        F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1443        Fut: std::future::Future<Output = ()> + Send + 'static,
1444    {
1445        let mut rx = self.inner.session.subscribe();
1446        tokio::spawn(async move {
1447            while let Ok(ev) = rx.recv().await {
1448                if ev.method == "Page.frameNavigated" {
1449                    if let Some(fe) = FrameEvent::from_navigated(&ev.params) {
1450                        handler(fe).await;
1451                    }
1452                }
1453            }
1454        });
1455    }
1456
1457    pub fn on_dialog<F, Fut>(&self, handler: F)
1458    where
1459        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1460        Fut: std::future::Future<Output = ()> + Send + 'static,
1461    {
1462        let mut rx = self.inner.session.subscribe();
1463        let session = Arc::clone(&self.inner.session);
1464        tokio::spawn(async move {
1465            while let Ok(ev) = rx.recv().await {
1466                if ev.method == "Page.javascriptDialogOpening" {
1467                    let dialog = Dialog::from_event(&ev.params, &session);
1468                    handler(dialog).await;
1469                }
1470            }
1471        });
1472    }
1473
1474    /// Register a handler invoked for each network request sent.
1475    pub fn on_request<F, Fut>(&self, handler: F)
1476    where
1477        F: Fn(Request) -> Fut + Send + Sync + 'static,
1478        Fut: Future<Output = ()> + Send + 'static,
1479    {
1480        let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1481        self.inner.on_request_handlers.lock().push(h);
1482    }
1483
1484    /// Wait for a network request whose URL contains `url_predicate`, returning
1485    /// it. Times out after `timeout` (default 30s) with [`Error::Timeout`].
1486    ///
1487    /// The matcher is a plain substring test against the request URL
1488    /// (`str::contains`), not a regex or glob — keep predicates literal.
1489    ///
1490    /// A one-shot handler is registered with [`on_request`](Self::on_request);
1491    /// it sends the first matching event over a `tokio::sync::oneshot` channel
1492    /// and then drains its sender to `None`, so subsequent events are no-ops.
1493    /// The handler is *not* removed from the handler list after it fires — it
1494    /// simply has nothing to send — so it is idempotent and harmless to leave
1495    /// in place (one extra closure in a `Vec`, no side effects).
1496    ///
1497    /// Typical use races the wait against the action that triggers the request:
1498    ///
1499    /// ```no_run
1500    /// # use std::time::Duration;
1501    /// # async fn example(page: &playwright_cdp::Page) {
1502    /// let button = page.locator("button#submit");
1503    /// let (req, _) = tokio::join!(
1504    ///     page.expect_request("/api/login", Some(Duration::from_secs(5))),
1505    ///     button.click(None),
1506    /// );
1507    /// let req = req.unwrap();
1508    /// assert!(req.url().contains("/api/login"));
1509    /// # }
1510    /// ```
1511    pub async fn expect_request(
1512        &self,
1513        url_predicate: &str,
1514        timeout: Option<Duration>,
1515    ) -> Result<Request> {
1516        let (tx, rx) = oneshot::channel::<Request>();
1517        let tx = Arc::new(Mutex::new(Some(tx)));
1518        // Own the predicate so the `'static` handler closure can capture it.
1519        let url_predicate = url_predicate.to_string();
1520        self.on_request({
1521            // Move a separate clone into the closure so the outer binding
1522            // remains available for the timeout message below.
1523            let url_predicate = url_predicate.clone();
1524            move |req| {
1525                let tx = Arc::clone(&tx);
1526                // The `Fn` closure may fire many times; re-clone per call so the
1527                // owned predicate is never consumed.
1528                let url_predicate = url_predicate.clone();
1529                Box::pin(async move {
1530                    if req.url().contains(&url_predicate) {
1531                        if let Some(sender) = tx.lock().take() {
1532                            let _ = sender.send(req);
1533                        }
1534                    }
1535                })
1536            }
1537        });
1538        match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1539            Ok(Ok(req)) => Ok(req),
1540            Ok(Err(_)) => Err(Error::Timeout(
1541                "expect_request: oneshot sender dropped before a match arrived".into(),
1542            )),
1543            Err(_) => Err(Error::Timeout(format!(
1544                "expect_request: no request matched {url_predicate:?} within the timeout"
1545            ))),
1546        }
1547    }
1548
1549    /// Register a handler invoked for each network response received.
1550    pub fn on_response<F, Fut>(&self, handler: F)
1551    where
1552        F: Fn(Response) -> Fut + Send + Sync + 'static,
1553        Fut: Future<Output = ()> + Send + 'static,
1554    {
1555        let h: ResponseHandler = Arc::new(move |r| Box::pin(handler(r)));
1556        self.inner.on_response_handlers.lock().push(h);
1557    }
1558
1559    /// Wait for a network response whose URL contains `url_predicate`, returning
1560    /// it. Times out after `timeout` (default 30s) with [`Error::Timeout`].
1561    ///
1562    /// The matcher is a plain substring test against the response URL
1563    /// (`str::contains`), not a regex or glob — keep predicates literal.
1564    ///
1565    /// A one-shot handler is registered with [`on_response`](Self::on_response);
1566    /// it sends the first matching event over a `tokio::sync::oneshot` channel
1567    /// and then drains its sender to `None`, so subsequent events are no-ops.
1568    /// The handler is *not* removed from the handler list after it fires — it
1569    /// simply has nothing to send — so it is idempotent and harmless to leave
1570    /// in place (one extra closure in a `Vec`, no side effects).
1571    ///
1572    /// Typical use races the wait against the navigation that triggers it:
1573    ///
1574    /// ```no_run
1575    /// # use std::time::Duration;
1576    /// # async fn example(page: &playwright_cdp::Page, url: &str) {
1577    /// let (resp, _) = tokio::join!(
1578    ///     page.expect_response("127.0.0.1", Some(Duration::from_secs(5))),
1579    ///     page.goto(url, None),
1580    /// );
1581    /// assert_eq!(resp.unwrap().status(), 200);
1582    /// # }
1583    /// ```
1584    pub async fn expect_response(
1585        &self,
1586        url_predicate: &str,
1587        timeout: Option<Duration>,
1588    ) -> Result<Response> {
1589        let (tx, rx) = oneshot::channel::<Response>();
1590        let tx = Arc::new(Mutex::new(Some(tx)));
1591        // Own the predicate so the `'static` handler closure can capture it.
1592        let url_predicate = url_predicate.to_string();
1593        self.on_response({
1594            // Move a separate clone into the closure so the outer binding
1595            // remains available for the timeout message below.
1596            let url_predicate = url_predicate.clone();
1597            move |resp| {
1598                let tx = Arc::clone(&tx);
1599                // The `Fn` closure may fire many times; re-clone per call so the
1600                // owned predicate is never consumed.
1601                let url_predicate = url_predicate.clone();
1602                Box::pin(async move {
1603                    if resp.url().contains(&url_predicate) {
1604                        if let Some(sender) = tx.lock().take() {
1605                            let _ = sender.send(resp);
1606                        }
1607                    }
1608                })
1609            }
1610        });
1611        match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1612            Ok(Ok(resp)) => Ok(resp),
1613            Ok(Err(_)) => Err(Error::Timeout(
1614                "expect_response: oneshot sender dropped before a match arrived".into(),
1615            )),
1616            Err(_) => Err(Error::Timeout(format!(
1617                "expect_response: no response matched {url_predicate:?} within the timeout"
1618            ))),
1619        }
1620    }
1621
1622    /// Wait for the next CDP event whose method equals `event_name`, returning
1623    /// its `params`. Times out after `timeout` (default 30s) with
1624    /// [`Error::Timeout`].
1625    ///
1626    /// Subscribes to the page session's event stream, so it only sees events
1627    /// emitted **after** this call — race it against the triggering action with
1628    /// `tokio::join!`.
1629    ///
1630    /// Example: wait for the page's load event during navigation.
1631    ///
1632    /// ```no_run
1633    /// # use std::time::Duration;
1634    /// # async fn example(page: &playwright_cdp::Page, url: &str) {
1635    /// let (params, _) = tokio::join!(
1636    ///     page.expect_event("Page.loadEventFired", Some(Duration::from_secs(5))),
1637    ///     page.goto(url, None),
1638    /// );
1639    /// let _params = params.unwrap();
1640    /// # }
1641    /// ```
1642    pub async fn expect_event(
1643        &self,
1644        event_name: &str,
1645        timeout: Option<Duration>,
1646    ) -> Result<Value> {
1647        // Subscribe *before* the triggering action so we capture every event
1648        // it produces. New receivers only see events emitted after subscribe.
1649        let mut rx = self.inner.session.subscribe();
1650        let effective = timeout.unwrap_or(Duration::from_secs(30));
1651        let deadline = tokio::time::Instant::now() + effective;
1652
1653        loop {
1654            match tokio::time::timeout_at(deadline, rx.recv()).await {
1655                Ok(Ok(ev)) if ev.method == event_name => return Ok(ev.params),
1656                Ok(Ok(_)) => continue,
1657                Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
1658                Ok(Err(broadcast::error::RecvError::Closed)) => {
1659                    return Err(Error::Timeout(format!(
1660                        "expect_event('{event_name}'): event stream closed before a match arrived"
1661                    )));
1662                }
1663                Err(_) => {
1664                    return Err(Error::Timeout(format!(
1665                        "expect_event('{event_name}') timed out after {}ms",
1666                        effective.as_millis()
1667                    )));
1668                }
1669            }
1670        }
1671    }
1672
1673    /// Register a handler invoked when a network request fails.
1674    pub fn on_requestfailed<F, Fut>(&self, handler: F)
1675    where
1676        F: Fn(Request) -> Fut + Send + Sync + 'static,
1677        Fut: Future<Output = ()> + Send + 'static,
1678    {
1679        let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1680        self.inner.on_requestfailed_handlers.lock().push(h);
1681    }
1682
1683    /// Register a handler invoked when an uncaught page error occurs.
1684    pub fn on_pageerror<F, Fut>(&self, handler: F)
1685    where
1686        F: Fn(String) -> Fut + Send + Sync + 'static,
1687        Fut: Future<Output = ()> + Send + 'static,
1688    {
1689        let mut rx = self.inner.session.subscribe();
1690        tokio::spawn(async move {
1691            while let Ok(ev) = rx.recv().await {
1692                if ev.method == "Runtime.exceptionThrown" {
1693                    let msg = ev
1694                        .params
1695                        .get("exceptionDetails")
1696                        .and_then(|d| d.get("exception"))
1697                        .and_then(|e| e.get("description"))
1698                        .and_then(|v| v.as_str())
1699                        .unwrap_or("unknown error")
1700                        .to_string();
1701                    handler(msg).await;
1702                }
1703            }
1704        });
1705    }
1706
1707    /// Register a handler invoked for each new WebSocket connection, mirroring
1708    /// Playwright's `page.on('websocket')`.
1709    ///
1710    /// Capture is always on: the page's [`WebSocketRegistry`] subscribes to the
1711    /// session in [`Page::attach`] (before `Network.enable`), so every
1712    /// `Network.webSocket*` event is already being accumulated into a
1713    /// [`WebSocket`] handle. This method registers against the registry's
1714    /// created-connection bus and invokes `handler` for each newly-created
1715    /// connection. Connections created *before* this call are not replayed —
1716    /// register the handler early (before the page opens the socket) to observe
1717    /// every one.
1718    pub fn on_websocket<F, Fut>(&self, handler: F)
1719    where
1720        F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1721        Fut: Future<Output = ()> + Send + 'static,
1722    {
1723        let mut rx = self.inner.web_socket_registry.on_created();
1724        // Wrap in an Arc<dyn Fn> so the handler can be invoked from each
1725        // spawned task without being moved out of the loop.
1726        let handler: Arc<dyn Fn(WebSocket) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync> =
1727            Arc::new(move |ws| Box::pin(handler(ws)));
1728        tokio::spawn(async move {
1729            while let Ok(ws) = rx.recv().await {
1730                let h = Arc::clone(&handler);
1731                // Invoke on its own task so a slow handler doesn't stall the
1732                // registry's broadcast bus.
1733                tokio::spawn(async move {
1734                    h(ws).await;
1735                });
1736            }
1737        });
1738    }
1739
1740    /// Register a handler invoked when a network request finishes loading.
1741    pub fn on_requestfinished<F, Fut>(&self, handler: F)
1742    where
1743        F: Fn(Request) -> Fut + Send + Sync + 'static,
1744        Fut: Future<Output = ()> + Send + 'static,
1745    {
1746        let mut rx = self.inner.session.subscribe();
1747        let store = Arc::clone(&self.inner.network_store);
1748        tokio::spawn(async move {
1749            while let Ok(ev) = rx.recv().await {
1750                if ev.method == "Network.loadingFinished" {
1751                    if let Some(rid) = ev.params.get("requestId").and_then(|v| v.as_str()) {
1752                        if let Some(req) = store.get_request(rid) {
1753                            handler(req).await;
1754                        }
1755                    }
1756                }
1757            }
1758        });
1759    }
1760
1761    /// Register a handler invoked when the page closes.
1762    pub fn on_close<F, Fut>(&self, handler: F)
1763    where
1764        F: Fn() -> Fut + Send + Sync + 'static,
1765        Fut: Future<Output = ()> + Send + 'static,
1766    {
1767        let h: CloseHandler = Arc::new(move || Box::pin(handler()));
1768        self.inner.on_close_handlers.lock().push(h);
1769    }
1770
1771    /// Register a handler invoked for each file download (`Page.downloadWillBegin`).
1772    ///
1773    /// On first registration, downloads are routed to a per-page temp directory
1774    /// via `Page.setDownloadBehavior` `allow`. Progress events update the
1775    /// download's shared state so [`Download::path`] / [`Download::save_as`]
1776    /// can await completion.
1777    pub fn on_download<F, Fut>(&self, handler: F)
1778    where
1779        F: Fn(Download) -> Fut + Send + Sync + 'static,
1780        Fut: Future<Output = ()> + Send + 'static,
1781    {
1782        // Lazily set up the download behavior + listener task exactly once.
1783        if self
1784            .inner
1785            .download_started
1786            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1787            .is_ok()
1788        {
1789            let dir = match tempfile::TempDir::new() {
1790                Ok(d) => Arc::new(d),
1791                Err(e) => {
1792                    tracing::error!("failed to create download temp dir: {e}");
1793                    return;
1794                }
1795            };
1796            let dir_path = dir.path().to_path_buf();
1797            *self.inner.download_dir.lock() = Some(Arc::clone(&dir));
1798            *self.inner.download_path.lock() = Some(dir_path.clone());
1799
1800            // Tell Chrome to save downloads into our temp dir.
1801            let session_for_setup = Arc::clone(&self.inner.session);
1802            let setup_path = dir_path.clone();
1803            tokio::spawn(async move {
1804                let _ = session_for_setup
1805                    .send(
1806                        "Page.setDownloadBehavior",
1807                        json!({ "behavior": "allow", "downloadPath": setup_path }),
1808                    )
1809                    .await;
1810            });
1811
1812            // Listener task: maps downloadWillBegin -> Download, downloadProgress -> state.
1813            let mut rx = self.inner.session.subscribe();
1814            let session = Arc::clone(&self.inner.session);
1815            let states = Arc::clone(&self.inner.download_states);
1816            let download_path = dir_path.clone();
1817            // The handler is wrapped in an Arc and invoked from the task. We
1818            // hold a Page clone so each Download can reference its page.
1819            let page = self.clone();
1820            let handler: Arc<dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync> =
1821                Arc::new(move |d| Box::pin(handler(d)));
1822            tokio::spawn(async move {
1823                download_listener(
1824                    &mut rx,
1825                    &session,
1826                    &states,
1827                    &download_path,
1828                    page.clone(),
1829                    &handler,
1830                )
1831                .await;
1832            });
1833        }
1834    }
1835
1836    /// Wait for a download whose URL contains `url_predicate`, returning it.
1837    /// Times out after `timeout` (default 30s) with [`Error::Timeout`].
1838    ///
1839    /// The matcher is a plain substring test against the download URL
1840    /// (`str::contains`), not a regex or glob — keep predicates literal.
1841    ///
1842    /// A one-shot handler is registered with [`on_download`](Self::on_download);
1843    /// it sends the first matching event over a `tokio::sync::oneshot` channel
1844    /// and then drains its sender to `None`, so subsequent events are no-ops.
1845    /// The handler is *not* removed from the handler list after it fires — it
1846    /// simply has nothing to send — so it is idempotent and harmless to leave
1847    /// in place (one extra closure in a `Vec`, no side effects).
1848    ///
1849    /// Note that the first `on_download` registration on a page lazily wires up
1850    /// `Page.setDownloadBehavior`, so this call implicitly opts the page into
1851    /// download routing to a temp dir.
1852    ///
1853    /// Typical use races the wait against the click that triggers the download:
1854    ///
1855    /// ```no_run
1856    /// # use std::time::Duration;
1857    /// # async fn example(page: &playwright_cdp::Page) {
1858    /// let link = page.locator("a#download");
1859    /// let (dl, _) = tokio::join!(
1860    ///     page.expect_download("/report.pdf", Some(Duration::from_secs(5))),
1861    ///     link.click(None),
1862    /// );
1863    /// let dl = dl.unwrap();
1864    /// assert!(dl.url().contains("/report.pdf"));
1865    /// # }
1866    /// ```
1867    pub async fn expect_download(
1868        &self,
1869        url_predicate: &str,
1870        timeout: Option<Duration>,
1871    ) -> Result<Download> {
1872        let (tx, rx) = oneshot::channel::<Download>();
1873        let tx = Arc::new(Mutex::new(Some(tx)));
1874        // Own the predicate so the `'static` handler closure can capture it.
1875        let url_predicate = url_predicate.to_string();
1876        self.on_download({
1877            // Move a separate clone into the closure so the outer binding
1878            // remains available for the timeout message below.
1879            let url_predicate = url_predicate.clone();
1880            move |dl| {
1881                let tx = Arc::clone(&tx);
1882                // The `Fn` closure may fire many times; re-clone per call so the
1883                // owned predicate is never consumed.
1884                let url_predicate = url_predicate.clone();
1885                Box::pin(async move {
1886                    if dl.url().contains(&url_predicate) {
1887                        if let Some(sender) = tx.lock().take() {
1888                            let _ = sender.send(dl);
1889                        }
1890                    }
1891                })
1892            }
1893        });
1894        match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1895            Ok(Ok(dl)) => Ok(dl),
1896            Ok(Err(_)) => Err(Error::Timeout(
1897                "expect_download: oneshot sender dropped before a match arrived".into(),
1898            )),
1899            Err(_) => Err(Error::Timeout(format!(
1900                "expect_download: no download matched {url_predicate:?} within the timeout"
1901            ))),
1902        }
1903    }
1904
1905    /// Wait for a console message whose text contains `text_predicate`,
1906    /// returning it. Times out after `timeout` (default 30s) with
1907    /// [`Error::Timeout`].
1908    ///
1909    /// The matcher is a plain substring test against the console message text
1910    /// ([`ConsoleMessage::text`], `str::contains`), not a regex or glob — keep
1911    /// predicates literal.
1912    ///
1913    /// A one-shot handler is registered with [`on_console`](Self::on_console);
1914    /// it sends the first matching event over a `tokio::sync::oneshot` channel
1915    /// and then drains its sender to `None`, so subsequent events are no-ops.
1916    /// The handler task keeps running for the life of the page (one extra
1917    /// spawned task, no side effects), mirroring the oneshot pattern used by
1918    /// the other `expect_*` helpers.
1919    ///
1920    /// Typical use races the wait against the script that logs the message:
1921    ///
1922    /// ```no_run
1923    /// # use std::time::Duration;
1924    /// # async fn example(page: &playwright_cdp::Page) {
1925    /// let (msg, _) = tokio::join!(
1926    ///     page.expect_console_message("hello", Some(Duration::from_secs(5))),
1927    ///     page.evaluate::<()>("console.log('hello world')"),
1928    /// );
1929    /// assert!(msg.unwrap().text().contains("hello"));
1930    /// # }
1931    /// ```
1932    pub async fn expect_console_message(
1933        &self,
1934        text_predicate: &str,
1935        timeout: Option<Duration>,
1936    ) -> Result<ConsoleMessage> {
1937        let (tx, rx) = oneshot::channel::<ConsoleMessage>();
1938        let tx = Arc::new(Mutex::new(Some(tx)));
1939        // Own the predicate so the `'static` handler closure can capture it.
1940        let text_predicate = text_predicate.to_string();
1941        self.on_console({
1942            // Move a separate clone into the closure so the outer binding
1943            // remains available for the timeout message below.
1944            let text_predicate = text_predicate.clone();
1945            move |msg| {
1946                let tx = Arc::clone(&tx);
1947                // The `Fn` closure may fire many times; re-clone per call so the
1948                // owned predicate is never consumed.
1949                let text_predicate = text_predicate.clone();
1950                Box::pin(async move {
1951                    if msg.text().contains(&text_predicate) {
1952                        if let Some(sender) = tx.lock().take() {
1953                            let _ = sender.send(msg);
1954                        }
1955                    }
1956                })
1957            }
1958        });
1959        match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1960            Ok(Ok(msg)) => Ok(msg),
1961            Ok(Err(_)) => Err(Error::Timeout(
1962                "expect_console_message: oneshot sender dropped before a match arrived".into(),
1963            )),
1964            Err(_) => Err(Error::Timeout(format!(
1965                "expect_console_message: no message matched {text_predicate:?} within the timeout"
1966            ))),
1967        }
1968    }
1969
1970    /// Wait for a popup (`window.open`) opened by this page, returning it as a
1971    /// new [`Page`]. Times out after `timeout` (default 30s) with
1972    /// [`Error::Timeout`].
1973    ///
1974    /// # Implementation (polling-based)
1975    ///
1976    /// A popup (`window.open`) is a new top-level page target rather than a
1977    /// child of this page's target, so child-target auto-attach does not surface
1978    /// it (and `Target.targetCreated` does not fire for popups on stock Chrome
1979    /// in this setup, while `Target.getTargets` omits `openerId`). This helper
1980    /// therefore registers a one-shot handler with
1981    /// [`on_popup`](Self::on_popup), which polls `Target.getTargets` and treats
1982    /// the first new `type == "page"` target in this page's browser context as
1983    /// the popup. See [`on_popup`](Self::on_popup) for the full limitations.
1984    ///
1985    /// The popup [`Page`] is built with an empty init-script set and this
1986    /// page's default timeout; per-context defaults (extra headers, user
1987    /// agent, viewport) are not re-applied, since `Page` does not expose them
1988    /// as raw values. This is sufficient for typical popup-driving flows
1989    /// (interact with the popup, then close it).
1990    ///
1991    /// Typical use races the wait against the script that opens the popup:
1992    ///
1993    /// ```no_run
1994    /// # use std::time::Duration;
1995    /// # async fn example(page: &playwright_cdp::Page) {
1996    /// let (popup, _) = tokio::join!(
1997    ///     page.expect_popup(Some(Duration::from_secs(5))),
1998    ///     page.evaluate::<()>("window.open('about:blank')"),
1999    /// );
2000    /// let _popup = popup.unwrap();
2001    /// # }
2002    /// ```
2003    pub async fn expect_popup(&self, timeout: Option<Duration>) -> Result<Page> {
2004        let (tx, rx) = oneshot::channel::<Page>();
2005        let tx = Arc::new(Mutex::new(Some(tx)));
2006        self.on_popup(move |popup| {
2007            let tx = Arc::clone(&tx);
2008            Box::pin(async move {
2009                if let Some(sender) = tx.lock().take() {
2010                    let _ = sender.send(popup);
2011                }
2012            })
2013        })
2014        .await;
2015        match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
2016            Ok(Ok(popup)) => Ok(popup),
2017            Ok(Err(_)) => Err(Error::Timeout(
2018                "expect_popup: oneshot sender dropped before a popup arrived".into(),
2019            )),
2020            Err(_) => Err(Error::Timeout(
2021                "expect_popup: no popup opened within the timeout".into(),
2022            )),
2023        }
2024    }
2025
2026    /// Register a handler invoked when this page opens a popup (`window.open`),
2027    /// mirroring Playwright's `page.on('popup')`.
2028    ///
2029    /// # Implementation (polling, not event-driven)
2030    ///
2031    /// A popup is a brand-new top-level page target, *not* a child of this
2032    /// page's target, so this page session's `Target.setAutoAttach` does not
2033    /// surface it (and, empirically, `Target.setDiscoverTargets` /
2034    /// `Target.targetCreated` do not fire for `window.open` popups on stock
2035    /// Chrome in this setup). `Target.getTargets` also omits `openerId`, so
2036    /// strict opener attribution is not available.
2037    ///
2038    /// Instead, on first registration a background task polls `Target.getTargets`
2039    /// on a short interval. Any `type == "page"` target that shares this page's
2040    /// `browserContextId`, is not this page itself, and has not already been
2041    /// delivered is treated as the popup: the task attaches to it
2042    /// (`Target.attachToTarget { flatten }`), builds a full [`Page`] via
2043    /// [`Page::attach`], and hands it to every registered handler. A `delivered`
2044    /// set (rather than a registration-time baseline) is used because a baseline
2045    /// diff races with the trigger under `tokio::join!` — the popup can appear
2046    /// before the snapshot completes.
2047    ///
2048    /// **Limitations** (documented honestly): attribution is "any page target in
2049    /// this browser context that isn't this page," not opener-precise, so it can
2050    /// misattribute if another page already exists or is opened concurrently in
2051    /// the same context. Each distinct popup target fires the handlers once.
2052    ///
2053    /// The popup [`Page`] is built with an empty init-scripts set and this
2054    /// page's default timeout; context-level defaults (extra headers, user
2055    /// agent, viewport) are not re-applied (see [`expect_popup`](Self::expect_popup)).
2056    pub async fn on_popup<F, Fut>(&self, handler: F)
2057    where
2058        F: Fn(Page) -> Fut + Send + Sync + 'static,
2059        Fut: Future<Output = ()> + Send + 'static,
2060    {
2061        // Record this handler so the background task dispatches to it.
2062        let handler: PopupHandler = Arc::new(move |p| Box::pin(handler(p)));
2063        self.inner.on_popup_handlers.lock().push(handler);
2064
2065        // Lazily spawn the popup poller once.
2066        if self
2067            .inner
2068            .popup_started
2069            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2070            .is_ok()
2071        {
2072            let browser = self.inner.browser.clone();
2073            let default_timeout_ms = self.inner.default_timeout_ms.load(Ordering::Relaxed);
2074            let opener_target_id = self.inner.target_id.clone();
2075            let handlers = Arc::clone(&self.inner.on_popup_handlers);
2076            let browser_session = browser.new_browser_cdp_session();
2077
2078            tokio::spawn(async move {
2079                // Determine this page's browserContextId (and the baseline set
2080                // of already-existing page targets) so we only surface brand-new
2081                // page targets in the same context.
2082                let resp = match browser_session
2083                    .send("Target.getTargets", json!({}))
2084                    .await
2085                {
2086                    Ok(r) => r,
2087                    Err(_) => return,
2088                };
2089                let targets = resp
2090                    .get("targetInfos")
2091                    .and_then(|v| v.as_array())
2092                    .cloned()
2093                    .unwrap_or_default();
2094                // This page's context id (look it up by target id).
2095                let my_context = targets
2096                    .iter()
2097                    .find(|t| {
2098                        t.get("targetId").and_then(|v| v.as_str()) == Some(&opener_target_id)
2099                    })
2100                    .and_then(|t| t.get("browserContextId"))
2101                    .and_then(|v| v.as_str())
2102                    .map(|s| s.to_string());
2103                // Track which popup targets we have already delivered, so each
2104                // distinct popup fires the handlers exactly once. We do NOT
2105                // snapshot a baseline: a baseline diff races with the trigger
2106                // under `tokio::join!` (the popup can appear before the
2107                // snapshot completes). Instead we deliver every same-context
2108                // page target that isn't this page itself and that we haven't
2109                // already handed off. This assumes no pre-existing unrelated
2110                // page target in the same browser context at registration time
2111                // (true for the common one-page-then-popup flow).
2112                let mut delivered: std::collections::HashSet<String> =
2113                    std::collections::HashSet::new();
2114
2115                loop {
2116                    tokio::time::sleep(Duration::from_millis(100)).await;
2117                    let resp = match browser_session.send("Target.getTargets", json!({})).await {
2118                        Ok(r) => r,
2119                        Err(_) => continue,
2120                    };
2121                    let targets = match resp.get("targetInfos").and_then(|v| v.as_array()) {
2122                        Some(a) => a,
2123                        None => continue,
2124                    };
2125                    // Undelivered page targets in the same context (excluding
2126                    // this page itself).
2127                    let new_popups: Vec<&Value> = targets
2128                        .iter()
2129                        .filter(|t| t.get("type").and_then(|v| v.as_str()) == Some("page"))
2130                        .filter(|t| {
2131                            my_context
2132                                .as_deref()
2133                                .zip(t.get("browserContextId").and_then(|v| v.as_str()))
2134                                .map(|(mc, c)| mc == c)
2135                                .unwrap_or(true)
2136                        })
2137                        .filter(|t| {
2138                            t.get("targetId")
2139                                .and_then(|v| v.as_str())
2140                                .map(|id| {
2141                                    !delivered.contains(id) && id != opener_target_id
2142                                })
2143                                .unwrap_or(false)
2144                        })
2145                        .collect();
2146                    if new_popups.is_empty() {
2147                        continue;
2148                    }
2149                    for np in new_popups {
2150                        let target_id = match np.get("targetId").and_then(|v| v.as_str()) {
2151                            Some(id) => id.to_string(),
2152                            None => continue,
2153                        };
2154                        delivered.insert(target_id.clone());
2155                        // Attach to the popup target to obtain a session id.
2156                        let attach = match browser_session
2157                            .send(
2158                                "Target.attachToTarget",
2159                                json!({ "targetId": target_id, "flatten": true }),
2160                            )
2161                            .await
2162                        {
2163                            Ok(r) => r,
2164                            Err(_) => continue,
2165                        };
2166                        let sid = match attach.get("sessionId").and_then(|v| v.as_str()) {
2167                            Some(s) => s.to_string(),
2168                            None => continue,
2169                        };
2170                        // Build a full Page for the popup target, mirroring
2171                        // BrowserContext::new_page (empty init scripts / no
2172                        // extra defaults — see the doc on `expect_popup`).
2173                        let popup = match Page::attach(
2174                            browser.clone(),
2175                            sid,
2176                            target_id,
2177                            &[],
2178                            default_timeout_ms,
2179                            None,
2180                            None,
2181                            None,
2182                        )
2183                        .await
2184                        {
2185                            Ok(p) => p,
2186                            Err(_) => continue,
2187                        };
2188                        // Snapshot the current handlers and dispatch to each.
2189                        let snapshot = handlers.lock().clone();
2190                        for h in snapshot {
2191                            let p = popup.clone();
2192                            tokio::spawn(async move {
2193                                (h)(p).await;
2194                            });
2195                        }
2196                    }
2197                }
2198            });
2199        }
2200    }
2201
2202    /// Register a handler invoked when a file-chooser dialog opens
2203    /// (`Page.fileChooserOpened`), mirroring Playwright's `page.on('filechooser')`.
2204    ///
2205    /// On first registration, chooser interception is enabled once via
2206    /// `Page.setInterceptFileChooserDialog { enabled: true }` (must be in place
2207    /// before the action that opens the chooser). The handler may inspect the
2208    /// [`FileChooser`] and call [`FileChooser::set_files`] to accept it.
2209    pub async fn on_filechooser<F, Fut>(&self, handler: F)
2210    where
2211        F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
2212        Fut: Future<Output = ()> + Send + 'static,
2213    {
2214        // Lazily enable interception + spawn the listener task exactly once.
2215        if self
2216            .inner
2217            .filechooser_started
2218            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2219            .is_ok()
2220        {
2221            // Subscribe before enabling so the first fileChooserOpened event
2222            // is never missed.
2223            let mut rx = self.inner.session.subscribe();
2224            // Enable interception and await it inline so the caller knows it is
2225            // in place before triggering the chooser.
2226            let _ = self
2227                .inner
2228                .session
2229                .send(
2230                    "Page.setInterceptFileChooserDialog",
2231                    json!({ "enabled": true }),
2232                )
2233                .await;
2234
2235            // Wrap the handler in an Arc for invocation from the task.
2236            let page = self.clone();
2237            let handler: Arc<
2238                dyn Fn(FileChooser) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>>
2239                    + Send
2240                    + Sync,
2241            > = Arc::new(move |fc| Box::pin(handler(fc)));
2242            tokio::spawn(async move {
2243                while let Ok(ev) = rx.recv().await {
2244                    if ev.method == "Page.fileChooserOpened" {
2245                        let multiple = ev
2246                            .params
2247                            .get("mode")
2248                            .and_then(|v| v.as_str())
2249                            == Some("selectMultiple");
2250                        let backend_node_id = ev
2251                            .params
2252                            .get("backendNodeId")
2253                            .and_then(|v| v.as_i64());
2254                        let fc = FileChooser::new(page.clone(), backend_node_id, multiple);
2255                        let h = Arc::clone(&handler);
2256                        tokio::spawn(async move {
2257                            (h)(fc).await;
2258                        });
2259                    }
2260                }
2261            });
2262        }
2263    }
2264
2265    /// Register a handler invoked when a web/service/shared worker is created,
2266    /// mirroring Playwright's `page.on('worker')`.
2267    ///
2268    /// On first registration, flattened auto-attach is enabled on the page
2269    /// session (`Target.setAutoAttach { flatten: true }`) so workers show up as
2270    /// child sessions on the same connection. Each child target of type
2271    /// `worker`/`service_worker`/`shared_worker` surfaces via
2272    /// `Target.attachedToTarget` and is wrapped in a [`Worker`].
2273    ///
2274    /// Subscribe before enabling so the first `attachedToTarget` is never missed.
2275    pub async fn on_worker<F, Fut>(&self, handler: F)
2276    where
2277        F: Fn(Worker) -> Fut + Send + Sync + 'static,
2278        Fut: Future<Output = ()> + Send + 'static,
2279    {
2280        // Lazily enable page-session auto-attach + spawn the listener once.
2281        if self
2282            .inner
2283            .worker_started
2284            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2285            .is_ok()
2286        {
2287            // Subscribe BEFORE enabling auto-attach so the attach event for a
2288            // worker spawned right after is captured.
2289            let mut rx = self.inner.session.subscribe();
2290            let _ = self
2291                .inner
2292                .session
2293                .send(
2294                    "Target.setAutoAttach",
2295                    json!({ "autoAttach": true, "waitForDebuggerOnStart": false, "flatten": true }),
2296                )
2297                .await;
2298
2299            let connection = self.inner.session.connection().clone();
2300            let handler: Arc<
2301                dyn Fn(Worker) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
2302            > = Arc::new(move |w| Box::pin(handler(w)));
2303            tokio::spawn(async move {
2304                while let Ok(ev) = rx.recv().await {
2305                    if ev.method != "Target.attachedToTarget" {
2306                        continue;
2307                    }
2308                    // Defensive casing: `sessionId` (modern) then `session_id`.
2309                    let session_id = ev
2310                        .params
2311                        .get("sessionId")
2312                        .or_else(|| ev.params.get("session_id"))
2313                        .and_then(|v| v.as_str());
2314                    let target_info = match ev.params.get("targetInfo") {
2315                        Some(t) => t,
2316                        None => continue,
2317                    };
2318                    let target_type = target_info
2319                        .get("type")
2320                        .and_then(|v| v.as_str())
2321                        .unwrap_or("");
2322                    // Only workers — skip iframes, pages, popups, etc.
2323                    if !matches!(target_type, "worker" | "service_worker" | "shared_worker") {
2324                        continue;
2325                    }
2326                    let (sid, url) = match (session_id, target_info.get("url").and_then(|v| v.as_str())) {
2327                        (Some(sid), Some(url)) => (sid.to_string(), url.to_string()),
2328                        _ => continue,
2329                    };
2330                    let worker = Worker::new(connection.clone(), sid, url);
2331                    // Enable Runtime on the worker session so evaluate works.
2332                    worker.enable_runtime().await;
2333                    let h = Arc::clone(&handler);
2334                    tokio::spawn(async move {
2335                        (h)(worker).await;
2336                    });
2337                }
2338            });
2339        }
2340    }
2341
2342    // --- close ---
2343
2344    pub async fn close(&self) -> Result<()> {
2345        if *self.inner.closed.lock() {
2346            return Ok(());
2347        }
2348        *self.inner.closed.lock() = true;
2349        let handlers = std::mem::take(&mut *self.inner.on_close_handlers.lock());
2350        for h in handlers {
2351            tokio::spawn(async move { (h)().await; });
2352        }
2353        let _ = self
2354            .inner
2355            .browser
2356            .browser_session()
2357            .send("Target.closeTarget", json!({ "targetId": self.inner.target_id }))
2358            .await;
2359        Ok(())
2360    }
2361
2362    // --- network interception (Fetch domain) ---
2363
2364    /// Intercept requests matching `pattern` (a URL glob, `*`-wildcarded) and
2365    /// route them to `handler`. The handler must continue/fulfill/abort the
2366    /// [`Route`] (an unhandled route stalls the request).
2367    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
2368    where
2369        F: Fn(Route) -> Fut + Send + Sync + 'static,
2370        Fut: Future<Output = ()> + Send + 'static,
2371    {
2372        let entry = RouteEntry {
2373            pattern: pattern.to_string(),
2374            handler: Arc::new(move |r| Box::pin(handler(r))),
2375        };
2376        self.inner.route_handlers.lock().push(entry);
2377        self.ensure_route_listener().await;
2378        self.refresh_fetch_patterns().await
2379    }
2380
2381    /// Remove the first route registered with exactly `pattern`.
2382    pub async fn unroute(&self, pattern: &str) -> Result<()> {
2383        let mut handlers = self.inner.route_handlers.lock();
2384        if let Some(pos) = handlers.iter().position(|e| e.pattern == pattern) {
2385            handlers.remove(pos);
2386        }
2387        drop(handlers);
2388        self.refresh_fetch_patterns().await
2389    }
2390
2391    /// Remove all routes.
2392    pub async fn unroute_all(&self) -> Result<()> {
2393        self.inner.route_handlers.lock().clear();
2394        let _ = self.inner.session.send("Fetch.disable", json!({})).await;
2395        Ok(())
2396    }
2397
2398    async fn ensure_route_listener(&self) {
2399        if self
2400            .inner
2401            .route_started
2402            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2403            .is_ok()
2404        {
2405            let rx = self.inner.session.subscribe();
2406            let session = Arc::clone(&self.inner.session);
2407            let store = Arc::clone(&self.inner.network_store);
2408            let handlers = Arc::clone(&self.inner.route_handlers);
2409            tokio::spawn(async move {
2410                route_listener(rx, session, store, handlers).await;
2411            });
2412        }
2413    }
2414
2415    async fn refresh_fetch_patterns(&self) -> Result<()> {
2416        let patterns: Vec<Value> = self
2417            .inner
2418            .route_handlers
2419            .lock()
2420            .iter()
2421            .map(|e| json!({ "urlPattern": e.pattern }))
2422            .collect();
2423        if patterns.is_empty() {
2424            let _ = self.inner.session.send("Fetch.disable", json!({})).await;
2425        } else {
2426            self.inner
2427                .session
2428                .send("Fetch.enable", json!({ "patterns": patterns }))
2429                .await?;
2430        }
2431        Ok(())
2432    }
2433
2434    // --- Tier 3 stubs (API completeness) ---
2435
2436    pub async fn pdf(&self, opts: Option<crate::options::PdfOptions>) -> Result<Vec<u8>> {
2437        let opts = opts.unwrap_or_default();
2438        let mut params = json!({ "printBackground": opts.print_background.unwrap_or(true) });
2439        if let Some(fmt) = opts.format {
2440            let (w, h) = fmt.inches();
2441            params["paperWidth"] = json!(w);
2442            params["paperHeight"] = json!(h);
2443        }
2444        if let Some(l) = opts.landscape {
2445            params["landscape"] = json!(l);
2446        }
2447        if let Some(s) = opts.scale {
2448            params["scale"] = json!(s);
2449        }
2450        if let Some(p) = opts.prefer_css_page_size {
2451            params["preferCSSPageSize"] = json!(p);
2452        }
2453        if let Some(m) = opts.margin {
2454            if let Some(v) = m.top {
2455                params["marginTop"] = json!(v);
2456            }
2457            if let Some(v) = m.bottom {
2458                params["marginBottom"] = json!(v);
2459            }
2460            if let Some(v) = m.left {
2461                params["marginLeft"] = json!(v);
2462            }
2463            if let Some(v) = m.right {
2464                params["marginRight"] = json!(v);
2465            }
2466        }
2467        let resp = self.inner.session.send("Page.printToPDF", params).await?;
2468        let data = resp
2469            .get("data")
2470            .and_then(|v| v.as_str())
2471            .ok_or_else(|| Error::ProtocolError("pdf missing data".into()))?;
2472        Ok(base64::engine::general_purpose::STANDARD
2473            .decode(data)
2474            .map_err(|e| Error::ProtocolError(format!("pdf base64 decode: {e}")))?)
2475    }
2476
2477    /// Capture an aria-snapshot (Playwright's YAML-ish accessibility-tree
2478    /// format) of the whole page.
2479    ///
2480    /// Fetches the full accessibility tree via `Accessibility.getFullAXTree`
2481    /// and serializes it with [`crate::aria_snapshot`]. The Accessibility
2482    /// domain is enabled best-effort first (some Chrome builds require it).
2483    pub async fn aria_snapshot(&self) -> Result<String> {
2484        let _ = self
2485            .inner
2486            .session
2487            .send("Accessibility.enable", json!({}))
2488            .await;
2489        let resp = self
2490            .inner
2491            .session
2492            .send("Accessibility.getFullAXTree", json!({}))
2493            .await?;
2494        let nodes = resp
2495            .get("nodes")
2496            .and_then(|v| v.as_array())
2497            .cloned()
2498            .unwrap_or_default();
2499        Ok(crate::aria_snapshot::serialize(&nodes, None))
2500    }
2501}
2502
2503fn attr_escape(s: &str) -> String {
2504    s.replace('\\', "\\\\").replace('"', "\\\"")
2505}
2506
2507/// Derive the CDP `securityOrigin` (`scheme://host[:port]`) from a URL string.
2508///
2509/// Opaque schemes (`about:`, `data:`, `blob:`) yield an empty origin, since
2510/// `DOMStorage` has nothing to scope to for them. A URL without a parseable
2511/// scheme also yields an empty origin.
2512fn derive_security_origin(url: &str) -> String {
2513    // Find the scheme delimiter.
2514    let after_scheme = match url.find("://") {
2515        Some(i) => &url[i + 3..],
2516        None => return String::new(),
2517    };
2518    let scheme = &url[..url.find("://").unwrap()];
2519    // Opaque/special schemes have no real origin.
2520    if matches!(scheme, "about" | "data" | "blob" | "javascript") {
2521        return String::new();
2522    }
2523    // The authority ends at the first `/`, `?`, or `#`.
2524    let end = after_scheme
2525        .find(|c: char| c == '/' || c == '?' || c == '#')
2526        .unwrap_or(after_scheme.len());
2527    let authority = &after_scheme[..end];
2528    if authority.is_empty() {
2529        return String::new();
2530    }
2531    format!("{scheme}://{authority}")
2532}
2533
2534/// Whether a JSON value is "truthy" for `wait_for_function`: `null`, `false`,
2535fn is_truthy(v: &Value) -> bool {
2536    match v {
2537        Value::Null => false,
2538        Value::Bool(b) => *b,
2539        Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
2540        Value::String(s) => !s.is_empty(),
2541        _ => true,
2542    }
2543}
2544
2545/// Match `pattern` against `value`: exact, or a leading/trailing/both `*` glob
2546/// (good enough for `wait_for_url`).
2547fn glob_matches(pattern: &str, value: &str) -> bool {
2548    if pattern == value {
2549        return true;
2550    }
2551    // `*middle*` → contains.
2552    if pattern.len() >= 2 && pattern.starts_with('*') && pattern.ends_with('*') {
2553        return value.contains(&pattern[1..pattern.len() - 1]);
2554    }
2555    if let Some(rest) = pattern.strip_prefix('*') {
2556        if value.ends_with(rest) {
2557            return true;
2558        }
2559    }
2560    if let Some(rest) = pattern.strip_suffix('*') {
2561        if value.starts_with(rest) {
2562            return true;
2563        }
2564    }
2565    false
2566}
2567
2568/// Recursively walk a `Page.getFrameTree` response into the frame store.
2569fn walk_frame_tree(node: &Value, frames: &mut HashMap<String, FrameData>) {
2570    let frame = match node.get("frame") {
2571        Some(f) => f,
2572        None => return,
2573    };
2574    let id = match frame.get("id").and_then(|v| v.as_str()) {
2575        Some(s) => s.to_string(),
2576        None => return,
2577    };
2578    let data = FrameData {
2579        url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2580        name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2581        parent_id: frame
2582            .get("parentId")
2583            .and_then(|v| v.as_str())
2584            .map(String::from),
2585        detached: false,
2586    };
2587    frames.insert(id, data);
2588    if let Some(children) = node.get("childFrames").and_then(|v| v.as_array()) {
2589        for child in children {
2590            walk_frame_tree(child, frames);
2591        }
2592    }
2593}
2594
2595/// Background task: keep the frame store fresh from `Page.frame*` events.
2596async fn frame_tracker(
2597    mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
2598    frames: Arc<Mutex<HashMap<String, FrameData>>>,
2599    main_id: Arc<Mutex<Option<String>>>,
2600) {
2601    loop {
2602        match rx.recv().await {
2603            Ok(ev) => match ev.method.as_str() {
2604                "Page.frameNavigated" => {
2605                    if let Some(frame) = ev.params.get("frame") {
2606                        let id = match frame.get("id").and_then(|v| v.as_str()) {
2607                            Some(s) => s.to_string(),
2608                            None => continue,
2609                        };
2610                        let parent = frame
2611                            .get("parentId")
2612                            .and_then(|v| v.as_str())
2613                            .map(String::from);
2614                        let data = FrameData {
2615                            url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2616                            name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2617                            parent_id: parent.clone(),
2618                            detached: false,
2619                        };
2620                        if parent.is_none() {
2621                            *main_id.lock() = Some(id.clone());
2622                        }
2623                        frames.lock().insert(id, data);
2624                    }
2625                }
2626                "Page.frameDetached" => {
2627                    if let Some(id) = ev.params.get("frameId").and_then(|v| v.as_str()) {
2628                        if let Some(d) = frames.lock().get_mut(id) {
2629                            d.detached = true;
2630                        }
2631                    }
2632                }
2633                _ => {}
2634            },
2635            Err(broadcast::error::RecvError::Closed) => break,
2636            Err(broadcast::error::RecvError::Lagged(_)) => continue,
2637        }
2638    }
2639}
2640
2641/// Background task: dispatch `Page.downloadWillBegin`/`downloadProgress` events.
2642async fn download_listener(
2643    rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
2644    _session: &Arc<CdpSession>,
2645    states: &Arc<Mutex<HashMap<String, DownloadStateCell>>>,
2646    download_path: &std::path::Path,
2647    page: Page,
2648    handler: &Arc<
2649        dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
2650    >,
2651) {
2652    while let Ok(ev) = rx.recv().await {
2653        match ev.method.as_str() {
2654            "Page.downloadWillBegin" => {
2655                let guid = ev
2656                    .params
2657                    .get("guid")
2658                    .and_then(|v| v.as_str())
2659                    .unwrap_or("")
2660                    .to_string();
2661                let url = ev
2662                    .params
2663                    .get("url")
2664                    .and_then(|v| v.as_str())
2665                    .unwrap_or("")
2666                    .to_string();
2667                let suggested = ev
2668                    .params
2669                    .get("suggestedFilename")
2670                    .and_then(|v| v.as_str())
2671                    .unwrap_or("download")
2672                    .to_string();
2673                let cell = DownloadStateCell::new();
2674                states.lock().insert(guid.clone(), cell.clone());
2675                let dl = Download::new(
2676                    url,
2677                    suggested,
2678                    guid,
2679                    cell,
2680                    download_path.to_path_buf(),
2681                    page.clone(),
2682                );
2683                let h = Arc::clone(handler);
2684                tokio::spawn(async move {
2685                    (h)(dl).await;
2686                });
2687            }
2688            "Page.downloadProgress" => {
2689                let guid = match ev.params.get("guid").and_then(|v| v.as_str()) {
2690                    Some(g) => g.to_string(),
2691                    None => continue,
2692                };
2693                let state = ev
2694                    .params
2695                    .get("state")
2696                    .and_then(|v| v.as_str())
2697                    .unwrap_or("InProgress");
2698                let mapped = match state {
2699                    "Completed" => DownloadState::Completed,
2700                    "Canceled" => DownloadState::Canceled,
2701                    _ => DownloadState::InProgress,
2702                };
2703                if let Some(cell) = states.lock().get(&guid) {
2704                    *cell.state.lock() = mapped;
2705                }
2706            }
2707            _ => {}
2708        }
2709    }
2710}
2711
2712/// The distinct internal name under which the CDP binding for the public
2713/// `name` is registered (`Runtime.addBinding`). Keeping it separate avoids
2714/// clashing with the Promise-returning `window[name]` wrapper.
2715fn internal_binding_name(name: &str) -> String {
2716    format!("__pwcdpInvoke_{name}")
2717}
2718
2719/// The JS wrapper that turns `window[name]` into a Promise-returning function.
2720/// Each call assigns a monotonic id, registers its `resolve`, and forwards
2721/// `{ id, args }` (as a JSON string) to the CDP binding under
2722/// `__pwcdpInvoke_<name>`. The Rust listener resolves the promise later.
2723fn binding_wrapper_source(name: &str) -> String {
2724    // `name` is interpolated as a JSON string literal so it is safe to embed
2725    // even if it contains quotes/backslashes.
2726    let name_json = serde_json::to_string(name).unwrap_or_else(|_| "\"\"".to_string());
2727    format!(
2728        r#"(function(){{
2729  var publicName = {name_json};
2730  var invokeName = "__pwcdpInvoke_" + publicName;
2731  var pending = (self.__pwcdpBindings = self.__pwcdpBindings || {{}});
2732  var map = pending[publicName] = pending[publicName] || {{ id: 0, cbs: {{}} }};
2733  self[publicName] = function(){{
2734    var args = Array.prototype.slice.call(arguments);
2735    return new Promise(function(resolve){{
2736      var id = ++map.id;
2737      map.cbs[id] = resolve;
2738      // The CDP binding (self[invokeName]) is installed by Runtime.addBinding;
2739      // if it is not present yet, surface an error so callers see a clear cause.
2740      if (typeof self[invokeName] !== "function") {{
2741        delete map.cbs[id];
2742        resolve({{ "__error": "binding not installed: " + invokeName }});
2743        return;
2744      }}
2745      try {{
2746        self[invokeName](JSON.stringify({{ id: id, args: args }}));
2747      }} catch (e) {{
2748        delete map.cbs[id];
2749        resolve({{ "__error": String((e && e.message) || e) }});
2750      }}
2751    }});
2752  }};
2753}})();
2754"#
2755    )
2756}
2757
2758/// Background task for one `expose_function` registration: on
2759/// `Runtime.bindingCalled` for the internal binding name, parse the payload,
2760/// run the Rust callback on its own task, then resolve the JS promise.
2761async fn binding_listener(
2762    rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
2763    session: &Arc<CdpSession>,
2764    name: &str,
2765    handler: &ExposedBindingHandler,
2766) {
2767    let binding_name = internal_binding_name(name);
2768    loop {
2769        match rx.recv().await {
2770            Ok(ev) if ev.method == "Runtime.bindingCalled" => {
2771                let fired_name = ev
2772                    .params
2773                    .get("name")
2774                    .and_then(|v| v.as_str())
2775                    .unwrap_or("");
2776                if fired_name != binding_name {
2777                    continue;
2778                }
2779                let payload_str = match ev.params.get("payload").and_then(|v| v.as_str()) {
2780                    Some(s) => s,
2781                    None => continue,
2782                };
2783                let parsed: Value = match serde_json::from_str(payload_str) {
2784                    Ok(v) => v,
2785                    Err(_) => continue,
2786                };
2787                let id = match parsed.get("id").and_then(|v| v.as_i64()) {
2788                    Some(i) => i,
2789                    None => continue,
2790                };
2791                let args = parsed
2792                    .get("args")
2793                    .and_then(|v| v.as_array())
2794                    .cloned()
2795                    .unwrap_or_default();
2796
2797                let h = Arc::clone(handler);
2798                let session = Arc::clone(session);
2799                let name_owned = name.to_string();
2800                // Resolve on a separate task so a slow callback doesn't stall
2801                // event processing.
2802                tokio::spawn(async move {
2803                    let result = (h)(args).await;
2804                    let result_json = serde_json::to_string(&result)
2805                        .unwrap_or_else(|_| r#"{"__error":"serialize failed"}"#.to_string());
2806                    // Re-serialize as a JSON string literal so it can be passed
2807                    // to JSON.parse safely (handles quotes/newlines/etc.).
2808                    let result_str_literal = serde_json::to_string(&result_json)
2809                        .unwrap_or_else(|_| r#""{\"__error\":\"serialize failed\"}""#.to_string());
2810                    // Resolve the pending promise: look up the resolve fn by id,
2811                    // call it with the parsed result, then delete the entry.
2812                    let name_j = serde_json::to_string(&name_owned).unwrap_or_else(|_| "\"\"".into());
2813                    let expr = format!(
2814                        "(function(){{
2815  var m = (self.__pwcdpBindings && self.__pwcdpBindings[{name_j}]) || null;
2816  if (!m || !m.cbs || !m.cbs[{id}]) return;
2817  var fn_ = m.cbs[{id}]; delete m.cbs[{id}];
2818  fn_(JSON.parse({result_str_literal}));
2819}})();"
2820                    );
2821                    let _ = session
2822                        .send(
2823                            "Runtime.evaluate",
2824                            json!({ "expression": expr, "awaitPromise": false }),
2825                        )
2826                        .await;
2827                });
2828            }
2829            Ok(_) => {}
2830            Err(broadcast::error::RecvError::Closed) => break,
2831            Err(broadcast::error::RecvError::Lagged(_)) => continue,
2832        }
2833    }
2834}
2835
2836fn parse_console(params: &Value) -> ConsoleMessage {    let text = params
2837        .get("args")
2838        .and_then(|a| a.as_array())
2839        .map(|args| {
2840            args.iter()
2841                .filter_map(|a| {
2842                    a.get("value")
2843                        .and_then(|v| v.as_str())
2844                        .or_else(|| a.get("description").and_then(|v| v.as_str()))
2845                        .map(String::from)
2846                })
2847                .collect::<Vec<_>>()
2848                .join(" ")
2849        })
2850        .unwrap_or_default();
2851    let kind = params
2852        .get("type")
2853        .and_then(|v| v.as_str())
2854        .unwrap_or("log")
2855        .to_string();
2856    // Source location from the top call frame of the reported stack trace.
2857    let location = params
2858        .get("stackTrace")
2859        .and_then(|s| s.get("callFrames"))
2860        .and_then(|f| f.as_array())
2861        .and_then(|frames| frames.first())
2862        .map(|frame| {
2863            use crate::types::ConsoleMessageLocation;
2864            ConsoleMessageLocation {
2865                url: frame
2866                    .get("url")
2867                    .and_then(|v| v.as_str())
2868                    .map(String::from),
2869                line_number: frame.get("lineNumber").and_then(|v| v.as_i64()),
2870                column_number: frame.get("columnNumber").and_then(|v| v.as_i64()),
2871            }
2872        });
2873    ConsoleMessage {
2874        text,
2875        r#type: kind,
2876        location,
2877    }
2878}
2879
2880/// Background task: track the main-world execution context and keep the
2881/// selector engine installed after navigations. Also records per-frame
2882/// contexts (frame id → context id) from `auxData.frameId` so child-frame
2883/// evaluation/locator resolution can target a frame's own context.
2884///
2885/// `main_world_ctx` is set ONLY for the main (top) frame's default context —
2886/// a child iframe also has a "default" context, and accepting every default
2887/// context would let the child's context clobber the main one.
2888async fn context_tracker(
2889    mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
2890    session: Arc<CdpSession>,
2891    ctx_cell: Arc<Mutex<Option<i64>>>,
2892    frame_ctx_cell: Arc<Mutex<HashMap<String, i64>>>,
2893    main_frame_id: Arc<Mutex<Option<String>>>,
2894) {
2895    loop {
2896        match rx.recv().await {
2897            Ok(ev) => match ev.method.as_str() {
2898                "Runtime.executionContextCreated" => {
2899                    let context = match ev.params.get("context") {
2900                        Some(c) => c,
2901                        None => continue,
2902                    };
2903                    let aux = context.get("auxData");
2904                    let is_default = aux
2905                        .and_then(|a| a.get("type"))
2906                        .and_then(|t| t.as_str())
2907                        == Some("default");
2908                    let id = context.get("id").and_then(|i| i.as_i64());
2909                    let Some(id) = id else { continue };
2910
2911                    let frame_id = aux
2912                        .and_then(|a| a.get("frameId"))
2913                        .and_then(|v| v.as_str())
2914                        .map(String::from);
2915
2916                    if is_default {
2917                        // Record the frame → context mapping. Only default
2918                        // (main-world) contexts are tracked; utility/isolated
2919                        // worlds have their own ids and must not shadow a
2920                        // frame's main context.
2921                        if let Some(fid) = &frame_id {
2922                            frame_ctx_cell.lock().insert(fid.clone(), id);
2923                        }
2924
2925                        // The page-wide "main" context is the main frame's
2926                        // default context. A child iframe's default context
2927                        // must NOT overwrite it. Accept this context as main
2928                        // when (a) its frame is the known main frame, or
2929                        // (b) the main frame id is not yet known and no main
2930                        // context has been recorded yet (initial top-frame
2931                        // context typically arrives before the frame tree is
2932                        // populated).
2933                        let main_id = main_frame_id.lock().clone();
2934                        let is_main_frame = match (&main_id, &frame_id) {
2935                            (Some(m), Some(f)) => m == f,
2936                            (None, _) => ctx_cell.lock().is_none(),
2937                            // main frame known but this context has no frameId:
2938                            // treat as main only if none recorded yet.
2939                            (Some(_), None) => ctx_cell.lock().is_none(),
2940                        };
2941                        if is_main_frame {
2942                            *ctx_cell.lock() = Some(id);
2943                        }
2944                    }
2945
2946                    // Ensure the engine is installed in this context.
2947                    let s = Arc::clone(&session);
2948                    tokio::spawn(async move {
2949                        let _ = s
2950                            .send(
2951                                "Runtime.evaluate",
2952                                json!({
2953                                    "expression": selectors::INJECTED_SCRIPT,
2954                                    "contextId": id,
2955                                }),
2956                            )
2957                            .await;
2958                    });
2959                }
2960                "Runtime.executionContextsCleared" => {
2961                    *ctx_cell.lock() = None;
2962                    frame_ctx_cell.lock().clear();
2963                }
2964                "Runtime.executionContextDestroyed" => {
2965                    let id = ev
2966                        .params
2967                        .get("executionContextId")
2968                        .and_then(|v| v.as_i64());
2969                    {
2970                        let mut cell = ctx_cell.lock();
2971                        if id.is_some() && *cell == id {
2972                            *cell = None;
2973                        }
2974                    }
2975                    if let Some(id) = id {
2976                        let mut frames = frame_ctx_cell.lock();
2977                        // Drop any frame→context entries that pointed at the
2978                        // destroyed context (a new contextCreated will repopulate).
2979                        frames.retain(|_, v| *v != id);
2980                    }
2981                }
2982                _ => {}
2983            },
2984            Err(broadcast::error::RecvError::Closed) => break,
2985            Err(broadcast::error::RecvError::Lagged(_)) => continue,
2986        }
2987    }
2988}
2989
2990/// A frame lifecycle event payload for `on_frameattached` /
2991/// `on_framedetached` / `on_framenavigated`. Carries the frame id, its
2992/// parent's id (if any), and — for navigations — the frame's new URL.
2993#[derive(Debug, Clone)]
2994pub struct FrameEvent {
2995    /// The id of the frame the event concerns.
2996    pub id: String,
2997    /// The parent frame id, if the frame has one (the main frame does not).
2998    pub parent_id: Option<String>,
2999    /// The frame's URL. Populated for navigations; empty for attach/detach.
3000    pub url: String,
3001}
3002
3003impl FrameEvent {
3004    /// Parse `Page.frameAttached` params (`params.frame.{id,parentFrameId}`).
3005    pub(crate) fn from_attached(params: &Value) -> Option<Self> {
3006        let frame = params.get("frame").unwrap_or(params);
3007        let id = frame.get("id").and_then(|v| v.as_str())?.to_string();
3008        let parent_id = frame
3009            .get("parentFrameId")
3010            .and_then(|v| v.as_str())
3011            .map(String::from);
3012        Some(Self {
3013            id,
3014            parent_id,
3015            url: String::new(),
3016        })
3017    }
3018
3019    /// Parse `Page.frameDetached` params (`params.frameId`, plus any `frame`).
3020    pub(crate) fn from_detached(params: &Value) -> Option<Self> {
3021        // frameDetached carries a bare `frameId`; `frame` may be absent.
3022        let id = params
3023            .get("frameId")
3024            .or_else(|| params.get("frame"))
3025            .and_then(|v| {
3026                v.as_str()
3027                    .map(String::from)
3028                    .or_else(|| v.get("id").and_then(|i| i.as_str()).map(String::from))
3029            })?;
3030        let parent_id = params
3031            .get("frame")
3032            .and_then(|f| f.get("parentFrameId"))
3033            .and_then(|v| v.as_str())
3034            .map(String::from);
3035        Some(Self {
3036            id,
3037            parent_id,
3038            url: String::new(),
3039        })
3040    }
3041
3042    /// Parse `Page.frameNavigated` params (`params.frame.{id,url,parentId}`).
3043    pub(crate) fn from_navigated(params: &Value) -> Option<Self> {
3044        let frame = params.get("frame")?;
3045        let id = frame.get("id").and_then(|v| v.as_str())?.to_string();
3046        let url = frame
3047            .get("url")
3048            .and_then(|v| v.as_str())
3049            .unwrap_or("")
3050            .to_string();
3051        let parent_id = frame
3052            .get("parentId")
3053            .and_then(|v| v.as_str())
3054            .map(String::from);
3055        Some(Self {
3056            id,
3057            parent_id,
3058            url,
3059        })
3060    }
3061}
3062
3063/// A JavaScript dialog (alert/confirm/prompt/beforeunload).
3064pub struct Dialog {
3065    message: String,
3066    kind: String,
3067    session: Arc<CdpSession>,
3068}
3069
3070impl Dialog {
3071    pub(crate) fn from_event(params: &Value, session: &Arc<CdpSession>) -> Self {
3072        Self {
3073            message: params
3074                .get("message")
3075                .and_then(|v| v.as_str())
3076                .unwrap_or("")
3077                .to_string(),
3078            kind: params
3079                .get("type")
3080                .and_then(|v| v.as_str())
3081                .unwrap_or("alert")
3082                .to_string(),
3083            session: Arc::clone(session),
3084        }
3085    }
3086
3087    pub fn message(&self) -> &str {
3088        &self.message
3089    }
3090
3091    pub fn kind(&self) -> &str {
3092        &self.kind
3093    }
3094
3095    pub async fn accept(&self, prompt_text: Option<&str>) -> Result<()> {
3096        let mut p = json!({ "accept": true });
3097        if let Some(t) = prompt_text {
3098            p["promptText"] = json!(t);
3099        }
3100        self.session
3101            .send("Page.handleJavaScriptDialog", p)
3102            .await
3103            .map(|_| ())
3104    }
3105
3106    pub async fn dismiss(&self) -> Result<()> {
3107        self.session
3108            .send("Page.handleJavaScriptDialog", json!({ "accept": false }))
3109            .await
3110            .map(|_| ())
3111    }
3112}