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