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