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