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