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