Skip to main content

chromiumoxide/
element.rs

1use hashbrown::HashMap;
2use std::path::Path;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6
7use futures_util::{future, FutureExt, Stream};
8use std::future::Future;
9
10use chromiumoxide_cdp::cdp::browser_protocol::dom::{
11    BackendNodeId, DescribeNodeParams, GetBoxModelParams, GetContentQuadsParams, Node, NodeId,
12    ResolveNodeParams,
13};
14use chromiumoxide_cdp::cdp::browser_protocol::page::{
15    CaptureScreenshotFormat, CaptureScreenshotParams, Viewport,
16};
17use chromiumoxide_cdp::cdp::js_protocol::runtime::{
18    CallFunctionOnReturns, GetPropertiesParams, PropertyDescriptor, RemoteObjectId,
19    RemoteObjectType,
20};
21
22use crate::error::{CdpError, Result};
23use crate::handler::PageInner;
24use crate::layout::{BoundingBox, BoxModel, ElementQuad, Point};
25use crate::utils;
26
27/// Represents a [DOM Element](https://developer.mozilla.org/en-US/docs/Web/API/Element).
28#[derive(Debug)]
29pub struct Element {
30    /// The Unique object identifier
31    pub remote_object_id: RemoteObjectId,
32    /// Identifier of the backend node.
33    pub backend_node_id: BackendNodeId,
34    /// The identifier of the node this element represents.
35    pub node_id: NodeId,
36    /// The active tab.
37    tab: Arc<PageInner>,
38}
39
40impl Element {
41    pub(crate) async fn new(tab: Arc<PageInner>, node_id: NodeId) -> Result<Self> {
42        let backend_node_id = tab
43            .execute(
44                DescribeNodeParams::builder()
45                    .node_id(node_id)
46                    .depth(-1)
47                    .pierce(true)
48                    .build(),
49            )
50            .await?
51            .node
52            .backend_node_id;
53
54        let resp = tab
55            .execute(
56                ResolveNodeParams::builder()
57                    .backend_node_id(backend_node_id)
58                    .build(),
59            )
60            .await?;
61
62        let remote_object_id = resp
63            .result
64            .object
65            .object_id
66            .ok_or_else(|| CdpError::msg(format!("No object Id found for {node_id:?}")))?;
67
68        Ok(Self {
69            remote_object_id,
70            backend_node_id,
71            node_id,
72            tab,
73        })
74    }
75
76    /// Convert a slice of `NodeId`s into a `Vec` of `Element`s
77    pub(crate) async fn from_nodes(tab: &Arc<PageInner>, node_ids: &[NodeId]) -> Result<Vec<Self>> {
78        future::join_all(
79            node_ids
80                .iter()
81                .copied()
82                .map(|id| Element::new(Arc::clone(tab), id)),
83        )
84        .await
85        .into_iter()
86        .collect::<Result<Vec<_>, _>>()
87    }
88
89    /// Returns the first element in the document which matches the given CSS
90    /// selector.
91    pub async fn find_element(&self, selector: impl Into<String>) -> Result<Self> {
92        let node_id = self.tab.find_element(selector, self.node_id).await?;
93        Element::new(Arc::clone(&self.tab), node_id).await
94    }
95
96    /// Return all `Element`s in the document that match the given selector
97    pub async fn find_elements(&self, selector: impl Into<String>) -> Result<Vec<Element>> {
98        Element::from_nodes(
99            &self.tab,
100            &self.tab.find_elements(selector, self.node_id).await?,
101        )
102        .await
103    }
104
105    async fn box_model(&self) -> Result<BoxModel> {
106        let model = self
107            .tab
108            .execute(
109                GetBoxModelParams::builder()
110                    .backend_node_id(self.backend_node_id)
111                    .build(),
112            )
113            .await?
114            .result
115            .model;
116        Ok(BoxModel {
117            content: ElementQuad::from_quad(&model.content),
118            padding: ElementQuad::from_quad(&model.padding),
119            border: ElementQuad::from_quad(&model.border),
120            margin: ElementQuad::from_quad(&model.margin),
121            width: model.width as u32,
122            height: model.height as u32,
123        })
124    }
125
126    /// Returns the bounding box of the element (relative to the main frame)
127    pub async fn bounding_box(&self) -> Result<BoundingBox> {
128        let bounds = self.box_model().await?;
129        let quad = bounds.border;
130
131        let x = quad.most_left();
132        let y = quad.most_top();
133        let width = quad.most_right() - x;
134        let height = quad.most_bottom() - y;
135
136        Ok(BoundingBox {
137            x,
138            y,
139            width,
140            height,
141        })
142    }
143
144    /// Returns the best `Point` of this node to execute a click on.
145    pub async fn clickable_point(&self) -> Result<Point> {
146        let content_quads = self
147            .tab
148            .execute(
149                GetContentQuadsParams::builder()
150                    .backend_node_id(self.backend_node_id)
151                    .build(),
152            )
153            .await?;
154        content_quads
155            .quads
156            .iter()
157            .filter(|q| q.inner().len() == 8)
158            .map(ElementQuad::from_quad)
159            .filter(|q| q.quad_area() > 1.)
160            .map(|q| q.quad_center())
161            .next()
162            .ok_or_else(|| CdpError::msg("Node is either not visible or not an HTMLElement"))
163    }
164
165    /// Submits a javascript function to the page and returns the evaluated
166    /// result
167    ///
168    /// # Example get the element as JSON object
169    ///
170    /// ```no_run
171    /// # use chromiumoxide::element::Element;
172    /// # use chromiumoxide::error::Result;
173    /// # async fn demo(element: Element) -> Result<()> {
174    ///     let js_fn = "function() { return this; }";
175    ///     let element_json = element.call_js_fn(js_fn, false).await?;
176    ///     # Ok(())
177    /// # }
178    /// ```
179    ///
180    /// # Execute an async javascript function
181    ///
182    /// ```no_run
183    /// # use chromiumoxide::element::Element;
184    /// # use chromiumoxide::error::Result;
185    /// # async fn demo(element: Element) -> Result<()> {
186    ///     let js_fn = "async function() { return this; }";
187    ///     let element_json = element.call_js_fn(js_fn, true).await?;
188    ///     # Ok(())
189    /// # }
190    /// ```
191    pub async fn call_js_fn(
192        &self,
193        function_declaration: impl Into<String>,
194        await_promise: bool,
195    ) -> Result<CallFunctionOnReturns> {
196        self.tab
197            .call_js_fn(
198                function_declaration,
199                await_promise,
200                self.remote_object_id.clone(),
201            )
202            .await
203    }
204
205    /// Returns a JSON representation of this element.
206    pub async fn json_value(&self) -> Result<serde_json::Value> {
207        let element_json = self
208            .call_js_fn("function() { return this; }", false)
209            .await?;
210        element_json.result.value.ok_or(CdpError::NotFound)
211    }
212
213    /// Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element.
214    pub async fn focus(&self) -> Result<&Self> {
215        self.call_js_fn("function() { this.focus(); }", true)
216            .await?;
217        Ok(self)
218    }
219
220    /// Scrolls the element into view and uses a mouse event to move the mouse
221    /// over the center of this element.
222    pub async fn hover(&self) -> Result<&Self> {
223        self.scroll_into_view().await?;
224        self.tab.move_mouse(self.clickable_point().await?).await?;
225        Ok(self)
226    }
227
228    /// Scrolls the element into view.
229    ///
230    /// Fails if the element's node is not a HTML element or is detached from
231    /// the document
232    pub async fn scroll_into_view(&self) -> Result<&Self> {
233        let resp = self
234            .call_js_fn(
235                "async function(){if(!this.isConnected)return'Node is detached from document';if(this.nodeType!==Node.ELEMENT_NODE)return'Node is not of type HTMLElement';const e=await new Promise(t=>{const o=new IntersectionObserver(e=>{t(e[0].intersectionRatio),o.disconnect()});o.observe(this)});return 1!==e&&this.scrollIntoView({block:'center',inline:'center',behavior:'instant'}),!1}",
236                true,
237            )
238            .await?;
239
240        if resp.result.r#type == RemoteObjectType::String {
241            let error_text = resp
242                .result
243                .value
244                .unwrap_or_default()
245                .as_str()
246                .unwrap_or_default()
247                .to_string();
248            return Err(CdpError::ScrollingFailed(error_text));
249        }
250        Ok(self)
251    }
252
253    /// This focuses the element by click on it
254    ///
255    /// Bear in mind that if `click()` triggers a navigation this element may be
256    /// not exist anymore.
257    pub async fn click(&self) -> Result<&Self> {
258        let center = self.scroll_into_view().await?.clickable_point().await?;
259        self.tab.click(center).await?;
260        Ok(self)
261    }
262
263    /// Click this element after approaching it along a human-like bezier
264    /// path with the configured pre-click dwell.
265    ///
266    /// Same scroll-into-view + clickable-point resolution as
267    /// [`click`](Self::click), but routes the dispatch through
268    /// [`Page::click_smooth`](crate::Page::click_smooth) so the cursor
269    /// arrives via a smart-mouse trajectory instead of teleporting. Use
270    /// this on antibot-sensitive flows (captcha, login forms,
271    /// challenge widgets) where the move→press timing fingerprint
272    /// matters.
273    pub async fn click_smooth(&self) -> Result<&Self> {
274        let center = self.scroll_into_view().await?.clickable_point().await?;
275        self.tab.click_smooth(center).await?;
276        Ok(self)
277    }
278
279    /// Type the input
280    ///
281    /// # Example type text into an input element
282    ///
283    /// ```no_run
284    /// # use chromiumoxide::page::Page;
285    /// # use chromiumoxide::error::Result;
286    /// # async fn demo(page: Page) -> Result<()> {
287    ///     let element = page.find_element("input#searchInput").await?;
288    ///     element.click().await?.type_str("this goes into the input field").await?;
289    ///     # Ok(())
290    /// # }
291    /// ```
292    pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
293        self.tab.type_str(input).await?;
294        Ok(self)
295    }
296
297    /// Type the input
298    ///
299    /// # Example type text into an input element
300    ///
301    /// ```no_run
302    /// # use chromiumoxide::page::Page;
303    /// # use chromiumoxide::error::Result;
304    /// # async fn demo(page: Page) -> Result<()> {
305    ///     let element = page.find_element("input#searchInput").await?;
306    ///     element.click().await?.type_str("this goes into the input field").await?;
307    ///     # Ok(())
308    /// # }
309    /// ```
310    pub async fn type_str_with_modifier(
311        &self,
312        input: impl AsRef<str>,
313        modifiers: i64,
314    ) -> Result<&Self> {
315        self.tab
316            .type_str_with_modifier(input, Some(modifiers))
317            .await?;
318        Ok(self)
319    }
320
321    /// Presses the key.
322    ///
323    /// # Example type text into an input element and hit enter
324    ///
325    /// ```no_run
326    /// # use chromiumoxide::page::Page;
327    /// # use chromiumoxide::error::Result;
328    /// # async fn demo(page: Page) -> Result<()> {
329    ///     let element = page.find_element("input#searchInput").await?;
330    ///     element.click().await?.type_str("this goes into the input field").await?
331    ///          .press_key("Enter").await?;
332    ///     # Ok(())
333    /// # }
334    /// ```
335    pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
336        self.tab.press_key(key).await?;
337        Ok(self)
338    }
339
340    /// The description of the element's node
341    pub async fn description(&self) -> Result<Node> {
342        Ok(self
343            .tab
344            .execute(
345                DescribeNodeParams::builder()
346                    .backend_node_id(self.backend_node_id)
347                    .depth(100)
348                    .build(),
349            )
350            .await?
351            .result
352            .node)
353    }
354
355    /// Attributes of the `Element` node in the form of flat array `[name1,
356    /// value1, name2, value2]
357    pub async fn attributes(&self) -> Result<Vec<String>> {
358        let node = self.description().await?;
359        Ok(node.attributes.unwrap_or_default())
360    }
361
362    /// Returns the value of the element's attribute
363    pub async fn attribute(&self, attribute: impl AsRef<str>) -> Result<Option<String>> {
364        let js_fn = format!(
365            "function() {{ return this.getAttribute('{}'); }}",
366            attribute.as_ref()
367        );
368        let resp = self.call_js_fn(js_fn, false).await?;
369        if let Some(value) = resp.result.value {
370            Ok(serde_json::from_value(value)?)
371        } else {
372            Ok(None)
373        }
374    }
375
376    /// A `Stream` over all attributes and their values
377    pub async fn iter_attributes(
378        &self,
379    ) -> Result<impl Stream<Item = (String, Result<Option<String>>)> + '_> {
380        let attributes = self.attributes().await?;
381        Ok(AttributeStream {
382            attributes,
383            fut: None,
384            element: self,
385        })
386    }
387
388    /// The inner text of this element.
389    pub async fn inner_text(&self) -> Result<Option<String>> {
390        self.string_property("innerText").await
391    }
392
393    /// The inner HTML of this element.
394    pub async fn inner_html(&self) -> Result<Option<String>> {
395        self.string_property("innerHTML").await
396    }
397
398    /// The outer HTML of this element.
399    pub async fn outer_html(&self) -> Result<Option<String>> {
400        self.string_property("outerHTML").await
401    }
402
403    /// Returns the string property of the element.
404    ///
405    /// If the property is an empty String, `None` is returned.
406    pub async fn string_property(&self, property: impl AsRef<str>) -> Result<Option<String>> {
407        let property = property.as_ref();
408        let value = self.property(property).await?.ok_or(CdpError::NotFound)?;
409        let txt: String = serde_json::from_value(value)?;
410        if !txt.is_empty() {
411            Ok(Some(txt))
412        } else {
413            Ok(None)
414        }
415    }
416
417    /// Returns the javascript `property` of this element where `property` is
418    /// the name of the requested property of this element.
419    ///
420    /// See also `Element::inner_html`.
421    pub async fn property(&self, property: impl AsRef<str>) -> Result<Option<serde_json::Value>> {
422        let js_fn = format!("function() {{ return this.{}; }}", property.as_ref());
423        let resp = self.call_js_fn(js_fn, false).await?;
424        Ok(resp.result.value)
425    }
426
427    /// Returns a map with all `PropertyDescriptor`s of this element keyed by
428    /// their names
429    pub async fn properties(&self) -> Result<HashMap<String, PropertyDescriptor>> {
430        let mut params = GetPropertiesParams::new(self.remote_object_id.clone());
431        params.own_properties = Some(true);
432
433        let properties = self.tab.execute(params).await?;
434
435        Ok(properties
436            .result
437            .result
438            .into_iter()
439            .map(|p| (p.name.clone(), p))
440            .collect())
441    }
442
443    /// Scrolls the element into and takes a screenshot of it
444    pub async fn screenshot(&self, format: CaptureScreenshotFormat) -> Result<Vec<u8>> {
445        let mut bounding_box = self.scroll_into_view().await?.bounding_box().await?;
446        let viewport = self.tab.layout_metrics().await?.css_layout_viewport;
447
448        bounding_box.x += viewport.page_x as f64;
449        bounding_box.y += viewport.page_y as f64;
450
451        let clip = Viewport {
452            x: viewport.page_x as f64 + bounding_box.x,
453            y: viewport.page_y as f64 + bounding_box.y,
454            width: bounding_box.width,
455            height: bounding_box.height,
456            scale: 1.,
457        };
458
459        self.tab
460            .screenshot(
461                CaptureScreenshotParams::builder()
462                    .format(format)
463                    .clip(clip)
464                    .build(),
465            )
466            .await
467    }
468
469    /// Save a screenshot of the element and write it to `output`
470    pub async fn save_screenshot(
471        &self,
472        format: CaptureScreenshotFormat,
473        output: impl AsRef<Path>,
474    ) -> Result<Vec<u8>> {
475        let img = self.screenshot(format).await?;
476        utils::write(output.as_ref(), &img).await?;
477        Ok(img)
478    }
479}
480
481pub type AttributeValueFuture<'a> = Option<(
482    String,
483    Pin<Box<dyn Future<Output = Result<Option<String>>> + 'a>>,
484)>;
485
486/// Stream over all element's attributes
487#[must_use = "streams do nothing unless polled"]
488#[allow(missing_debug_implementations)]
489pub struct AttributeStream<'a> {
490    attributes: Vec<String>,
491    fut: AttributeValueFuture<'a>,
492    element: &'a Element,
493}
494
495impl<'a> Stream for AttributeStream<'a> {
496    type Item = (String, Result<Option<String>>);
497
498    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
499        let pin = self.get_mut();
500
501        if pin.fut.is_none() {
502            if let Some(name) = pin.attributes.pop() {
503                let fut = Box::pin(pin.element.attribute(name.clone()));
504                pin.fut = Some((name, fut));
505            } else {
506                return Poll::Ready(None);
507            }
508        }
509
510        if let Some((name, mut fut)) = pin.fut.take() {
511            if let Poll::Ready(res) = fut.poll_unpin(cx) {
512                return Poll::Ready(Some((name, res)));
513            } else {
514                pin.fut = Some((name, fut));
515            }
516        }
517        Poll::Pending
518    }
519}