Skip to main content

chaser_oxide/
element.rs

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