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?.send_command(cmd).await?;
509        }
510
511        if let Ok(cmd) = cmd.r#type(DispatchMouseEventType::MouseReleased).build() {
512            self.execute(cmd).await?;
513        }
514
515        self.smart_mouse.set_position(point);
516        Ok(self)
517    }
518
519    /// Move smoothly to `point` with human-like movement, then click.
520    pub async fn click_smooth(&self, point: Point) -> Result<&Self> {
521        self.move_mouse_smooth(point).await?;
522        self.click(point).await
523    }
524
525    /// Performs a mouse click event at the point's location with the amount of clicks and modifier.
526    pub async fn click_with_count(
527        &self,
528        point: Point,
529        click_count: impl Into<i64>,
530        modifiers: impl Into<i64>,
531    ) -> Result<&Self> {
532        self.click_with_count_base(point, click_count, modifiers, MouseButton::Left)
533            .await
534    }
535
536    /// Performs a mouse right click event at the point's location with the amount of clicks and modifier.
537    pub async fn right_click_with_count(
538        &self,
539        point: Point,
540        click_count: impl Into<i64>,
541        modifiers: impl Into<i64>,
542    ) -> Result<&Self> {
543        self.click_with_count_base(point, click_count, modifiers, MouseButton::Right)
544            .await
545    }
546
547    /// Performs a mouse middle click event at the point's location with the amount of clicks and modifier.
548    pub async fn middle_click_with_count(
549        &self,
550        point: Point,
551        click_count: impl Into<i64>,
552        modifiers: impl Into<i64>,
553    ) -> Result<&Self> {
554        self.click_with_count_base(point, click_count, modifiers, MouseButton::Middle)
555            .await
556    }
557
558    /// Performs a mouse back click event at the point's location with the amount of clicks and modifier.
559    pub async fn back_click_with_count(
560        &self,
561        point: Point,
562        click_count: impl Into<i64>,
563        modifiers: impl Into<i64>,
564    ) -> Result<&Self> {
565        self.click_with_count_base(point, click_count, modifiers, MouseButton::Back)
566            .await
567    }
568
569    /// Performs a mouse forward click event at the point's location with the amount of clicks and modifier.
570    pub async fn forward_click_with_count(
571        &self,
572        point: Point,
573        click_count: impl Into<i64>,
574        modifiers: impl Into<i64>,
575    ) -> Result<&Self> {
576        self.click_with_count_base(point, click_count, modifiers, MouseButton::Forward)
577            .await
578    }
579
580    /// Performs a click-and-drag from one point to another with optional modifiers.
581    pub async fn click_and_drag(
582        &self,
583        from: Point,
584        to: Point,
585        modifiers: impl Into<i64>,
586    ) -> Result<&Self> {
587        let modifiers = modifiers.into();
588        let click_count = 1;
589
590        let cmd = DispatchMouseEventParams::builder()
591            .button(MouseButton::Left)
592            .click_count(click_count)
593            .modifiers(modifiers);
594
595        if let Ok(cmd) = cmd
596            .clone()
597            .x(from.x)
598            .y(from.y)
599            .r#type(DispatchMouseEventType::MousePressed)
600            .build()
601        {
602            self.move_mouse(from).await?.send_command(cmd).await?;
603        }
604
605        if let Ok(cmd) = cmd
606            .clone()
607            .x(to.x)
608            .y(to.y)
609            .r#type(DispatchMouseEventType::MouseMoved)
610            .build()
611        {
612            self.move_mouse(to).await?.send_command(cmd).await?;
613        }
614
615        if let Ok(cmd) = cmd
616            .r#type(DispatchMouseEventType::MouseReleased)
617            .x(to.x)
618            .y(to.y)
619            .build()
620        {
621            self.send_command(cmd).await?;
622        }
623
624        self.smart_mouse.set_position(to);
625        Ok(self)
626    }
627
628    /// Performs a smooth click-and-drag: moves to `from` with a bezier path,
629    /// presses, drags along a bezier path to `to`, then releases.
630    pub async fn click_and_drag_smooth(
631        &self,
632        from: Point,
633        to: Point,
634        modifiers: impl Into<i64>,
635    ) -> Result<&Self> {
636        let modifiers = modifiers.into();
637
638        // Smooth move to the starting point
639        self.move_mouse_smooth(from).await?;
640
641        // Press at starting point
642        if let Ok(cmd) = DispatchMouseEventParams::builder()
643            .x(from.x)
644            .y(from.y)
645            .button(MouseButton::Left)
646            .click_count(1)
647            .modifiers(modifiers)
648            .r#type(DispatchMouseEventType::MousePressed)
649            .build()
650        {
651            self.send_command(cmd).await?;
652        }
653
654        // Smooth drag to destination (dispatching MouseMoved with button held).
655        // Absolute-deadline pacing (same shape as `move_mouse_smooth`):
656        // a slow send on step N shortens the wait before step N+1
657        // instead of shifting the whole schedule, so total time tracks
658        // `sum(step.delay)` regardless of per-iteration variance. The
659        // final step's sleep is skipped — the MouseReleased dispatch
660        // follows immediately and any trailing delay would just be
661        // wall-clock waste.
662        let path = self.smart_mouse.path_to(to);
663        let last_idx = path.len().saturating_sub(1);
664        let mut deadline = tokio::time::Instant::now();
665        for (i, step) in path.iter().enumerate() {
666            if let Ok(cmd) = DispatchMouseEventParams::builder()
667                .x(step.point.x)
668                .y(step.point.y)
669                .button(MouseButton::Left)
670                .modifiers(modifiers)
671                .r#type(DispatchMouseEventType::MouseMoved)
672                .build()
673            {
674                self.send_command(cmd).await?;
675            }
676            if i < last_idx {
677                deadline += step.delay;
678                tokio::time::sleep_until(deadline).await;
679            }
680        }
681
682        // Release at destination
683        if let Ok(cmd) = DispatchMouseEventParams::builder()
684            .x(to.x)
685            .y(to.y)
686            .button(MouseButton::Left)
687            .click_count(1)
688            .modifiers(modifiers)
689            .r#type(DispatchMouseEventType::MouseReleased)
690            .build()
691        {
692            self.send_command(cmd).await?;
693        }
694
695        Ok(self)
696    }
697
698    /// Performs a mouse click event at the point's location
699    pub async fn click(&self, point: Point) -> Result<&Self> {
700        self.click_with_count(point, 1, 0).await
701    }
702
703    /// Performs a mouse double click event at the point's location
704    pub async fn double_click(&self, point: Point) -> Result<&Self> {
705        self.click_with_count(point, 2, 0).await
706    }
707
708    /// Performs a mouse right click event at the point's location
709    pub async fn right_click(&self, point: Point) -> Result<&Self> {
710        self.right_click_with_count(point, 1, 0).await
711    }
712
713    /// Performs a mouse middle click event at the point's location
714    pub async fn middle_click(&self, point: Point) -> Result<&Self> {
715        self.middle_click_with_count(point, 1, 0).await
716    }
717
718    /// Performs a mouse back click event at the point's location
719    pub async fn back_click(&self, point: Point) -> Result<&Self> {
720        self.back_click_with_count(point, 1, 0).await
721    }
722
723    /// Performs a mouse forward click event at the point's location
724    pub async fn forward_click(&self, point: Point) -> Result<&Self> {
725        self.forward_click_with_count(point, 1, 0).await
726    }
727
728    /// Performs a mouse click event at the point's location and modifier: Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).
729    pub async fn click_with_modifier(
730        &self,
731        point: Point,
732        modifiers: impl Into<i64>,
733    ) -> Result<&Self> {
734        self.click_with_count(point, 1, modifiers).await
735    }
736
737    /// 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).
738    pub async fn right_click_with_modifier(
739        &self,
740        point: Point,
741        modifiers: impl Into<i64>,
742    ) -> Result<&Self> {
743        self.right_click_with_count(point, 1, modifiers).await
744    }
745
746    /// 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).
747    pub async fn middle_click_with_modifier(
748        &self,
749        point: Point,
750        modifiers: impl Into<i64>,
751    ) -> Result<&Self> {
752        self.middle_click_with_count(point, 1, modifiers).await
753    }
754
755    /// 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).
756    pub async fn double_click_with_modifier(
757        &self,
758        point: Point,
759        modifiers: impl Into<i64>,
760    ) -> Result<&Self> {
761        self.click_with_count(point, 2, modifiers).await
762    }
763
764    /// This simulates pressing keys on the page.
765    ///
766    /// # Note The `input` is treated as series of `KeyDefinition`s, where each
767    /// char is inserted as a separate keystroke. So sending
768    /// `page.type_str("Enter")` will be processed as a series of single
769    /// keystrokes:  `["E", "n", "t", "e", "r"]`. To simulate pressing the
770    /// actual Enter key instead use `page.press_key(
771    /// keys::get_key_definition("Enter").unwrap())`.
772    pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
773        for c in input.as_ref().split("").filter(|s| !s.is_empty()) {
774            self._press_key(c, None).await?;
775        }
776        Ok(self)
777    }
778
779    /// Fetches the entire accessibility tree for the root Document
780    pub async fn get_full_ax_tree(
781        &self,
782        depth: Option<i64>,
783        frame_id: Option<FrameId>,
784    ) -> Result<GetFullAxTreeReturns> {
785        let mut builder = GetFullAxTreeParamsBuilder::default();
786
787        if let Some(depth) = depth {
788            builder = builder.depth(depth);
789        }
790
791        if let Some(frame_id) = frame_id {
792            builder = builder.frame_id(frame_id);
793        }
794
795        let resp = self.execute(builder.build()).await?;
796
797        Ok(resp.result)
798    }
799
800    /// Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.
801    pub async fn get_partial_ax_tree(
802        &self,
803        node_id: Option<chromiumoxide_cdp::cdp::browser_protocol::dom::NodeId>,
804        backend_node_id: Option<BackendNodeId>,
805        object_id: Option<RemoteObjectId>,
806        fetch_relatives: Option<bool>,
807    ) -> Result<GetPartialAxTreeReturns> {
808        let mut builder = GetPartialAxTreeParamsBuilder::default();
809
810        if let Some(node_id) = node_id {
811            builder = builder.node_id(node_id);
812        }
813
814        if let Some(backend_node_id) = backend_node_id {
815            builder = builder.backend_node_id(backend_node_id);
816        }
817
818        if let Some(object_id) = object_id {
819            builder = builder.object_id(object_id);
820        }
821
822        if let Some(fetch_relatives) = fetch_relatives {
823            builder = builder.fetch_relatives(fetch_relatives);
824        }
825
826        let resp = self.execute(builder.build()).await?;
827
828        Ok(resp.result)
829    }
830
831    /// This simulates pressing keys on the page.
832    ///
833    /// # Note The `input` is treated as series of `KeyDefinition`s, where each
834    /// char is inserted as a separate keystroke. So sending
835    /// `page.type_str("Enter")` will be processed as a series of single
836    /// keystrokes:  `["E", "n", "t", "e", "r"]`. To simulate pressing the
837    /// actual Enter key instead use `page.press_key(
838    /// keys::get_key_definition("Enter").unwrap())`.
839    pub async fn type_str_with_modifier(
840        &self,
841        input: impl AsRef<str>,
842        modifiers: Option<i64>,
843    ) -> Result<&Self> {
844        for c in input.as_ref().split("").filter(|s| !s.is_empty()) {
845            self._press_key(c, modifiers).await?;
846        }
847        Ok(self)
848    }
849
850    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
851    /// keys.
852    async fn _press_key(&self, key: impl AsRef<str>, modifiers: Option<i64>) -> Result<&Self> {
853        let key = key.as_ref();
854        let key_definition = keys::get_key_definition(key)
855            .ok_or_else(|| CdpError::msg(format!("Key not found: {key}")))?;
856        let mut cmd = DispatchKeyEventParams::builder();
857
858        // See https://github.com/GoogleChrome/puppeteer/blob/62da2366c65b335751896afbb0206f23c61436f1/lib/Input.js#L114-L115
859        // And https://github.com/GoogleChrome/puppeteer/blob/62da2366c65b335751896afbb0206f23c61436f1/lib/Input.js#L52
860        let key_down_event_type = if let Some(txt) = key_definition.text {
861            cmd = cmd.text(txt);
862            DispatchKeyEventType::KeyDown
863        } else if key_definition.key.len() == 1 {
864            cmd = cmd.text(key_definition.key);
865            DispatchKeyEventType::KeyDown
866        } else {
867            DispatchKeyEventType::RawKeyDown
868        };
869
870        cmd = cmd
871            .r#type(DispatchKeyEventType::KeyDown)
872            .key(key_definition.key)
873            .code(key_definition.code)
874            .windows_virtual_key_code(key_definition.key_code)
875            .native_virtual_key_code(key_definition.key_code);
876
877        if let Some(modifiers) = modifiers {
878            cmd = cmd.modifiers(modifiers);
879        }
880
881        let key_down = cmd.clone().r#type(key_down_event_type).build().ok();
882        let key_up = cmd.r#type(DispatchKeyEventType::KeyUp).build().ok();
883
884        match (key_down, key_up) {
885            (Some(kd), Some(ku)) => {
886                tokio::try_join!(self.execute(kd), self.execute(ku))?;
887            }
888            (Some(kd), None) => {
889                self.execute(kd).await?;
890            }
891            (None, Some(ku)) => {
892                self.execute(ku).await?;
893            }
894            (None, None) => {}
895        }
896
897        Ok(self)
898    }
899
900    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
901    /// keys.
902    pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
903        self._press_key(key, None).await
904    }
905
906    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
907    /// keys and modifiers.
908    pub async fn press_key_with_modifier(
909        &self,
910        key: impl AsRef<str>,
911        modifiers: Option<i64>,
912    ) -> Result<&Self> {
913        self._press_key(key, modifiers).await
914    }
915
916    /// Calls function with given declaration on the remote object with the
917    /// matching id
918    pub async fn call_js_fn(
919        &self,
920        function_declaration: impl Into<String>,
921        await_promise: bool,
922        remote_object_id: RemoteObjectId,
923    ) -> Result<CallFunctionOnReturns> {
924        if let Ok(resp) = CallFunctionOnParams::builder()
925            .object_id(remote_object_id)
926            .function_declaration(function_declaration)
927            .generate_preview(true)
928            .await_promise(await_promise)
929            .build()
930        {
931            let resp = self.execute(resp).await?;
932            Ok(resp.result)
933        } else {
934            Err(CdpError::NotFound)
935        }
936    }
937
938    pub async fn evaluate_expression(
939        &self,
940        evaluate: impl Into<EvaluateParams>,
941    ) -> Result<EvaluationResult> {
942        let mut evaluate = evaluate.into();
943        if evaluate.context_id.is_none() {
944            evaluate.context_id = self.execution_context().await?;
945        }
946        if evaluate.await_promise.is_none() {
947            evaluate.await_promise = Some(true);
948        }
949        if evaluate.return_by_value.is_none() {
950            evaluate.return_by_value = Some(true);
951        }
952
953        // evaluate.silent = Some(true);
954
955        let resp = self.execute(evaluate).await?.result;
956
957        if let Some(exception) = resp.exception_details {
958            return Err(CdpError::JavascriptException(Box::new(exception)));
959        }
960
961        Ok(EvaluationResult::new(resp.result))
962    }
963
964    pub async fn evaluate_function(
965        &self,
966        evaluate: impl Into<CallFunctionOnParams>,
967    ) -> Result<EvaluationResult> {
968        let mut evaluate = evaluate.into();
969        if evaluate.execution_context_id.is_none() {
970            evaluate.execution_context_id = self.execution_context().await?;
971        }
972        if evaluate.await_promise.is_none() {
973            evaluate.await_promise = Some(true);
974        }
975        if evaluate.return_by_value.is_none() {
976            evaluate.return_by_value = Some(true);
977        }
978
979        // evaluate.silent = Some(true);
980
981        let resp = self.execute(evaluate).await?.result;
982        if let Some(exception) = resp.exception_details {
983            return Err(CdpError::JavascriptException(Box::new(exception)));
984        }
985        Ok(EvaluationResult::new(resp.result))
986    }
987
988    pub async fn execution_context(&self) -> Result<Option<ExecutionContextId>> {
989        self.execution_context_for_world(None, DOMWorldKind::Main)
990            .await
991    }
992
993    pub async fn secondary_execution_context(&self) -> Result<Option<ExecutionContextId>> {
994        self.execution_context_for_world(None, DOMWorldKind::Secondary)
995            .await
996    }
997
998    pub async fn frame_execution_context(
999        &self,
1000        frame_id: FrameId,
1001    ) -> Result<Option<ExecutionContextId>> {
1002        self.execution_context_for_world(Some(frame_id), DOMWorldKind::Main)
1003            .await
1004    }
1005
1006    pub async fn frame_secondary_execution_context(
1007        &self,
1008        frame_id: FrameId,
1009    ) -> Result<Option<ExecutionContextId>> {
1010        self.execution_context_for_world(Some(frame_id), DOMWorldKind::Secondary)
1011            .await
1012    }
1013
1014    pub async fn execution_context_for_world(
1015        &self,
1016        frame_id: Option<FrameId>,
1017        dom_world: DOMWorldKind,
1018    ) -> Result<Option<ExecutionContextId>> {
1019        let (tx, rx) = oneshot_channel();
1020        let msg = TargetMessage::GetExecutionContext(GetExecutionContext {
1021            dom_world,
1022            frame_id,
1023            tx,
1024        });
1025        match self.sender.try_send(msg) {
1026            Ok(()) => {}
1027            Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
1028                tokio::time::timeout(self.request_timeout, self.sender.send(msg))
1029                    .await
1030                    .map_err(|_| CdpError::Timeout)?
1031                    .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
1032            }
1033            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
1034                return Err(CdpError::ChannelSendError(crate::error::ChannelError::Send));
1035            }
1036        }
1037        Ok(tokio::time::timeout(self.request_timeout, rx)
1038            .await
1039            .map_err(|_| CdpError::Timeout)??)
1040    }
1041
1042    /// Returns metrics relating to the layout of the page
1043    pub async fn layout_metrics(&self) -> Result<GetLayoutMetricsReturns> {
1044        Ok(self
1045            .execute(GetLayoutMetricsParams::default())
1046            .await?
1047            .result)
1048    }
1049
1050    /// Enable page Content Security Policy by-passing.
1051    pub async fn set_bypass_csp(&self, enabled: bool) -> Result<&Self> {
1052        self.execute(SetBypassCspParams::new(enabled)).await?;
1053        Ok(self)
1054    }
1055
1056    /// Take a screenshot of the page.
1057    pub async fn screenshot(&self, params: impl Into<ScreenshotParams>) -> Result<Vec<u8>> {
1058        self.activate().await?;
1059        let params = params.into();
1060        let full_page = params.full_page();
1061        let omit_background = params.omit_background();
1062
1063        let mut cdp_params = params.cdp_params;
1064
1065        if full_page {
1066            let metrics = self.layout_metrics().await?;
1067            let width = metrics.css_content_size.width;
1068            let height = metrics.css_content_size.height;
1069
1070            cdp_params.clip = Some(Viewport {
1071                x: 0.,
1072                y: 0.,
1073                width,
1074                height,
1075                scale: 1.,
1076            });
1077
1078            self.execute(SetDeviceMetricsOverrideParams::new(
1079                width as i64,
1080                height as i64,
1081                1.,
1082                false,
1083            ))
1084            .await?;
1085        }
1086
1087        if omit_background {
1088            self.execute(SetDefaultBackgroundColorOverrideParams {
1089                color: Some(Rgba {
1090                    r: 0,
1091                    g: 0,
1092                    b: 0,
1093                    a: Some(0.),
1094                }),
1095            })
1096            .await?;
1097        }
1098
1099        let res = self.execute(cdp_params).await?.result;
1100
1101        if omit_background {
1102            self.send_command(SetDefaultBackgroundColorOverrideParams { color: None })
1103                .await?;
1104        }
1105
1106        if full_page {
1107            self.send_command(ClearDeviceMetricsOverrideParams {})
1108                .await?;
1109        }
1110
1111        Ok(utils::base64::decode(&res.data)?)
1112    }
1113
1114    /// Convert the page to PDF.
1115    pub async fn print_to_pdf(&self, params: impl Into<PrintToPdfParams>) -> Result<Vec<u8>> {
1116        self.activate().await?;
1117        let params = params.into();
1118
1119        let res = self.execute(params).await?.result;
1120
1121        Ok(utils::base64::decode(&res.data)?)
1122    }
1123}
1124
1125pub(crate) async fn execute<T: Command>(
1126    cmd: T,
1127    sender: PageSender,
1128    session: Option<SessionId>,
1129    request_timeout: std::time::Duration,
1130) -> Result<CommandResponse<T::Response>> {
1131    let method = cmd.identifier();
1132    let rx = send_command(cmd, sender, session, request_timeout).await?;
1133    let resp = tokio::time::timeout(request_timeout, rx)
1134        .await
1135        .map_err(|_| CdpError::Timeout)???;
1136    to_command_response::<T>(resp, method)
1137}
1138
1139/// Execute a command without waiting.
1140///
1141/// Uses a `try_send` fast path to avoid async overhead when the channel has
1142/// capacity (common case). Falls back to an async send with timeout when full.
1143pub(crate) async fn send_command<T: Command>(
1144    cmd: T,
1145    sender: PageSender,
1146    session: Option<SessionId>,
1147    request_timeout: std::time::Duration,
1148) -> Result<tokio::sync::oneshot::Receiver<Result<chromiumoxide_types::Response, CdpError>>> {
1149    let (tx, rx) = oneshot_channel();
1150    let msg = CommandMessage::with_session(cmd, tx, session)?;
1151    let target_msg = TargetMessage::Command(msg);
1152    match sender.try_send(target_msg) {
1153        Ok(()) => {}
1154        Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
1155            tokio::time::timeout(request_timeout, sender.send(msg))
1156                .await
1157                .map_err(|_| CdpError::Timeout)?
1158                .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
1159        }
1160        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
1161            return Err(CdpError::ChannelSendError(crate::error::ChannelError::Send));
1162        }
1163    }
1164    Ok(rx)
1165}
1166
1167#[cfg(test)]
1168mod page_channel_capacity_tests {
1169    //! Unit tests for `PageHandle::with_capacity` and the capacity plumbing.
1170    //!
1171    //! These verify the three guarantees that keep the change
1172    //! backwards-compatible and panic-free:
1173    //!
1174    //! 1. `PageHandle::new` (legacy signature) still allocates the historic
1175    //!    2048-slot channel — proven by filling the channel to exactly
1176    //!    `DEFAULT_PAGE_CHANNEL_CAPACITY` via `try_send` and then expecting
1177    //!    the next `try_send` to return `Full`. This pins the default.
1178    //!
1179    //! 2. `PageHandle::with_capacity(N)` allocates an N-slot channel for
1180    //!    any N, verified the same way at a small N so the test runs fast.
1181    //!
1182    //! 3. `PageHandle::with_capacity(0)` does NOT panic — we clamp to 1
1183    //!    internally because `tokio::sync::mpsc::channel(0)` aborts. The
1184    //!    resulting channel has exactly one slot.
1185    //!
1186    //! No browser / runtime is spun up — these tests are purely in-process
1187    //! on the mpsc primitives.
1188    use super::{PageHandle, DEFAULT_PAGE_CHANNEL_CAPACITY};
1189    use crate::handler::target::TargetMessage;
1190    use chromiumoxide_cdp::cdp::browser_protocol::target::{SessionId, TargetId};
1191    use std::time::Duration;
1192    use tokio::sync::mpsc::error::TrySendError;
1193
1194    /// Trivial zero-cost `TargetMessage` for filling the channel.
1195    /// We're only exercising the bounded-mpsc slot count — the payload
1196    /// shape is irrelevant, so use the smallest variant.
1197    fn make_msg() -> TargetMessage {
1198        TargetMessage::BlockNetwork(false)
1199    }
1200
1201    fn make_handle(capacity: usize) -> PageHandle {
1202        PageHandle::with_capacity(
1203            TargetId::from("t".to_string()),
1204            SessionId::from("s".to_string()),
1205            None,
1206            Duration::from_secs(30),
1207            None,
1208            capacity,
1209        )
1210    }
1211
1212    /// Fill a page's channel to capacity via `try_send` and return the
1213    /// observed slot count — the number of sends that succeeded before
1214    /// the first `Full` error.
1215    fn observed_capacity(handle: &PageHandle, upper_bound: usize) -> usize {
1216        // `PageSender` is the page's send half; `try_send` surfaces the
1217        // underlying mpsc `TrySendError::Full` once the bounded buffer
1218        // is saturated without consuming a slot, which is what we count.
1219        let sender = &handle.page.sender;
1220        let mut sent = 0;
1221        for _ in 0..upper_bound {
1222            match sender.try_send(make_msg()) {
1223                Ok(()) => sent += 1,
1224                Err(TrySendError::Full(_)) => return sent,
1225                Err(TrySendError::Closed(_)) => {
1226                    panic!("channel unexpectedly closed at {sent} sends")
1227                }
1228            }
1229        }
1230        sent
1231    }
1232
1233    #[test]
1234    fn new_delegates_to_default_capacity() {
1235        let handle = PageHandle::new(
1236            TargetId::from("t".to_string()),
1237            SessionId::from("s".to_string()),
1238            None,
1239            Duration::from_secs(30),
1240            None,
1241        );
1242        let n = observed_capacity(&handle, DEFAULT_PAGE_CHANNEL_CAPACITY + 16);
1243        assert_eq!(
1244            n, DEFAULT_PAGE_CHANNEL_CAPACITY,
1245            "legacy PageHandle::new must preserve the 2048-slot default"
1246        );
1247    }
1248
1249    #[test]
1250    fn with_capacity_respects_arbitrary_value() {
1251        // Small N keeps the test fast; 4 is enough to distinguish from
1252        // the 2048 default.
1253        for n in [1_usize, 4, 16, 64] {
1254            let handle = make_handle(n);
1255            assert_eq!(
1256                observed_capacity(&handle, n + 16),
1257                n,
1258                "with_capacity({n}) should produce exactly {n} slots",
1259            );
1260        }
1261    }
1262
1263    #[test]
1264    fn zero_capacity_is_clamped_to_one_and_does_not_panic() {
1265        // `tokio::sync::mpsc::channel(0)` panics — our `.max(1)` clamp
1266        // must turn that into a single-slot channel rather than an abort.
1267        let handle = make_handle(0);
1268        assert_eq!(
1269            observed_capacity(&handle, 4),
1270            1,
1271            "zero capacity must clamp to 1 and not panic"
1272        );
1273    }
1274}