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    /// Type the input
264    ///
265    /// # Example type text into an input element
266    ///
267    /// ```no_run
268    /// # use chromiumoxide::page::Page;
269    /// # use chromiumoxide::error::Result;
270    /// # async fn demo(page: Page) -> Result<()> {
271    ///     let element = page.find_element("input#searchInput").await?;
272    ///     element.click().await?.type_str("this goes into the input field").await?;
273    ///     # Ok(())
274    /// # }
275    /// ```
276    pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
277        self.tab.type_str(input).await?;
278        Ok(self)
279    }
280
281    /// Type the input
282    ///
283    /// # Example type text into an input element
284    ///
285    /// ```no_run
286    /// # use chromiumoxide::page::Page;
287    /// # use chromiumoxide::error::Result;
288    /// # async fn demo(page: Page) -> Result<()> {
289    ///     let element = page.find_element("input#searchInput").await?;
290    ///     element.click().await?.type_str("this goes into the input field").await?;
291    ///     # Ok(())
292    /// # }
293    /// ```
294    pub async fn type_str_with_modifier(
295        &self,
296        input: impl AsRef<str>,
297        modifiers: i64,
298    ) -> Result<&Self> {
299        self.tab
300            .type_str_with_modifier(input, Some(modifiers))
301            .await?;
302        Ok(self)
303    }
304
305    /// Presses the key.
306    ///
307    /// # Example type text into an input element and hit enter
308    ///
309    /// ```no_run
310    /// # use chromiumoxide::page::Page;
311    /// # use chromiumoxide::error::Result;
312    /// # async fn demo(page: Page) -> Result<()> {
313    ///     let element = page.find_element("input#searchInput").await?;
314    ///     element.click().await?.type_str("this goes into the input field").await?
315    ///          .press_key("Enter").await?;
316    ///     # Ok(())
317    /// # }
318    /// ```
319    pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
320        self.tab.press_key(key).await?;
321        Ok(self)
322    }
323
324    /// The description of the element's node
325    pub async fn description(&self) -> Result<Node> {
326        Ok(self
327            .tab
328            .execute(
329                DescribeNodeParams::builder()
330                    .backend_node_id(self.backend_node_id)
331                    .depth(100)
332                    .build(),
333            )
334            .await?
335            .result
336            .node)
337    }
338
339    /// Attributes of the `Element` node in the form of flat array `[name1,
340    /// value1, name2, value2]
341    pub async fn attributes(&self) -> Result<Vec<String>> {
342        let node = self.description().await?;
343        Ok(node.attributes.unwrap_or_default())
344    }
345
346    /// Returns the value of the element's attribute
347    pub async fn attribute(&self, attribute: impl AsRef<str>) -> Result<Option<String>> {
348        let js_fn = format!(
349            "function() {{ return this.getAttribute('{}'); }}",
350            attribute.as_ref()
351        );
352        let resp = self.call_js_fn(js_fn, false).await?;
353        if let Some(value) = resp.result.value {
354            Ok(serde_json::from_value(value)?)
355        } else {
356            Ok(None)
357        }
358    }
359
360    /// A `Stream` over all attributes and their values
361    pub async fn iter_attributes(
362        &self,
363    ) -> Result<impl Stream<Item = (String, Result<Option<String>>)> + '_> {
364        let attributes = self.attributes().await?;
365        Ok(AttributeStream {
366            attributes,
367            fut: None,
368            element: self,
369        })
370    }
371
372    /// The inner text of this element.
373    pub async fn inner_text(&self) -> Result<Option<String>> {
374        self.string_property("innerText").await
375    }
376
377    /// The inner HTML of this element.
378    pub async fn inner_html(&self) -> Result<Option<String>> {
379        self.string_property("innerHTML").await
380    }
381
382    /// The outer HTML of this element.
383    pub async fn outer_html(&self) -> Result<Option<String>> {
384        self.string_property("outerHTML").await
385    }
386
387    /// Returns the string property of the element.
388    ///
389    /// If the property is an empty String, `None` is returned.
390    pub async fn string_property(&self, property: impl AsRef<str>) -> Result<Option<String>> {
391        let property = property.as_ref();
392        let value = self.property(property).await?.ok_or(CdpError::NotFound)?;
393        let txt: String = serde_json::from_value(value)?;
394        if !txt.is_empty() {
395            Ok(Some(txt))
396        } else {
397            Ok(None)
398        }
399    }
400
401    /// Returns the javascript `property` of this element where `property` is
402    /// the name of the requested property of this element.
403    ///
404    /// See also `Element::inner_html`.
405    pub async fn property(&self, property: impl AsRef<str>) -> Result<Option<serde_json::Value>> {
406        let js_fn = format!("function() {{ return this.{}; }}", property.as_ref());
407        let resp = self.call_js_fn(js_fn, false).await?;
408        Ok(resp.result.value)
409    }
410
411    /// Returns a map with all `PropertyDescriptor`s of this element keyed by
412    /// their names
413    pub async fn properties(&self) -> Result<HashMap<String, PropertyDescriptor>> {
414        let mut params = GetPropertiesParams::new(self.remote_object_id.clone());
415        params.own_properties = Some(true);
416
417        let properties = self.tab.execute(params).await?;
418
419        Ok(properties
420            .result
421            .result
422            .into_iter()
423            .map(|p| (p.name.clone(), p))
424            .collect())
425    }
426
427    /// Scrolls the element into and takes a screenshot of it
428    pub async fn screenshot(&self, format: CaptureScreenshotFormat) -> Result<Vec<u8>> {
429        let mut bounding_box = self.scroll_into_view().await?.bounding_box().await?;
430        let viewport = self.tab.layout_metrics().await?.css_layout_viewport;
431
432        bounding_box.x += viewport.page_x as f64;
433        bounding_box.y += viewport.page_y as f64;
434
435        let clip = Viewport {
436            x: viewport.page_x as f64 + bounding_box.x,
437            y: viewport.page_y as f64 + bounding_box.y,
438            width: bounding_box.width,
439            height: bounding_box.height,
440            scale: 1.,
441        };
442
443        self.tab
444            .screenshot(
445                CaptureScreenshotParams::builder()
446                    .format(format)
447                    .clip(clip)
448                    .build(),
449            )
450            .await
451    }
452
453    /// Save a screenshot of the element and write it to `output`
454    pub async fn save_screenshot(
455        &self,
456        format: CaptureScreenshotFormat,
457        output: impl AsRef<Path>,
458    ) -> Result<Vec<u8>> {
459        let img = self.screenshot(format).await?;
460        utils::write(output.as_ref(), &img).await?;
461        Ok(img)
462    }
463}
464
465pub type AttributeValueFuture<'a> = Option<(
466    String,
467    Pin<Box<dyn Future<Output = Result<Option<String>>> + 'a>>,
468)>;
469
470/// Stream over all element's attributes
471#[must_use = "streams do nothing unless polled"]
472#[allow(missing_debug_implementations)]
473pub struct AttributeStream<'a> {
474    attributes: Vec<String>,
475    fut: AttributeValueFuture<'a>,
476    element: &'a Element,
477}
478
479impl<'a> Stream for AttributeStream<'a> {
480    type Item = (String, Result<Option<String>>);
481
482    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
483        let pin = self.get_mut();
484
485        if pin.fut.is_none() {
486            if let Some(name) = pin.attributes.pop() {
487                let fut = Box::pin(pin.element.attribute(name.clone()));
488                pin.fut = Some((name, fut));
489            } else {
490                return Poll::Ready(None);
491            }
492        }
493
494        if let Some((name, mut fut)) = pin.fut.take() {
495            if let Poll::Ready(res) = fut.poll_unpin(cx) {
496                return Poll::Ready(Some((name, res)));
497            } else {
498                pin.fut = Some((name, fut));
499            }
500        }
501        Poll::Pending
502    }
503}