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
66impl PageHandle {
67    pub fn new(
68        target_id: TargetId,
69        session_id: SessionId,
70        opener_id: Option<TargetId>,
71        request_timeout: std::time::Duration,
72        page_wake: Option<Arc<Notify>>,
73    ) -> Self {
74        let (commands, rx) = channel(2048);
75        let page = PageInner {
76            target_id,
77            session_id,
78            opener_id,
79            sender: PageSender::new(commands, page_wake),
80            smart_mouse: SmartMouse::new(),
81            request_timeout,
82        };
83        ACTIVE_PAGES.fetch_add(1, Ordering::Relaxed);
84        Self {
85            rx,
86            page: Arc::new(page),
87        }
88    }
89
90    pub(crate) fn inner(&self) -> &Arc<PageInner> {
91        &self.page
92    }
93}
94
95#[derive(Debug)]
96pub(crate) struct PageInner {
97    /// The page target ID.
98    target_id: TargetId,
99    /// The session ID.
100    session_id: SessionId,
101    /// The opener ID.
102    opener_id: Option<TargetId>,
103    /// The sender for the target (with optional handler notification).
104    sender: PageSender,
105    /// Smart mouse with position tracking and human-like movement.
106    pub(crate) smart_mouse: SmartMouse,
107    /// The request timeout for CDP commands issued from this page.
108    request_timeout: std::time::Duration,
109}
110
111impl Drop for PageInner {
112    fn drop(&mut self) {
113        ACTIVE_PAGES.fetch_sub(1, Ordering::Relaxed);
114    }
115}
116
117impl PageInner {
118    /// Execute a PDL command and return its response
119    pub(crate) async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
120        execute(
121            cmd,
122            self.sender.clone(),
123            Some(self.session_id.clone()),
124            self.request_timeout,
125        )
126        .await
127    }
128
129    /// Execute a PDL command without waiting for the response.
130    pub(crate) async fn send_command<T: Command>(&self, cmd: T) -> Result<&Self> {
131        let _ = send_command(
132            cmd,
133            self.sender.clone(),
134            Some(self.session_id.clone()),
135            self.request_timeout,
136        )
137        .await;
138        Ok(self)
139    }
140
141    /// Create a PDL command future
142    pub(crate) fn command_future<T: Command>(&self, cmd: T) -> Result<CommandFuture<T>> {
143        CommandFuture::new(
144            cmd,
145            self.sender.clone(),
146            Some(self.session_id.clone()),
147            self.request_timeout,
148        )
149    }
150
151    /// This creates navigation future with the final http response when the page is loaded
152    pub(crate) fn wait_for_navigation(&self) -> TargetMessageFuture<ArcHttpRequest> {
153        TargetMessageFuture::<ArcHttpRequest>::wait_for_navigation(
154            self.sender.clone(),
155            self.request_timeout,
156        )
157    }
158
159    /// Resolves once `DOMContentLoaded` fires (before `load`).
160    pub(crate) fn wait_for_dom_content_loaded(&self) -> TargetMessageFuture<ArcHttpRequest> {
161        TargetMessageFuture::<ArcHttpRequest>::wait_for_dom_content_loaded(
162            self.sender.clone(),
163            self.request_timeout,
164        )
165    }
166
167    /// Resolves once the `load` event fires (all subresources done).
168    pub(crate) fn wait_for_load(&self) -> TargetMessageFuture<ArcHttpRequest> {
169        TargetMessageFuture::<ArcHttpRequest>::wait_for_load(
170            self.sender.clone(),
171            self.request_timeout,
172        )
173    }
174
175    /// This creates navigation future with the final http response when the page network is idle
176    pub(crate) fn wait_for_network_idle(&self) -> TargetMessageFuture<ArcHttpRequest> {
177        TargetMessageFuture::<ArcHttpRequest>::wait_for_network_idle(
178            self.sender.clone(),
179            self.request_timeout,
180        )
181    }
182
183    /// This creates navigation future with the final http response when the page network is almost idle
184    pub(crate) fn wait_for_network_almost_idle(&self) -> TargetMessageFuture<ArcHttpRequest> {
185        TargetMessageFuture::<ArcHttpRequest>::wait_for_network_almost_idle(
186            self.sender.clone(),
187            self.request_timeout,
188        )
189    }
190
191    /// This creates HTTP future with navigation and responds with the final
192    /// http response when the page is loaded
193    pub(crate) fn http_future<T: Command>(&self, cmd: T) -> Result<HttpFuture<T>> {
194        Ok(HttpFuture::new(
195            self.sender.clone(),
196            self.command_future(cmd)?,
197            self.request_timeout,
198        ))
199    }
200
201    /// The identifier of this page's target
202    pub fn target_id(&self) -> &TargetId {
203        &self.target_id
204    }
205
206    /// The identifier of this page's target's session
207    pub fn session_id(&self) -> &SessionId {
208        &self.session_id
209    }
210
211    /// The identifier of this page's target's opener target
212    pub fn opener_id(&self) -> &Option<TargetId> {
213        &self.opener_id
214    }
215
216    /// Send a `TargetMessage` with the page's request timeout.
217    /// Uses a `try_send` fast path to avoid the async overhead when
218    /// the channel has capacity (common case under normal load).
219    pub(crate) async fn send_msg(&self, msg: TargetMessage) -> Result<()> {
220        match self.sender.try_send(msg) {
221            Ok(()) => Ok(()),
222            Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
223                tokio::time::timeout(self.request_timeout, self.sender.send(msg))
224                    .await
225                    .map_err(|_| CdpError::Timeout)?
226                    .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
227                Ok(())
228            }
229            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
230                Err(CdpError::ChannelSendError(crate::error::ChannelError::Send))
231            }
232        }
233    }
234
235    /// Await a oneshot response with the page's request timeout.
236    pub(crate) async fn recv_msg<T>(&self, rx: tokio::sync::oneshot::Receiver<T>) -> Result<T> {
237        tokio::time::timeout(self.request_timeout, rx)
238            .await
239            .map_err(|_| CdpError::Timeout)?
240            .map_err(|e| CdpError::ChannelSendError(crate::error::ChannelError::Canceled(e)))
241    }
242
243    /// Returns the first element in the node which matches the given CSS
244    /// selector.
245    pub async fn find_element(&self, selector: impl Into<String>, node: NodeId) -> Result<NodeId> {
246        Ok(self
247            .execute(QuerySelectorParams::new(node, selector))
248            .await?
249            .node_id)
250    }
251
252    /// Returns the outer html of the page.
253    pub async fn outer_html(
254        &self,
255        object_id: RemoteObjectId,
256        node_id: NodeId,
257        backend_node_id: BackendNodeId,
258    ) -> Result<String> {
259        let cmd = GetOuterHtmlParams {
260            backend_node_id: Some(backend_node_id),
261            node_id: Some(node_id),
262            object_id: Some(object_id),
263            ..Default::default()
264        };
265
266        let chromiumoxide_types::CommandResponse { result, .. } = self.execute(cmd).await?;
267
268        Ok(result.outer_html)
269    }
270
271    /// Activates (focuses) the target.
272    pub async fn activate(&self) -> Result<&Self> {
273        self.execute(ActivateTargetParams::new(self.target_id().clone()))
274            .await?;
275        Ok(self)
276    }
277
278    /// Version information about the browser
279    pub async fn version(&self) -> Result<GetVersionReturns> {
280        Ok(self.execute(GetVersionParams::default()).await?.result)
281    }
282
283    /// Return all `Element`s inside the node that match the given selector
284    pub(crate) async fn find_elements(
285        &self,
286        selector: impl Into<String>,
287        node: NodeId,
288    ) -> Result<Vec<NodeId>> {
289        Ok(self
290            .execute(QuerySelectorAllParams::new(node, selector))
291            .await?
292            .result
293            .node_ids)
294    }
295
296    /// Returns all elements which matches the given xpath selector
297    pub async fn find_xpaths(&self, query: impl Into<String>) -> Result<Vec<NodeId>> {
298        let perform_search_returns = self
299            .execute(PerformSearchParams {
300                query: query.into(),
301                include_user_agent_shadow_dom: Some(true),
302            })
303            .await?
304            .result;
305
306        let search_results = self
307            .execute(GetSearchResultsParams::new(
308                perform_search_returns.search_id.clone(),
309                0,
310                perform_search_returns.result_count,
311            ))
312            .await?
313            .result;
314
315        self.execute(DiscardSearchResultsParams::new(
316            perform_search_returns.search_id,
317        ))
318        .await?;
319
320        Ok(search_results.node_ids)
321    }
322
323    /// Moves the mouse to this point (dispatches a mouseMoved event).
324    /// Also updates the tracked mouse position.
325    pub async fn move_mouse(&self, point: Point) -> Result<&Self> {
326        self.smart_mouse.set_position(point);
327        self.execute(DispatchMouseEventParams::new(
328            DispatchMouseEventType::MouseMoved,
329            point.x,
330            point.y,
331        ))
332        .await?;
333        Ok(self)
334    }
335
336    /// Moves the mouse to `target` along a human-like bezier curve path,
337    /// dispatching intermediate `mouseMoved` events with natural timing.
338    pub async fn move_mouse_smooth(&self, target: Point) -> Result<&Self> {
339        let path = self.smart_mouse.path_to(target);
340        for step in &path {
341            self.execute(DispatchMouseEventParams::new(
342                DispatchMouseEventType::MouseMoved,
343                step.point.x,
344                step.point.y,
345            ))
346            .await?;
347            tokio::time::sleep(step.delay).await;
348        }
349        Ok(self)
350    }
351
352    /// Returns the current tracked mouse position.
353    pub fn mouse_position(&self) -> Point {
354        self.smart_mouse.position()
355    }
356
357    /// Scrolls the current page by the specified horizontal and vertical offsets.
358    /// This method helps when Chrome version may not support certain CDP dispatch events.
359    pub async fn scroll_by(
360        &self,
361        delta_x: f64,
362        delta_y: f64,
363        behavior: ScrollBehavior,
364    ) -> Result<&Self> {
365        let behavior_str = match behavior {
366            ScrollBehavior::Auto => "auto",
367            ScrollBehavior::Instant => "instant",
368            ScrollBehavior::Smooth => "smooth",
369        };
370
371        self.evaluate_expression(format!(
372            "window.scrollBy({{top: {}, left: {}, behavior: '{}'}});",
373            delta_y, delta_x, behavior_str
374        ))
375        .await?;
376
377        Ok(self)
378    }
379
380    /// Dispatches a `DragEvent`, moving the element to the given `point`.
381    ///
382    /// `point.x` defines the horizontal target, and `point.y` the vertical mouse position.
383    /// Accepts `drag_type`, `drag_data`, and optional keyboard `modifiers`.
384    pub async fn drag(
385        &self,
386        drag_type: DispatchDragEventType,
387        point: Point,
388        drag_data: DragData,
389        modifiers: Option<i64>,
390    ) -> Result<&Self> {
391        let mut params: DispatchDragEventParams =
392            DispatchDragEventParams::new(drag_type, point.x, point.y, drag_data);
393
394        if let Some(modifiers) = modifiers {
395            params.modifiers = Some(modifiers);
396        }
397
398        self.execute(params).await?;
399        Ok(self)
400    }
401
402    /// Moves the mouse to this point (dispatches a mouseWheel event).
403    /// If you get an error use page.scroll_by instead.
404    pub async fn scroll(&self, point: Point, delta: Delta) -> Result<&Self> {
405        let mut params: DispatchMouseEventParams =
406            DispatchMouseEventParams::new(DispatchMouseEventType::MouseWheel, point.x, point.y);
407
408        params.delta_x = Some(delta.delta_x);
409        params.delta_y = Some(delta.delta_y);
410
411        self.execute(params).await?;
412        Ok(self)
413    }
414
415    /// Performs a mouse click event at the point's location with the amount of clicks and modifier.
416    pub async fn click_with_count_base(
417        &self,
418        point: Point,
419        click_count: impl Into<i64>,
420        modifiers: impl Into<i64>,
421        button: impl Into<MouseButton>,
422    ) -> Result<&Self> {
423        let cmd = DispatchMouseEventParams::builder()
424            .x(point.x)
425            .y(point.y)
426            .button(button)
427            .click_count(click_count)
428            .modifiers(modifiers);
429
430        if let Ok(cmd) = cmd
431            .clone()
432            .r#type(DispatchMouseEventType::MousePressed)
433            .build()
434        {
435            self.move_mouse(point).await?.send_command(cmd).await?;
436        }
437
438        if let Ok(cmd) = cmd.r#type(DispatchMouseEventType::MouseReleased).build() {
439            self.execute(cmd).await?;
440        }
441
442        self.smart_mouse.set_position(point);
443        Ok(self)
444    }
445
446    /// Move smoothly to `point` with human-like movement, then click.
447    pub async fn click_smooth(&self, point: Point) -> Result<&Self> {
448        self.move_mouse_smooth(point).await?;
449        self.click(point).await
450    }
451
452    /// Performs a mouse click event at the point's location with the amount of clicks and modifier.
453    pub async fn click_with_count(
454        &self,
455        point: Point,
456        click_count: impl Into<i64>,
457        modifiers: impl Into<i64>,
458    ) -> Result<&Self> {
459        self.click_with_count_base(point, click_count, modifiers, MouseButton::Left)
460            .await
461    }
462
463    /// Performs a mouse right click event at the point's location with the amount of clicks and modifier.
464    pub async fn right_click_with_count(
465        &self,
466        point: Point,
467        click_count: impl Into<i64>,
468        modifiers: impl Into<i64>,
469    ) -> Result<&Self> {
470        self.click_with_count_base(point, click_count, modifiers, MouseButton::Right)
471            .await
472    }
473
474    /// Performs a mouse middle click event at the point's location with the amount of clicks and modifier.
475    pub async fn middle_click_with_count(
476        &self,
477        point: Point,
478        click_count: impl Into<i64>,
479        modifiers: impl Into<i64>,
480    ) -> Result<&Self> {
481        self.click_with_count_base(point, click_count, modifiers, MouseButton::Middle)
482            .await
483    }
484
485    /// Performs a mouse back click event at the point's location with the amount of clicks and modifier.
486    pub async fn back_click_with_count(
487        &self,
488        point: Point,
489        click_count: impl Into<i64>,
490        modifiers: impl Into<i64>,
491    ) -> Result<&Self> {
492        self.click_with_count_base(point, click_count, modifiers, MouseButton::Back)
493            .await
494    }
495
496    /// Performs a mouse forward click event at the point's location with the amount of clicks and modifier.
497    pub async fn forward_click_with_count(
498        &self,
499        point: Point,
500        click_count: impl Into<i64>,
501        modifiers: impl Into<i64>,
502    ) -> Result<&Self> {
503        self.click_with_count_base(point, click_count, modifiers, MouseButton::Forward)
504            .await
505    }
506
507    /// Performs a click-and-drag from one point to another with optional modifiers.
508    pub async fn click_and_drag(
509        &self,
510        from: Point,
511        to: Point,
512        modifiers: impl Into<i64>,
513    ) -> Result<&Self> {
514        let modifiers = modifiers.into();
515        let click_count = 1;
516
517        let cmd = DispatchMouseEventParams::builder()
518            .button(MouseButton::Left)
519            .click_count(click_count)
520            .modifiers(modifiers);
521
522        if let Ok(cmd) = cmd
523            .clone()
524            .x(from.x)
525            .y(from.y)
526            .r#type(DispatchMouseEventType::MousePressed)
527            .build()
528        {
529            self.move_mouse(from).await?.send_command(cmd).await?;
530        }
531
532        if let Ok(cmd) = cmd
533            .clone()
534            .x(to.x)
535            .y(to.y)
536            .r#type(DispatchMouseEventType::MouseMoved)
537            .build()
538        {
539            self.move_mouse(to).await?.send_command(cmd).await?;
540        }
541
542        if let Ok(cmd) = cmd
543            .r#type(DispatchMouseEventType::MouseReleased)
544            .x(to.x)
545            .y(to.y)
546            .build()
547        {
548            self.send_command(cmd).await?;
549        }
550
551        self.smart_mouse.set_position(to);
552        Ok(self)
553    }
554
555    /// Performs a smooth click-and-drag: moves to `from` with a bezier path,
556    /// presses, drags along a bezier path to `to`, then releases.
557    pub async fn click_and_drag_smooth(
558        &self,
559        from: Point,
560        to: Point,
561        modifiers: impl Into<i64>,
562    ) -> Result<&Self> {
563        let modifiers = modifiers.into();
564
565        // Smooth move to the starting point
566        self.move_mouse_smooth(from).await?;
567
568        // Press at starting point
569        if let Ok(cmd) = DispatchMouseEventParams::builder()
570            .x(from.x)
571            .y(from.y)
572            .button(MouseButton::Left)
573            .click_count(1)
574            .modifiers(modifiers)
575            .r#type(DispatchMouseEventType::MousePressed)
576            .build()
577        {
578            self.send_command(cmd).await?;
579        }
580
581        // Smooth drag to destination (dispatching MouseMoved with button held)
582        let path = self.smart_mouse.path_to(to);
583        for step in &path {
584            if let Ok(cmd) = DispatchMouseEventParams::builder()
585                .x(step.point.x)
586                .y(step.point.y)
587                .button(MouseButton::Left)
588                .modifiers(modifiers)
589                .r#type(DispatchMouseEventType::MouseMoved)
590                .build()
591            {
592                self.send_command(cmd).await?;
593            }
594            tokio::time::sleep(step.delay).await;
595        }
596
597        // Release at destination
598        if let Ok(cmd) = DispatchMouseEventParams::builder()
599            .x(to.x)
600            .y(to.y)
601            .button(MouseButton::Left)
602            .click_count(1)
603            .modifiers(modifiers)
604            .r#type(DispatchMouseEventType::MouseReleased)
605            .build()
606        {
607            self.send_command(cmd).await?;
608        }
609
610        Ok(self)
611    }
612
613    /// Performs a mouse click event at the point's location
614    pub async fn click(&self, point: Point) -> Result<&Self> {
615        self.click_with_count(point, 1, 0).await
616    }
617
618    /// Performs a mouse double click event at the point's location
619    pub async fn double_click(&self, point: Point) -> Result<&Self> {
620        self.click_with_count(point, 2, 0).await
621    }
622
623    /// Performs a mouse right click event at the point's location
624    pub async fn right_click(&self, point: Point) -> Result<&Self> {
625        self.right_click_with_count(point, 1, 0).await
626    }
627
628    /// Performs a mouse middle click event at the point's location
629    pub async fn middle_click(&self, point: Point) -> Result<&Self> {
630        self.middle_click_with_count(point, 1, 0).await
631    }
632
633    /// Performs a mouse back click event at the point's location
634    pub async fn back_click(&self, point: Point) -> Result<&Self> {
635        self.back_click_with_count(point, 1, 0).await
636    }
637
638    /// Performs a mouse forward click event at the point's location
639    pub async fn forward_click(&self, point: Point) -> Result<&Self> {
640        self.forward_click_with_count(point, 1, 0).await
641    }
642
643    /// Performs a mouse click event at the point's location and modifier: Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).
644    pub async fn click_with_modifier(
645        &self,
646        point: Point,
647        modifiers: impl Into<i64>,
648    ) -> Result<&Self> {
649        self.click_with_count(point, 1, modifiers).await
650    }
651
652    /// 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).
653    pub async fn right_click_with_modifier(
654        &self,
655        point: Point,
656        modifiers: impl Into<i64>,
657    ) -> Result<&Self> {
658        self.right_click_with_count(point, 1, modifiers).await
659    }
660
661    /// 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).
662    pub async fn middle_click_with_modifier(
663        &self,
664        point: Point,
665        modifiers: impl Into<i64>,
666    ) -> Result<&Self> {
667        self.middle_click_with_count(point, 1, modifiers).await
668    }
669
670    /// 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).
671    pub async fn double_click_with_modifier(
672        &self,
673        point: Point,
674        modifiers: impl Into<i64>,
675    ) -> Result<&Self> {
676        self.click_with_count(point, 2, modifiers).await
677    }
678
679    /// This simulates pressing keys on the page.
680    ///
681    /// # Note The `input` is treated as series of `KeyDefinition`s, where each
682    /// char is inserted as a separate keystroke. So sending
683    /// `page.type_str("Enter")` will be processed as a series of single
684    /// keystrokes:  `["E", "n", "t", "e", "r"]`. To simulate pressing the
685    /// actual Enter key instead use `page.press_key(
686    /// keys::get_key_definition("Enter").unwrap())`.
687    pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
688        for c in input.as_ref().split("").filter(|s| !s.is_empty()) {
689            self._press_key(c, None).await?;
690        }
691        Ok(self)
692    }
693
694    /// Fetches the entire accessibility tree for the root Document
695    pub async fn get_full_ax_tree(
696        &self,
697        depth: Option<i64>,
698        frame_id: Option<FrameId>,
699    ) -> Result<GetFullAxTreeReturns> {
700        let mut builder = GetFullAxTreeParamsBuilder::default();
701
702        if let Some(depth) = depth {
703            builder = builder.depth(depth);
704        }
705
706        if let Some(frame_id) = frame_id {
707            builder = builder.frame_id(frame_id);
708        }
709
710        let resp = self.execute(builder.build()).await?;
711
712        Ok(resp.result)
713    }
714
715    /// Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.
716    pub async fn get_partial_ax_tree(
717        &self,
718        node_id: Option<chromiumoxide_cdp::cdp::browser_protocol::dom::NodeId>,
719        backend_node_id: Option<BackendNodeId>,
720        object_id: Option<RemoteObjectId>,
721        fetch_relatives: Option<bool>,
722    ) -> Result<GetPartialAxTreeReturns> {
723        let mut builder = GetPartialAxTreeParamsBuilder::default();
724
725        if let Some(node_id) = node_id {
726            builder = builder.node_id(node_id);
727        }
728
729        if let Some(backend_node_id) = backend_node_id {
730            builder = builder.backend_node_id(backend_node_id);
731        }
732
733        if let Some(object_id) = object_id {
734            builder = builder.object_id(object_id);
735        }
736
737        if let Some(fetch_relatives) = fetch_relatives {
738            builder = builder.fetch_relatives(fetch_relatives);
739        }
740
741        let resp = self.execute(builder.build()).await?;
742
743        Ok(resp.result)
744    }
745
746    /// This simulates pressing keys on the page.
747    ///
748    /// # Note The `input` is treated as series of `KeyDefinition`s, where each
749    /// char is inserted as a separate keystroke. So sending
750    /// `page.type_str("Enter")` will be processed as a series of single
751    /// keystrokes:  `["E", "n", "t", "e", "r"]`. To simulate pressing the
752    /// actual Enter key instead use `page.press_key(
753    /// keys::get_key_definition("Enter").unwrap())`.
754    pub async fn type_str_with_modifier(
755        &self,
756        input: impl AsRef<str>,
757        modifiers: Option<i64>,
758    ) -> Result<&Self> {
759        for c in input.as_ref().split("").filter(|s| !s.is_empty()) {
760            self._press_key(c, modifiers).await?;
761        }
762        Ok(self)
763    }
764
765    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
766    /// keys.
767    async fn _press_key(&self, key: impl AsRef<str>, modifiers: Option<i64>) -> Result<&Self> {
768        let key = key.as_ref();
769        let key_definition = keys::get_key_definition(key)
770            .ok_or_else(|| CdpError::msg(format!("Key not found: {key}")))?;
771        let mut cmd = DispatchKeyEventParams::builder();
772
773        // See https://github.com/GoogleChrome/puppeteer/blob/62da2366c65b335751896afbb0206f23c61436f1/lib/Input.js#L114-L115
774        // And https://github.com/GoogleChrome/puppeteer/blob/62da2366c65b335751896afbb0206f23c61436f1/lib/Input.js#L52
775        let key_down_event_type = if let Some(txt) = key_definition.text {
776            cmd = cmd.text(txt);
777            DispatchKeyEventType::KeyDown
778        } else if key_definition.key.len() == 1 {
779            cmd = cmd.text(key_definition.key);
780            DispatchKeyEventType::KeyDown
781        } else {
782            DispatchKeyEventType::RawKeyDown
783        };
784
785        cmd = cmd
786            .r#type(DispatchKeyEventType::KeyDown)
787            .key(key_definition.key)
788            .code(key_definition.code)
789            .windows_virtual_key_code(key_definition.key_code)
790            .native_virtual_key_code(key_definition.key_code);
791
792        if let Some(modifiers) = modifiers {
793            cmd = cmd.modifiers(modifiers);
794        }
795
796        if let Ok(cmd) = cmd.clone().r#type(key_down_event_type).build() {
797            self.execute(cmd).await?;
798        }
799
800        if let Ok(cmd) = cmd.r#type(DispatchKeyEventType::KeyUp).build() {
801            self.execute(cmd).await?;
802        }
803
804        Ok(self)
805    }
806
807    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
808    /// keys.
809    pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
810        self._press_key(key, None).await
811    }
812
813    /// Uses the `DispatchKeyEvent` mechanism to simulate pressing keyboard
814    /// keys and modifiers.
815    pub async fn press_key_with_modifier(
816        &self,
817        key: impl AsRef<str>,
818        modifiers: Option<i64>,
819    ) -> Result<&Self> {
820        self._press_key(key, modifiers).await
821    }
822
823    /// Calls function with given declaration on the remote object with the
824    /// matching id
825    pub async fn call_js_fn(
826        &self,
827        function_declaration: impl Into<String>,
828        await_promise: bool,
829        remote_object_id: RemoteObjectId,
830    ) -> Result<CallFunctionOnReturns> {
831        if let Ok(resp) = CallFunctionOnParams::builder()
832            .object_id(remote_object_id)
833            .function_declaration(function_declaration)
834            .generate_preview(true)
835            .await_promise(await_promise)
836            .build()
837        {
838            let resp = self.execute(resp).await?;
839            Ok(resp.result)
840        } else {
841            Err(CdpError::NotFound)
842        }
843    }
844
845    pub async fn evaluate_expression(
846        &self,
847        evaluate: impl Into<EvaluateParams>,
848    ) -> Result<EvaluationResult> {
849        let mut evaluate = evaluate.into();
850        if evaluate.context_id.is_none() {
851            evaluate.context_id = self.execution_context().await?;
852        }
853        if evaluate.await_promise.is_none() {
854            evaluate.await_promise = Some(true);
855        }
856        if evaluate.return_by_value.is_none() {
857            evaluate.return_by_value = Some(true);
858        }
859
860        // evaluate.silent = Some(true);
861
862        let resp = self.execute(evaluate).await?.result;
863
864        if let Some(exception) = resp.exception_details {
865            return Err(CdpError::JavascriptException(Box::new(exception)));
866        }
867
868        Ok(EvaluationResult::new(resp.result))
869    }
870
871    pub async fn evaluate_function(
872        &self,
873        evaluate: impl Into<CallFunctionOnParams>,
874    ) -> Result<EvaluationResult> {
875        let mut evaluate = evaluate.into();
876        if evaluate.execution_context_id.is_none() {
877            evaluate.execution_context_id = self.execution_context().await?;
878        }
879        if evaluate.await_promise.is_none() {
880            evaluate.await_promise = Some(true);
881        }
882        if evaluate.return_by_value.is_none() {
883            evaluate.return_by_value = Some(true);
884        }
885
886        // evaluate.silent = Some(true);
887
888        let resp = self.execute(evaluate).await?.result;
889        if let Some(exception) = resp.exception_details {
890            return Err(CdpError::JavascriptException(Box::new(exception)));
891        }
892        Ok(EvaluationResult::new(resp.result))
893    }
894
895    pub async fn execution_context(&self) -> Result<Option<ExecutionContextId>> {
896        self.execution_context_for_world(None, DOMWorldKind::Main)
897            .await
898    }
899
900    pub async fn secondary_execution_context(&self) -> Result<Option<ExecutionContextId>> {
901        self.execution_context_for_world(None, DOMWorldKind::Secondary)
902            .await
903    }
904
905    pub async fn frame_execution_context(
906        &self,
907        frame_id: FrameId,
908    ) -> Result<Option<ExecutionContextId>> {
909        self.execution_context_for_world(Some(frame_id), DOMWorldKind::Main)
910            .await
911    }
912
913    pub async fn frame_secondary_execution_context(
914        &self,
915        frame_id: FrameId,
916    ) -> Result<Option<ExecutionContextId>> {
917        self.execution_context_for_world(Some(frame_id), DOMWorldKind::Secondary)
918            .await
919    }
920
921    pub async fn execution_context_for_world(
922        &self,
923        frame_id: Option<FrameId>,
924        dom_world: DOMWorldKind,
925    ) -> Result<Option<ExecutionContextId>> {
926        let (tx, rx) = oneshot_channel();
927        let msg = TargetMessage::GetExecutionContext(GetExecutionContext {
928            dom_world,
929            frame_id,
930            tx,
931        });
932        match self.sender.try_send(msg) {
933            Ok(()) => {}
934            Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
935                tokio::time::timeout(self.request_timeout, self.sender.send(msg))
936                    .await
937                    .map_err(|_| CdpError::Timeout)?
938                    .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
939            }
940            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
941                return Err(CdpError::ChannelSendError(crate::error::ChannelError::Send));
942            }
943        }
944        Ok(tokio::time::timeout(self.request_timeout, rx)
945            .await
946            .map_err(|_| CdpError::Timeout)??)
947    }
948
949    /// Returns metrics relating to the layout of the page
950    pub async fn layout_metrics(&self) -> Result<GetLayoutMetricsReturns> {
951        Ok(self
952            .execute(GetLayoutMetricsParams::default())
953            .await?
954            .result)
955    }
956
957    /// Enable page Content Security Policy by-passing.
958    pub async fn set_bypass_csp(&self, enabled: bool) -> Result<&Self> {
959        self.execute(SetBypassCspParams::new(enabled)).await?;
960        Ok(self)
961    }
962
963    /// Take a screenshot of the page.
964    pub async fn screenshot(&self, params: impl Into<ScreenshotParams>) -> Result<Vec<u8>> {
965        self.activate().await?;
966        let params = params.into();
967        let full_page = params.full_page();
968        let omit_background = params.omit_background();
969
970        let mut cdp_params = params.cdp_params;
971
972        if full_page {
973            let metrics = self.layout_metrics().await?;
974            let width = metrics.css_content_size.width;
975            let height = metrics.css_content_size.height;
976
977            cdp_params.clip = Some(Viewport {
978                x: 0.,
979                y: 0.,
980                width,
981                height,
982                scale: 1.,
983            });
984
985            self.execute(SetDeviceMetricsOverrideParams::new(
986                width as i64,
987                height as i64,
988                1.,
989                false,
990            ))
991            .await?;
992        }
993
994        if omit_background {
995            self.execute(SetDefaultBackgroundColorOverrideParams {
996                color: Some(Rgba {
997                    r: 0,
998                    g: 0,
999                    b: 0,
1000                    a: Some(0.),
1001                }),
1002            })
1003            .await?;
1004        }
1005
1006        let res = self.execute(cdp_params).await?.result;
1007
1008        if omit_background {
1009            self.send_command(SetDefaultBackgroundColorOverrideParams { color: None })
1010                .await?;
1011        }
1012
1013        if full_page {
1014            self.send_command(ClearDeviceMetricsOverrideParams {})
1015                .await?;
1016        }
1017
1018        Ok(utils::base64::decode(&res.data)?)
1019    }
1020
1021    /// Convert the page to PDF.
1022    pub async fn print_to_pdf(&self, params: impl Into<PrintToPdfParams>) -> Result<Vec<u8>> {
1023        self.activate().await?;
1024        let params = params.into();
1025
1026        let res = self.execute(params).await?.result;
1027
1028        Ok(utils::base64::decode(&res.data)?)
1029    }
1030}
1031
1032pub(crate) async fn execute<T: Command>(
1033    cmd: T,
1034    sender: PageSender,
1035    session: Option<SessionId>,
1036    request_timeout: std::time::Duration,
1037) -> Result<CommandResponse<T::Response>> {
1038    let method = cmd.identifier();
1039    let rx = send_command(cmd, sender, session, request_timeout).await?;
1040    let resp = tokio::time::timeout(request_timeout, rx)
1041        .await
1042        .map_err(|_| CdpError::Timeout)???;
1043    to_command_response::<T>(resp, method)
1044}
1045
1046/// Execute a command without waiting.
1047///
1048/// Uses a `try_send` fast path to avoid async overhead when the channel has
1049/// capacity (common case). Falls back to an async send with timeout when full.
1050pub(crate) async fn send_command<T: Command>(
1051    cmd: T,
1052    sender: PageSender,
1053    session: Option<SessionId>,
1054    request_timeout: std::time::Duration,
1055) -> Result<tokio::sync::oneshot::Receiver<Result<chromiumoxide_types::Response, CdpError>>> {
1056    let (tx, rx) = oneshot_channel();
1057    let msg = CommandMessage::with_session(cmd, tx, session)?;
1058    let target_msg = TargetMessage::Command(msg);
1059    match sender.try_send(target_msg) {
1060        Ok(()) => {}
1061        Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
1062            tokio::time::timeout(request_timeout, sender.send(msg))
1063                .await
1064                .map_err(|_| CdpError::Timeout)?
1065                .map_err(|_| CdpError::ChannelSendError(crate::error::ChannelError::Send))?;
1066        }
1067        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
1068            return Err(CdpError::ChannelSendError(crate::error::ChannelError::Send));
1069        }
1070    }
1071    Ok(rx)
1072}