Skip to main content

chromiumoxide/handler/
page.rs

1use std::sync::atomic::{AtomicUsize, Ordering};
2use std::sync::Arc;
3
4use chromiumoxide_cdp::cdp::browser_protocol::accessibility::{
5    GetFullAxTreeParamsBuilder, GetFullAxTreeReturns, GetPartialAxTreeParamsBuilder,
6    GetPartialAxTreeReturns,
7};
8use tokio::sync::mpsc::{channel, Receiver};
9use tokio::sync::oneshot::channel as oneshot_channel;
10use tokio::sync::Notify;
11
12use chromiumoxide_cdp::cdp::browser_protocol::browser::{GetVersionParams, GetVersionReturns};
13use chromiumoxide_cdp::cdp::browser_protocol::dom::{
14    BackendNodeId, DiscardSearchResultsParams, GetOuterHtmlParams, GetSearchResultsParams, NodeId,
15    PerformSearchParams, QuerySelectorAllParams, QuerySelectorParams, Rgba,
16};
17use chromiumoxide_cdp::cdp::browser_protocol::emulation::{
18    ClearDeviceMetricsOverrideParams, SetDefaultBackgroundColorOverrideParams,
19    SetDeviceMetricsOverrideParams,
20};
21use chromiumoxide_cdp::cdp::browser_protocol::input::{
22    DispatchDragEventParams, DispatchDragEventType, DispatchKeyEventParams, DispatchKeyEventType,
23    DispatchMouseEventParams, DispatchMouseEventType, DragData, MouseButton,
24};
25use chromiumoxide_cdp::cdp::browser_protocol::page::{
26    FrameId, GetLayoutMetricsParams, GetLayoutMetricsReturns, PrintToPdfParams, SetBypassCspParams,
27    Viewport,
28};
29use chromiumoxide_cdp::cdp::browser_protocol::target::{ActivateTargetParams, SessionId, TargetId};
30use chromiumoxide_cdp::cdp::js_protocol::runtime::{
31    CallFunctionOnParams, CallFunctionOnReturns, EvaluateParams, ExecutionContextId, RemoteObjectId,
32};
33use chromiumoxide_types::{Command, CommandResponse};
34
35use crate::cmd::{to_command_response, CommandMessage};
36use crate::error::{CdpError, Result};
37use crate::handler::commandfuture::CommandFuture;
38use crate::handler::domworld::DOMWorldKind;
39use crate::handler::httpfuture::HttpFuture;
40use crate::handler::sender::PageSender;
41use crate::handler::target::{GetExecutionContext, TargetMessage};
42use crate::handler::target_message_future::TargetMessageFuture;
43use crate::js::EvaluationResult;
44use crate::layout::{Delta, Point, ScrollBehavior};
45use crate::mouse::SmartMouse;
46use crate::page::ScreenshotParams;
47use crate::{keys, utils, ArcHttpRequest};
48
49/// Global count of live `PageInner` instances. Incremented on creation,
50/// decremented on `Drop`. Used to dynamically tune memory-sensitive
51/// thresholds (e.g. CDP body-streaming chunk size) under high concurrency.
52static ACTIVE_PAGES: AtomicUsize = AtomicUsize::new(0);
53
54/// Returns the number of currently live page instances across the process.
55#[inline]
56pub fn active_page_count() -> usize {
57    ACTIVE_PAGES.load(Ordering::Relaxed)
58}
59
60#[derive(Debug)]
61pub struct PageHandle {
62    pub(crate) rx: Receiver<TargetMessage>,
63    page: Arc<PageInner>,
64}
65
66/// Default capacity of the per-page `TargetMessage` channel.
67///
68/// Historical hard-coded value preserved for backwards compatibility —
69/// `PageHandle::new` delegates to `PageHandle::with_capacity` with this
70/// value. Override via `HandlerConfig::page_channel_capacity` (plumbed
71/// through `BrowserConfigBuilder::page_channel_capacity`) to tune under
72/// bursty per-page command load, which otherwise forces every extra
73/// command into the `CommandFuture` async-send fallback path.
74pub(crate) const DEFAULT_PAGE_CHANNEL_CAPACITY: usize = 2048;
75
76impl PageHandle {
77    /// Create a `PageHandle` with the default per-page channel capacity
78    /// (`DEFAULT_PAGE_CHANNEL_CAPACITY`). Preserved unchanged for
79    /// backwards compatibility — call `with_capacity` to override.
80    pub fn new(
81        target_id: TargetId,
82        session_id: SessionId,
83        opener_id: Option<TargetId>,
84        request_timeout: std::time::Duration,
85        page_wake: Option<Arc<Notify>>,
86    ) -> Self {
87        Self::with_capacity(
88            target_id,
89            session_id,
90            opener_id,
91            request_timeout,
92            page_wake,
93            DEFAULT_PAGE_CHANNEL_CAPACITY,
94        )
95    }
96
97    /// Create a `PageHandle` with a caller-chosen channel capacity.
98    ///
99    /// `capacity` is the tokio mpsc buffer size for `TargetMessage`s flowing
100    /// from this page to the handler. Capacity is clamped to at least `1`
101    /// because `tokio::sync::mpsc::channel(0)` panics; callers passing `0`
102    /// get a 1-slot channel instead of an abort.
103    ///
104    /// Under bursty per-page load, larger capacities reduce the rate at
105    /// which `CommandFuture` / `TargetMessageFuture` fall back to the
106    /// boxed async-send slow path on `TrySendError::Full`; smaller
107    /// capacities apply back-pressure sooner at the cost of that fallback.
108    pub fn with_capacity(
109        target_id: TargetId,
110        session_id: SessionId,
111        opener_id: Option<TargetId>,
112        request_timeout: std::time::Duration,
113        page_wake: Option<Arc<Notify>>,
114        capacity: usize,
115    ) -> Self {
116        let (commands, rx) = channel(capacity.max(1));
117        let page = PageInner {
118            target_id,
119            session_id,
120            opener_id,
121            sender: PageSender::new(commands, page_wake),
122            smart_mouse: SmartMouse::new(),
123            request_timeout,
124        };
125        ACTIVE_PAGES.fetch_add(1, Ordering::Relaxed);
126        Self {
127            rx,
128            page: Arc::new(page),
129        }
130    }
131
132    pub(crate) fn inner(&self) -> &Arc<PageInner> {
133        &self.page
134    }
135}
136
137#[derive(Debug)]
138pub(crate) struct PageInner {
139    /// The page target ID.
140    target_id: TargetId,
141    /// The session ID.
142    session_id: SessionId,
143    /// The opener ID.
144    opener_id: Option<TargetId>,
145    /// The sender for the target (with optional handler notification).
146    sender: PageSender,
147    /// Smart mouse with position tracking and human-like movement.
148    pub(crate) smart_mouse: SmartMouse,
149    /// The request timeout for CDP commands issued from this page.
150    request_timeout: std::time::Duration,
151}
152
153impl Drop for PageInner {
154    fn drop(&mut self) {
155        ACTIVE_PAGES.fetch_sub(1, Ordering::Relaxed);
156    }
157}
158
159impl PageInner {
160    /// Execute a PDL command and return its response
161    pub(crate) async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
162        execute(
163            cmd,
164            self.sender.clone(),
165            Some(self.session_id.clone()),
166            self.request_timeout,
167        )
168        .await
169    }
170
171    /// Execute a PDL command without waiting for the response.
172    pub(crate) async fn send_command<T: Command>(&self, cmd: T) -> Result<&Self> {
173        let _ = send_command(
174            cmd,
175            self.sender.clone(),
176            Some(self.session_id.clone()),
177            self.request_timeout,
178        )
179        .await;
180        Ok(self)
181    }
182
183    /// Create a PDL command future
184    pub(crate) fn command_future<T: Command>(&self, cmd: T) -> Result<CommandFuture<T>> {
185        CommandFuture::new(
186            cmd,
187            self.sender.clone(),
188            Some(self.session_id.clone()),
189            self.request_timeout,
190        )
191    }
192
193    /// This creates navigation future with the final http response when the page is loaded
194    pub(crate) fn wait_for_navigation(&self) -> TargetMessageFuture<ArcHttpRequest> {
195        TargetMessageFuture::<ArcHttpRequest>::wait_for_navigation(
196            self.sender.clone(),
197            self.request_timeout,
198        )
199    }
200
201    /// Resolves once `DOMContentLoaded` fires (before `load`).
202    pub(crate) fn wait_for_dom_content_loaded(&self) -> TargetMessageFuture<ArcHttpRequest> {
203        TargetMessageFuture::<ArcHttpRequest>::wait_for_dom_content_loaded(
204            self.sender.clone(),
205            self.request_timeout,
206        )
207    }
208
209    /// Resolves once the `load` event fires (all subresources done).
210    pub(crate) fn wait_for_load(&self) -> TargetMessageFuture<ArcHttpRequest> {
211        TargetMessageFuture::<ArcHttpRequest>::wait_for_load(
212            self.sender.clone(),
213            self.request_timeout,
214        )
215    }
216
217    /// This creates navigation future with the final http response when the page network is idle
218    pub(crate) fn wait_for_network_idle(&self) -> TargetMessageFuture<ArcHttpRequest> {
219        TargetMessageFuture::<ArcHttpRequest>::wait_for_network_idle(
220            self.sender.clone(),
221            self.request_timeout,
222        )
223    }
224
225    /// This creates navigation future with the final http response when the page network is almost idle
226    pub(crate) fn wait_for_network_almost_idle(&self) -> TargetMessageFuture<ArcHttpRequest> {
227        TargetMessageFuture::<ArcHttpRequest>::wait_for_network_almost_idle(
228            self.sender.clone(),
229            self.request_timeout,
230        )
231    }
232
233    /// This creates HTTP future with navigation and responds with the final
234    /// http response when the page is loaded
235    pub(crate) fn http_future<T: Command>(&self, cmd: T) -> Result<HttpFuture<T>> {
236        Ok(HttpFuture::new(
237            self.sender.clone(),
238            self.command_future(cmd)?,
239            self.request_timeout,
240        ))
241    }
242
243    /// The identifier of this page's target
244    pub fn target_id(&self) -> &TargetId {
245        &self.target_id
246    }
247
248    /// The identifier of this page's target's session
249    pub fn session_id(&self) -> &SessionId {
250        &self.session_id
251    }
252
253    /// The identifier of this page's target's opener target
254    pub fn opener_id(&self) -> &Option<TargetId> {
255        &self.opener_id
256    }
257
258    /// Send a `TargetMessage` with the page's request timeout.
259    /// Uses a `try_send` fast path to avoid the async overhead when
260    /// the channel has capacity (common case under normal load).
261    pub(crate) async fn send_msg(&self, msg: TargetMessage) -> Result<()> {
262        match self.sender.try_send(msg) {
263            Ok(()) => Ok(()),
264            Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
265                tokio::time::timeout(self.request_timeout, self.sender.send(msg))
266                    .await
267                    .map_err(|_| CdpError::Timeout)?
268                    .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
269                Ok(())
270            }
271            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
272                Err(CdpError::ChannelSendError(crate::error::ChannelError::Send))
273            }
274        }
275    }
276
277    /// Await a oneshot response with the page's request timeout.
278    pub(crate) async fn recv_msg<T>(&self, rx: tokio::sync::oneshot::Receiver<T>) -> Result<T> {
279        tokio::time::timeout(self.request_timeout, rx)
280            .await
281            .map_err(|_| CdpError::Timeout)?
282            .map_err(|e| CdpError::ChannelSendError(crate::error::ChannelError::Canceled(e)))
283    }
284
285    /// Returns the first element in the node which matches the given CSS
286    /// selector.
287    pub async fn find_element(&self, selector: impl Into<String>, node: NodeId) -> Result<NodeId> {
288        Ok(self
289            .execute(QuerySelectorParams::new(node, selector))
290            .await?
291            .node_id)
292    }
293
294    /// Returns the outer html of the page.
295    pub async fn outer_html(
296        &self,
297        object_id: RemoteObjectId,
298        node_id: NodeId,
299        backend_node_id: BackendNodeId,
300    ) -> Result<String> {
301        let cmd = GetOuterHtmlParams {
302            backend_node_id: Some(backend_node_id),
303            node_id: Some(node_id),
304            object_id: Some(object_id),
305            ..Default::default()
306        };
307
308        let chromiumoxide_types::CommandResponse { result, .. } = self.execute(cmd).await?;
309
310        Ok(result.outer_html)
311    }
312
313    /// Activates (focuses) the target.
314    pub async fn activate(&self) -> Result<&Self> {
315        self.execute(ActivateTargetParams::new(self.target_id().clone()))
316            .await?;
317        Ok(self)
318    }
319
320    /// Version information about the browser
321    pub async fn version(&self) -> Result<GetVersionReturns> {
322        Ok(self.execute(GetVersionParams::default()).await?.result)
323    }
324
325    /// Return all `Element`s inside the node that match the given selector
326    pub(crate) async fn find_elements(
327        &self,
328        selector: impl Into<String>,
329        node: NodeId,
330    ) -> Result<Vec<NodeId>> {
331        Ok(self
332            .execute(QuerySelectorAllParams::new(node, selector))
333            .await?
334            .result
335            .node_ids)
336    }
337
338    /// Returns all elements which matches the given xpath selector
339    pub async fn find_xpaths(&self, query: impl Into<String>) -> Result<Vec<NodeId>> {
340        let perform_search_returns = self
341            .execute(PerformSearchParams {
342                query: query.into(),
343                include_user_agent_shadow_dom: Some(true),
344            })
345            .await?
346            .result;
347
348        let search_results = self
349            .execute(GetSearchResultsParams::new(
350                perform_search_returns.search_id.clone(),
351                0,
352                perform_search_returns.result_count,
353            ))
354            .await?
355            .result;
356
357        self.execute(DiscardSearchResultsParams::new(
358            perform_search_returns.search_id,
359        ))
360        .await?;
361
362        Ok(search_results.node_ids)
363    }
364
365    /// Moves the mouse to this point (dispatches a mouseMoved event).
366    /// Also updates the tracked mouse position.
367    pub async fn move_mouse(&self, point: Point) -> Result<&Self> {
368        self.smart_mouse.set_position(point);
369        self.execute(DispatchMouseEventParams::new(
370            DispatchMouseEventType::MouseMoved,
371            point.x,
372            point.y,
373        ))
374        .await?;
375        Ok(self)
376    }
377
378    /// Moves the mouse to `target` along a human-like bezier curve path,
379    /// dispatching intermediate `mouseMoved` events with natural timing.
380    ///
381    /// Concurrency shape:
382    /// * Each step is dispatched fire-and-forget via
383    ///   [`send_command`](Self::send_command) — the CDP command lands on
384    ///   the handler channel (with `try_send` + async fallback) and the
385    ///   response oneshot is dropped. Per-step CDP round-trips (~1-2ms
386    ///   each) are eliminated.
387    /// * Pacing uses [`tokio::time::sleep_until`] against an absolute
388    ///   deadline that accumulates each step's delay. This is
389    ///   **drift-free**: if one iteration's send or scheduler wake-up
390    ///   runs long, the next deadline is unchanged so the following
391    ///   wait shrinks to compensate. Total wall-clock convergence is
392    ///   `sum(step.delay)` regardless of per-iteration variance.
393    ///
394    /// Ordering: CDP processes commands in the order they arrive on the
395    /// single-session WebSocket, and the bounded mpsc between `Page`
396    /// and handler is FIFO, so the sequence of `mouseMoved` events
397    /// reaches Chrome in issue order.
398    pub async fn move_mouse_smooth(&self, target: Point) -> Result<&Self> {
399        let path = self.smart_mouse.path_to(target);
400        let last_idx = path.len().saturating_sub(1);
401        let mut deadline = tokio::time::Instant::now();
402        for (i, step) in path.iter().enumerate() {
403            self.send_command(DispatchMouseEventParams::new(
404                DispatchMouseEventType::MouseMoved,
405                step.point.x,
406                step.point.y,
407            ))
408            .await?;
409            // Absolute-deadline pacing: advancing by `step.delay`
410            // means a slow send on iteration N shortens the wait
411            // before iteration N+1 instead of pushing the whole
412            // schedule back.
413            //
414            // Skip the sleep after the final step — there's no
415            // subsequent event to pace against, so the delay would
416            // be pure wall-clock waste (~5-30ms typical).
417            if i < last_idx {
418                deadline += step.delay;
419                tokio::time::sleep_until(deadline).await;
420            }
421        }
422        Ok(self)
423    }
424
425    /// Returns the current tracked mouse position.
426    pub fn mouse_position(&self) -> Point {
427        self.smart_mouse.position()
428    }
429
430    /// Scrolls the current page by the specified horizontal and vertical offsets.
431    /// This method helps when Chrome version may not support certain CDP dispatch events.
432    pub async fn scroll_by(
433        &self,
434        delta_x: f64,
435        delta_y: f64,
436        behavior: ScrollBehavior,
437    ) -> Result<&Self> {
438        let behavior_str = match behavior {
439            ScrollBehavior::Auto => "auto",
440            ScrollBehavior::Instant => "instant",
441            ScrollBehavior::Smooth => "smooth",
442        };
443
444        self.evaluate_expression(format!(
445            "window.scrollBy({{top: {}, left: {}, behavior: '{}'}});",
446            delta_y, delta_x, behavior_str
447        ))
448        .await?;
449
450        Ok(self)
451    }
452
453    /// Dispatches a `DragEvent`, moving the element to the given `point`.
454    ///
455    /// `point.x` defines the horizontal target, and `point.y` the vertical mouse position.
456    /// Accepts `drag_type`, `drag_data`, and optional keyboard `modifiers`.
457    pub async fn drag(
458        &self,
459        drag_type: DispatchDragEventType,
460        point: Point,
461        drag_data: DragData,
462        modifiers: Option<i64>,
463    ) -> Result<&Self> {
464        let mut params: DispatchDragEventParams =
465            DispatchDragEventParams::new(drag_type, point.x, point.y, drag_data);
466
467        if let Some(modifiers) = modifiers {
468            params.modifiers = Some(modifiers);
469        }
470
471        self.execute(params).await?;
472        Ok(self)
473    }
474
475    /// Moves the mouse to this point (dispatches a mouseWheel event).
476    /// If you get an error use page.scroll_by instead.
477    pub async fn scroll(&self, point: Point, delta: Delta) -> Result<&Self> {
478        let mut params: DispatchMouseEventParams =
479            DispatchMouseEventParams::new(DispatchMouseEventType::MouseWheel, point.x, point.y);
480
481        params.delta_x = Some(delta.delta_x);
482        params.delta_y = Some(delta.delta_y);
483
484        self.execute(params).await?;
485        Ok(self)
486    }
487
488    /// Performs a mouse click event at the point's location with the amount of clicks and modifier.
489    pub async fn click_with_count_base(
490        &self,
491        point: Point,
492        click_count: impl Into<i64>,
493        modifiers: impl Into<i64>,
494        button: impl Into<MouseButton>,
495    ) -> Result<&Self> {
496        let cmd = DispatchMouseEventParams::builder()
497            .x(point.x)
498            .y(point.y)
499            .button(button)
500            .click_count(click_count)
501            .modifiers(modifiers);
502
503        if let Ok(cmd) = cmd
504            .clone()
505            .r#type(DispatchMouseEventType::MousePressed)
506            .build()
507        {
508            self.move_mouse(point).await?;
509            // Pre-click dwell: real users hold the cursor at the target
510            // for a few tens of ms before pressing. A zero-gap
511            // mousemove→mousedown is a common antibot fingerprint.
512            // Disabled by setting `pre_click_dwell_ms = None` (or
513            // `Some((0, 0))`) on the SmartMouseConfig.
514            if let Some(dwell) = self.smart_mouse.pre_click_dwell() {
515                tokio::time::sleep(dwell).await;
516            }
517            self.send_command(cmd).await?;
518        }
519
520        if let Ok(cmd) = cmd.r#type(DispatchMouseEventType::MouseReleased).build() {
521            self.execute(cmd).await?;
522        }
523
524        self.smart_mouse.set_position(point);
525        Ok(self)
526    }
527
528    /// Move smoothly to `point` with human-like movement, then click.
529    pub async fn click_smooth(&self, point: Point) -> Result<&Self> {
530        self.move_mouse_smooth(point).await?;
531        self.click(point).await
532    }
533
534    /// Performs a mouse click event at the point's location with the amount of clicks and modifier.
535    pub async fn click_with_count(
536        &self,
537        point: Point,
538        click_count: impl Into<i64>,
539        modifiers: impl Into<i64>,
540    ) -> Result<&Self> {
541        self.click_with_count_base(point, click_count, modifiers, MouseButton::Left)
542            .await
543    }
544
545    /// Performs a mouse right click event at the point's location with the amount of clicks and modifier.
546    pub async fn right_click_with_count(
547        &self,
548        point: Point,
549        click_count: impl Into<i64>,
550        modifiers: impl Into<i64>,
551    ) -> Result<&Self> {
552        self.click_with_count_base(point, click_count, modifiers, MouseButton::Right)
553            .await
554    }
555
556    /// Performs a mouse middle click event at the point's location with the amount of clicks and modifier.
557    pub async fn middle_click_with_count(
558        &self,
559        point: Point,
560        click_count: impl Into<i64>,
561        modifiers: impl Into<i64>,
562    ) -> Result<&Self> {
563        self.click_with_count_base(point, click_count, modifiers, MouseButton::Middle)
564            .await
565    }
566
567    /// Performs a mouse back click event at the point's location with the amount of clicks and modifier.
568    pub async fn back_click_with_count(
569        &self,
570        point: Point,
571        click_count: impl Into<i64>,
572        modifiers: impl Into<i64>,
573    ) -> Result<&Self> {
574        self.click_with_count_base(point, click_count, modifiers, MouseButton::Back)
575            .await
576    }
577
578    /// Performs a mouse forward click event at the point's location with the amount of clicks and modifier.
579    pub async fn forward_click_with_count(
580        &self,
581        point: Point,
582        click_count: impl Into<i64>,
583        modifiers: impl Into<i64>,
584    ) -> Result<&Self> {
585        self.click_with_count_base(point, click_count, modifiers, MouseButton::Forward)
586            .await
587    }
588
589    /// Performs a click-and-drag from one point to another with optional modifiers.
590    pub async fn click_and_drag(
591        &self,
592        from: Point,
593        to: Point,
594        modifiers: impl Into<i64>,
595    ) -> Result<&Self> {
596        let modifiers = modifiers.into();
597        let click_count = 1;
598
599        let cmd = DispatchMouseEventParams::builder()
600            .button(MouseButton::Left)
601            .click_count(click_count)
602            .modifiers(modifiers);
603
604        if let Ok(cmd) = cmd
605            .clone()
606            .x(from.x)
607            .y(from.y)
608            .r#type(DispatchMouseEventType::MousePressed)
609            .build()
610        {
611            self.move_mouse(from).await?.send_command(cmd).await?;
612        }
613
614        if let Ok(cmd) = cmd
615            .clone()
616            .x(to.x)
617            .y(to.y)
618            .r#type(DispatchMouseEventType::MouseMoved)
619            .build()
620        {
621            self.move_mouse(to).await?.send_command(cmd).await?;
622        }
623
624        if let Ok(cmd) = cmd
625            .r#type(DispatchMouseEventType::MouseReleased)
626            .x(to.x)
627            .y(to.y)
628            .build()
629        {
630            self.send_command(cmd).await?;
631        }
632
633        self.smart_mouse.set_position(to);
634        Ok(self)
635    }
636
637    /// Performs a smooth click-and-drag: moves to `from` with a bezier path,
638    /// presses, drags along a bezier path to `to`, then releases.
639    pub async fn click_and_drag_smooth(
640        &self,
641        from: Point,
642        to: Point,
643        modifiers: impl Into<i64>,
644    ) -> Result<&Self> {
645        let modifiers = modifiers.into();
646
647        // Smooth move to the starting point
648        self.move_mouse_smooth(from).await?;
649
650        // Press at starting point
651        if let Ok(cmd) = DispatchMouseEventParams::builder()
652            .x(from.x)
653            .y(from.y)
654            .button(MouseButton::Left)
655            .click_count(1)
656            .modifiers(modifiers)
657            .r#type(DispatchMouseEventType::MousePressed)
658            .build()
659        {
660            self.send_command(cmd).await?;
661        }
662
663        // Smooth drag to destination (dispatching MouseMoved with button held).
664        // Absolute-deadline pacing (same shape as `move_mouse_smooth`):
665        // a slow send on step N shortens the wait before step N+1
666        // instead of shifting the whole schedule, so total time tracks
667        // `sum(step.delay)` regardless of per-iteration variance. The
668        // final step's sleep is skipped — the MouseReleased dispatch
669        // follows immediately and any trailing delay would just be
670        // wall-clock waste.
671        let path = self.smart_mouse.path_to(to);
672        let last_idx = path.len().saturating_sub(1);
673        let mut deadline = tokio::time::Instant::now();
674        for (i, step) in path.iter().enumerate() {
675            if let Ok(cmd) = DispatchMouseEventParams::builder()
676                .x(step.point.x)
677                .y(step.point.y)
678                .button(MouseButton::Left)
679                .modifiers(modifiers)
680                .r#type(DispatchMouseEventType::MouseMoved)
681                .build()
682            {
683                self.send_command(cmd).await?;
684            }
685            if i < last_idx {
686                deadline += step.delay;
687                tokio::time::sleep_until(deadline).await;
688            }
689        }
690
691        // Release at destination
692        if let Ok(cmd) = DispatchMouseEventParams::builder()
693            .x(to.x)
694            .y(to.y)
695            .button(MouseButton::Left)
696            .click_count(1)
697            .modifiers(modifiers)
698            .r#type(DispatchMouseEventType::MouseReleased)
699            .build()
700        {
701            self.send_command(cmd).await?;
702        }
703
704        Ok(self)
705    }
706
707    /// Performs a mouse click event at the point's location
708    pub async fn click(&self, point: Point) -> Result<&Self> {
709        self.click_with_count(point, 1, 0).await
710    }
711
712    /// Performs a mouse double click event at the point's location
713    pub async fn double_click(&self, point: Point) -> Result<&Self> {
714        self.click_with_count(point, 2, 0).await
715    }
716
717    /// Performs a mouse right click event at the point's location
718    pub async fn right_click(&self, point: Point) -> Result<&Self> {
719        self.right_click_with_count(point, 1, 0).await
720    }
721
722    /// Performs a mouse middle click event at the point's location
723    pub async fn middle_click(&self, point: Point) -> Result<&Self> {
724        self.middle_click_with_count(point, 1, 0).await
725    }
726
727    /// Performs a mouse back click event at the point's location
728    pub async fn back_click(&self, point: Point) -> Result<&Self> {
729        self.back_click_with_count(point, 1, 0).await
730    }
731
732    /// Performs a mouse forward click event at the point's location
733    pub async fn forward_click(&self, point: Point) -> Result<&Self> {
734        self.forward_click_with_count(point, 1, 0).await
735    }
736
737    /// Performs a mouse click event at the point's location and modifier: Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).
738    pub async fn click_with_modifier(
739        &self,
740        point: Point,
741        modifiers: impl Into<i64>,
742    ) -> Result<&Self> {
743        self.click_with_count(point, 1, modifiers).await
744    }
745
746    /// Performs a mouse right click event at the point's location and modifier: Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).
747    pub async fn right_click_with_modifier(
748        &self,
749        point: Point,
750        modifiers: impl Into<i64>,
751    ) -> Result<&Self> {
752        self.right_click_with_count(point, 1, modifiers).await
753    }
754
755    /// Performs a mouse middle click event at the point's location and modifier: Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).
756    pub async fn middle_click_with_modifier(
757        &self,
758        point: Point,
759        modifiers: impl Into<i64>,
760    ) -> Result<&Self> {
761        self.middle_click_with_count(point, 1, modifiers).await
762    }
763
764    /// Performs a mouse double click event at the point's location and modifier: Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).
765    pub async fn double_click_with_modifier(
766        &self,
767        point: Point,
768        modifiers: impl Into<i64>,
769    ) -> Result<&Self> {
770        self.click_with_count(point, 2, modifiers).await
771    }
772
773    /// This simulates pressing keys on the page.
774    ///
775    /// # Note The `input` is treated as series of `KeyDefinition`s, where each
776    /// char is inserted as a separate keystroke. So sending
777    /// `page.type_str("Enter")` will be processed as a series of single
778    /// keystrokes:  `["E", "n", "t", "e", "r"]`. To simulate pressing the
779    /// actual Enter key instead use `page.press_key(
780    /// keys::get_key_definition("Enter").unwrap())`.
781    pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
782        for c in input.as_ref().split("").filter(|s| !s.is_empty()) {
783            self._press_key(c, None).await?;
784        }
785        Ok(self)
786    }
787
788    /// Fetches the entire accessibility tree for the root Document
789    pub async fn get_full_ax_tree(
790        &self,
791        depth: Option<i64>,
792        frame_id: Option<FrameId>,
793    ) -> Result<GetFullAxTreeReturns> {
794        let mut builder = GetFullAxTreeParamsBuilder::default();
795
796        if let Some(depth) = depth {
797            builder = builder.depth(depth);
798        }
799
800        if let Some(frame_id) = frame_id {
801            builder = builder.frame_id(frame_id);
802        }
803
804        let resp = self.execute(builder.build()).await?;
805
806        Ok(resp.result)
807    }
808
809    /// Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.
810    pub async fn get_partial_ax_tree(
811        &self,
812        node_id: Option<chromiumoxide_cdp::cdp::browser_protocol::dom::NodeId>,
813        backend_node_id: Option<BackendNodeId>,
814        object_id: Option<RemoteObjectId>,
815        fetch_relatives: Option<bool>,
816    ) -> Result<GetPartialAxTreeReturns> {
817        let mut builder = GetPartialAxTreeParamsBuilder::default();
818
819        if let Some(node_id) = node_id {
820            builder = builder.node_id(node_id);
821        }
822
823        if let Some(backend_node_id) = backend_node_id {
824            builder = builder.backend_node_id(backend_node_id);
825        }
826
827        if let Some(object_id) = object_id {
828            builder = builder.object_id(object_id);
829        }
830
831        if let Some(fetch_relatives) = fetch_relatives {
832            builder = builder.fetch_relatives(fetch_relatives);
833        }
834
835        let resp = self.execute(builder.build()).await?;
836
837        Ok(resp.result)
838    }
839
840    /// This simulates pressing keys on the page.
841    ///
842    /// # Note The `input` is treated as series of `KeyDefinition`s, where each
843    /// char is inserted as a separate keystroke. So sending
844    /// `page.type_str("Enter")` will be processed as a series of single
845    /// keystrokes:  `["E", "n", "t", "e", "r"]`. To simulate pressing the
846    /// actual Enter key instead use `page.press_key(
847    /// keys::get_key_definition("Enter").unwrap())`.
848    pub async fn type_str_with_modifier(
849        &self,
850        input: impl AsRef<str>,
851        modifiers: Option<i64>,
852    ) -> Result<&Self> {
853        for c in input.as_ref().split("").filter(|s| !s.is_empty()) {
854            self._press_key(c, modifiers).await?;
855        }
856        Ok(self)
857    }
858
859    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
860    /// keys.
861    async fn _press_key(&self, key: impl AsRef<str>, modifiers: Option<i64>) -> Result<&Self> {
862        let key = key.as_ref();
863        let key_definition = keys::get_key_definition(key)
864            .ok_or_else(|| CdpError::msg(format!("Key not found: {key}")))?;
865        let mut cmd = DispatchKeyEventParams::builder();
866
867        // See https://github.com/GoogleChrome/puppeteer/blob/62da2366c65b335751896afbb0206f23c61436f1/lib/Input.js#L114-L115
868        // And https://github.com/GoogleChrome/puppeteer/blob/62da2366c65b335751896afbb0206f23c61436f1/lib/Input.js#L52
869        let key_down_event_type = if let Some(txt) = key_definition.text {
870            cmd = cmd.text(txt);
871            DispatchKeyEventType::KeyDown
872        } else if key_definition.key.len() == 1 {
873            cmd = cmd.text(key_definition.key);
874            DispatchKeyEventType::KeyDown
875        } else {
876            DispatchKeyEventType::RawKeyDown
877        };
878
879        cmd = cmd
880            .r#type(DispatchKeyEventType::KeyDown)
881            .key(key_definition.key)
882            .code(key_definition.code)
883            .windows_virtual_key_code(key_definition.key_code)
884            .native_virtual_key_code(key_definition.key_code);
885
886        if let Some(modifiers) = modifiers {
887            cmd = cmd.modifiers(modifiers);
888        }
889
890        let key_down = cmd.clone().r#type(key_down_event_type).build().ok();
891        let key_up = cmd.r#type(DispatchKeyEventType::KeyUp).build().ok();
892
893        match (key_down, key_up) {
894            (Some(kd), Some(ku)) => {
895                tokio::try_join!(self.execute(kd), self.execute(ku))?;
896            }
897            (Some(kd), None) => {
898                self.execute(kd).await?;
899            }
900            (None, Some(ku)) => {
901                self.execute(ku).await?;
902            }
903            (None, None) => {}
904        }
905
906        Ok(self)
907    }
908
909    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
910    /// keys.
911    pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
912        self._press_key(key, None).await
913    }
914
915    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
916    /// keys and modifiers.
917    pub async fn press_key_with_modifier(
918        &self,
919        key: impl AsRef<str>,
920        modifiers: Option<i64>,
921    ) -> Result<&Self> {
922        self._press_key(key, modifiers).await
923    }
924
925    /// Calls function with given declaration on the remote object with the
926    /// matching id
927    pub async fn call_js_fn(
928        &self,
929        function_declaration: impl Into<String>,
930        await_promise: bool,
931        remote_object_id: RemoteObjectId,
932    ) -> Result<CallFunctionOnReturns> {
933        if let Ok(resp) = CallFunctionOnParams::builder()
934            .object_id(remote_object_id)
935            .function_declaration(function_declaration)
936            .generate_preview(true)
937            .await_promise(await_promise)
938            .build()
939        {
940            let resp = self.execute(resp).await?;
941            Ok(resp.result)
942        } else {
943            Err(CdpError::NotFound)
944        }
945    }
946
947    pub async fn evaluate_expression(
948        &self,
949        evaluate: impl Into<EvaluateParams>,
950    ) -> Result<EvaluationResult> {
951        let mut evaluate = evaluate.into();
952        if evaluate.context_id.is_none() {
953            evaluate.context_id = self.execution_context().await?;
954        }
955        if evaluate.await_promise.is_none() {
956            evaluate.await_promise = Some(true);
957        }
958        if evaluate.return_by_value.is_none() {
959            evaluate.return_by_value = Some(true);
960        }
961
962        // evaluate.silent = Some(true);
963
964        let resp = self.execute(evaluate).await?.result;
965
966        if let Some(exception) = resp.exception_details {
967            return Err(CdpError::JavascriptException(Box::new(exception)));
968        }
969
970        Ok(EvaluationResult::new(resp.result))
971    }
972
973    pub async fn evaluate_function(
974        &self,
975        evaluate: impl Into<CallFunctionOnParams>,
976    ) -> Result<EvaluationResult> {
977        let mut evaluate = evaluate.into();
978        if evaluate.execution_context_id.is_none() {
979            evaluate.execution_context_id = self.execution_context().await?;
980        }
981        if evaluate.await_promise.is_none() {
982            evaluate.await_promise = Some(true);
983        }
984        if evaluate.return_by_value.is_none() {
985            evaluate.return_by_value = Some(true);
986        }
987
988        // evaluate.silent = Some(true);
989
990        let resp = self.execute(evaluate).await?.result;
991        if let Some(exception) = resp.exception_details {
992            return Err(CdpError::JavascriptException(Box::new(exception)));
993        }
994        Ok(EvaluationResult::new(resp.result))
995    }
996
997    pub async fn execution_context(&self) -> Result<Option<ExecutionContextId>> {
998        self.execution_context_for_world(None, DOMWorldKind::Main)
999            .await
1000    }
1001
1002    pub async fn secondary_execution_context(&self) -> Result<Option<ExecutionContextId>> {
1003        self.execution_context_for_world(None, DOMWorldKind::Secondary)
1004            .await
1005    }
1006
1007    pub async fn frame_execution_context(
1008        &self,
1009        frame_id: FrameId,
1010    ) -> Result<Option<ExecutionContextId>> {
1011        self.execution_context_for_world(Some(frame_id), DOMWorldKind::Main)
1012            .await
1013    }
1014
1015    pub async fn frame_secondary_execution_context(
1016        &self,
1017        frame_id: FrameId,
1018    ) -> Result<Option<ExecutionContextId>> {
1019        self.execution_context_for_world(Some(frame_id), DOMWorldKind::Secondary)
1020            .await
1021    }
1022
1023    pub async fn execution_context_for_world(
1024        &self,
1025        frame_id: Option<FrameId>,
1026        dom_world: DOMWorldKind,
1027    ) -> Result<Option<ExecutionContextId>> {
1028        let (tx, rx) = oneshot_channel();
1029        let msg = TargetMessage::GetExecutionContext(GetExecutionContext {
1030            dom_world,
1031            frame_id,
1032            tx,
1033        });
1034        match self.sender.try_send(msg) {
1035            Ok(()) => {}
1036            Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
1037                tokio::time::timeout(self.request_timeout, self.sender.send(msg))
1038                    .await
1039                    .map_err(|_| CdpError::Timeout)?
1040                    .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
1041            }
1042            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
1043                return Err(CdpError::ChannelSendError(crate::error::ChannelError::Send));
1044            }
1045        }
1046        Ok(tokio::time::timeout(self.request_timeout, rx)
1047            .await
1048            .map_err(|_| CdpError::Timeout)??)
1049    }
1050
1051    /// Returns metrics relating to the layout of the page
1052    pub async fn layout_metrics(&self) -> Result<GetLayoutMetricsReturns> {
1053        Ok(self
1054            .execute(GetLayoutMetricsParams::default())
1055            .await?
1056            .result)
1057    }
1058
1059    /// Enable page Content Security Policy by-passing.
1060    pub async fn set_bypass_csp(&self, enabled: bool) -> Result<&Self> {
1061        self.execute(SetBypassCspParams::new(enabled)).await?;
1062        Ok(self)
1063    }
1064
1065    /// Take a screenshot of the page.
1066    pub async fn screenshot(&self, params: impl Into<ScreenshotParams>) -> Result<Vec<u8>> {
1067        self.activate().await?;
1068        let params = params.into();
1069        let full_page = params.full_page();
1070        let omit_background = params.omit_background();
1071
1072        let mut cdp_params = params.cdp_params;
1073
1074        if full_page {
1075            let metrics = self.layout_metrics().await?;
1076            let width = metrics.css_content_size.width;
1077            let height = metrics.css_content_size.height;
1078
1079            cdp_params.clip = Some(Viewport {
1080                x: 0.,
1081                y: 0.,
1082                width,
1083                height,
1084                scale: 1.,
1085            });
1086
1087            self.execute(SetDeviceMetricsOverrideParams::new(
1088                width as i64,
1089                height as i64,
1090                1.,
1091                false,
1092            ))
1093            .await?;
1094        }
1095
1096        if omit_background {
1097            self.execute(SetDefaultBackgroundColorOverrideParams {
1098                color: Some(Rgba {
1099                    r: 0,
1100                    g: 0,
1101                    b: 0,
1102                    a: Some(0.),
1103                }),
1104            })
1105            .await?;
1106        }
1107
1108        let res = self.execute(cdp_params).await?.result;
1109
1110        if omit_background {
1111            self.send_command(SetDefaultBackgroundColorOverrideParams { color: None })
1112                .await?;
1113        }
1114
1115        if full_page {
1116            self.send_command(ClearDeviceMetricsOverrideParams {})
1117                .await?;
1118        }
1119
1120        Ok(utils::base64::decode(&res.data)?)
1121    }
1122
1123    /// Convert the page to PDF.
1124    pub async fn print_to_pdf(&self, params: impl Into<PrintToPdfParams>) -> Result<Vec<u8>> {
1125        self.activate().await?;
1126        let params = params.into();
1127
1128        let res = self.execute(params).await?.result;
1129
1130        Ok(utils::base64::decode(&res.data)?)
1131    }
1132}
1133
1134pub(crate) async fn execute<T: Command>(
1135    cmd: T,
1136    sender: PageSender,
1137    session: Option<SessionId>,
1138    request_timeout: std::time::Duration,
1139) -> Result<CommandResponse<T::Response>> {
1140    let method = cmd.identifier();
1141    let rx = send_command(cmd, sender, session, request_timeout).await?;
1142    let resp = tokio::time::timeout(request_timeout, rx)
1143        .await
1144        .map_err(|_| CdpError::Timeout)???;
1145    to_command_response::<T>(resp, method)
1146}
1147
1148/// Execute a command without waiting.
1149///
1150/// Uses a `try_send` fast path to avoid async overhead when the channel has
1151/// capacity (common case). Falls back to an async send with timeout when full.
1152pub(crate) async fn send_command<T: Command>(
1153    cmd: T,
1154    sender: PageSender,
1155    session: Option<SessionId>,
1156    request_timeout: std::time::Duration,
1157) -> Result<tokio::sync::oneshot::Receiver<Result<chromiumoxide_types::Response, CdpError>>> {
1158    let (tx, rx) = oneshot_channel();
1159    let msg = CommandMessage::with_session(cmd, tx, session)?;
1160    let target_msg = TargetMessage::Command(msg);
1161    match sender.try_send(target_msg) {
1162        Ok(()) => {}
1163        Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
1164            tokio::time::timeout(request_timeout, sender.send(msg))
1165                .await
1166                .map_err(|_| CdpError::Timeout)?
1167                .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
1168        }
1169        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
1170            return Err(CdpError::ChannelSendError(crate::error::ChannelError::Send));
1171        }
1172    }
1173    Ok(rx)
1174}
1175
1176#[cfg(test)]
1177mod page_channel_capacity_tests {
1178    //! Unit tests for `PageHandle::with_capacity` and the capacity plumbing.
1179    //!
1180    //! These verify the three guarantees that keep the change
1181    //! backwards-compatible and panic-free:
1182    //!
1183    //! 1. `PageHandle::new` (legacy signature) still allocates the historic
1184    //!    2048-slot channel — proven by filling the channel to exactly
1185    //!    `DEFAULT_PAGE_CHANNEL_CAPACITY` via `try_send` and then expecting
1186    //!    the next `try_send` to return `Full`. This pins the default.
1187    //!
1188    //! 2. `PageHandle::with_capacity(N)` allocates an N-slot channel for
1189    //!    any N, verified the same way at a small N so the test runs fast.
1190    //!
1191    //! 3. `PageHandle::with_capacity(0)` does NOT panic — we clamp to 1
1192    //!    internally because `tokio::sync::mpsc::channel(0)` aborts. The
1193    //!    resulting channel has exactly one slot.
1194    //!
1195    //! No browser / runtime is spun up — these tests are purely in-process
1196    //! on the mpsc primitives.
1197    use super::{PageHandle, DEFAULT_PAGE_CHANNEL_CAPACITY};
1198    use crate::handler::target::TargetMessage;
1199    use chromiumoxide_cdp::cdp::browser_protocol::target::{SessionId, TargetId};
1200    use std::time::Duration;
1201    use tokio::sync::mpsc::error::TrySendError;
1202
1203    /// Trivial zero-cost `TargetMessage` for filling the channel.
1204    /// We're only exercising the bounded-mpsc slot count — the payload
1205    /// shape is irrelevant, so use the smallest variant.
1206    fn make_msg() -> TargetMessage {
1207        TargetMessage::BlockNetwork(false)
1208    }
1209
1210    fn make_handle(capacity: usize) -> PageHandle {
1211        PageHandle::with_capacity(
1212            TargetId::from("t".to_string()),
1213            SessionId::from("s".to_string()),
1214            None,
1215            Duration::from_secs(30),
1216            None,
1217            capacity,
1218        )
1219    }
1220
1221    /// Fill a page's channel to capacity via `try_send` and return the
1222    /// observed slot count — the number of sends that succeeded before
1223    /// the first `Full` error.
1224    fn observed_capacity(handle: &PageHandle, upper_bound: usize) -> usize {
1225        // `PageSender` is the page's send half; `try_send` surfaces the
1226        // underlying mpsc `TrySendError::Full` once the bounded buffer
1227        // is saturated without consuming a slot, which is what we count.
1228        let sender = &handle.page.sender;
1229        let mut sent = 0;
1230        for _ in 0..upper_bound {
1231            match sender.try_send(make_msg()) {
1232                Ok(()) => sent += 1,
1233                Err(TrySendError::Full(_)) => return sent,
1234                Err(TrySendError::Closed(_)) => {
1235                    panic!("channel unexpectedly closed at {sent} sends")
1236                }
1237            }
1238        }
1239        sent
1240    }
1241
1242    #[test]
1243    fn new_delegates_to_default_capacity() {
1244        let handle = PageHandle::new(
1245            TargetId::from("t".to_string()),
1246            SessionId::from("s".to_string()),
1247            None,
1248            Duration::from_secs(30),
1249            None,
1250        );
1251        let n = observed_capacity(&handle, DEFAULT_PAGE_CHANNEL_CAPACITY + 16);
1252        assert_eq!(
1253            n, DEFAULT_PAGE_CHANNEL_CAPACITY,
1254            "legacy PageHandle::new must preserve the 2048-slot default"
1255        );
1256    }
1257
1258    #[test]
1259    fn with_capacity_respects_arbitrary_value() {
1260        // Small N keeps the test fast; 4 is enough to distinguish from
1261        // the 2048 default.
1262        for n in [1_usize, 4, 16, 64] {
1263            let handle = make_handle(n);
1264            assert_eq!(
1265                observed_capacity(&handle, n + 16),
1266                n,
1267                "with_capacity({n}) should produce exactly {n} slots",
1268            );
1269        }
1270    }
1271
1272    #[test]
1273    fn zero_capacity_is_clamped_to_one_and_does_not_panic() {
1274        // `tokio::sync::mpsc::channel(0)` panics — our `.max(1)` clamp
1275        // must turn that into a single-slot channel rather than an abort.
1276        let handle = make_handle(0);
1277        assert_eq!(
1278            observed_capacity(&handle, 4),
1279            1,
1280            "zero capacity must clamp to 1 and not panic"
1281        );
1282    }
1283}