headless_chrome/browser/tab/element/
mod.rs

1use std::fmt::Debug;
2use std::time::Duration;
3
4use anyhow::{Error, Result};
5
6use thiserror::Error;
7
8use log::{debug, error};
9
10use crate::browser::tab::NoElementFound;
11use crate::{browser::tab::point::Point, protocol::cdp::CSS::CSSComputedStyleProperty};
12
13mod box_model;
14
15use crate::util;
16pub use box_model::{BoxModel, ElementQuad};
17
18use crate::protocol::cdp::{Page, Runtime, CSS, DOM};
19
20#[derive(Debug, Error)]
21#[error("Couldnt get element quad")]
22pub struct NoQuadFound {}
23/// A handle to a [DOM Element](https://developer.mozilla.org/en-US/docs/Web/API/Element).
24///
25/// Typically you get access to these by passing `Tab.wait_for_element` a CSS selector. Once
26/// you have a handle to an element, you can click it, type into it, inspect its
27/// attributes, and more. You can even run a JavaScript function inside the tab which can reference
28/// the element via `this`.
29pub struct Element<'a> {
30    pub remote_object_id: String,
31    pub backend_node_id: DOM::NodeId,
32    pub node_id: DOM::NodeId,
33    pub parent: &'a super::Tab,
34    pub attributes: Option<Vec<String>>,
35    pub tag_name: String,
36    pub value: String,
37}
38
39impl Debug for Element<'_> {
40    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
41        write!(f, "Element {}", self.backend_node_id)?;
42        Ok(())
43    }
44}
45
46impl<'a> Element<'a> {
47    /// Using a 'node_id', of the type returned by QuerySelector and QuerySelectorAll, this finds
48    /// the 'backend_node_id' and 'remote_object_id' which are stable identifiers, unlike node_id.
49    /// We use these two when making various calls to the API because of that.
50    pub fn new(parent: &'a super::Tab, node_id: DOM::NodeId) -> Result<Self> {
51        if node_id == 0 {
52            return Err(NoElementFound {}.into());
53        }
54
55        let node = parent.describe_node(node_id).map_err(NoElementFound::map)?;
56
57        let attributes = node.attributes;
58        let tag_name = node.node_name;
59
60        let backend_node_id = node.backend_node_id;
61
62        let object = parent
63            .call_method(DOM::ResolveNode {
64                backend_node_id: Some(backend_node_id),
65                node_id: None,
66                object_group: None,
67                execution_context_id: None,
68            })?
69            .object;
70
71        let value = object.value.unwrap_or("".into()).to_string();
72        let remote_object_id = object.object_id.expect("couldn't find object ID");
73
74        Ok(Element {
75            remote_object_id,
76            backend_node_id,
77            node_id,
78            parent,
79            attributes,
80            tag_name,
81            value,
82        })
83    }
84
85    /// Returns the first element in the document which matches the given CSS selector.
86    ///
87    /// Equivalent to the following JS:
88    ///
89    /// ```js
90    /// document.querySelector(selector)
91    /// ```
92    ///
93    /// ```rust
94    /// # use anyhow::Result;
95    /// # // Awful hack to get access to testing utils common between integration, doctest, and unit tests
96    /// # mod server {
97    /// #     include!("../../../testing_utils/server.rs");
98    /// # }
99    /// # fn main() -> Result<()> {
100    /// #
101    /// use headless_chrome::Browser;
102    ///
103    /// let browser = Browser::default()?;
104    /// let initial_tab = browser.new_tab()?;
105    ///
106    /// let file_server = server::Server::with_dumb_html(include_str!("../../../../tests/simple.html"));
107    /// let containing_element = initial_tab.navigate_to(&file_server.url())?
108    ///     .wait_until_navigated()?
109    ///     .find_element("div#position-test")?;
110    /// let inner_element = containing_element.find_element("#strictly-above")?;
111    /// let attrs = inner_element.get_attributes()?.unwrap();
112    /// assert_eq!(attrs["id"], "strictly-above");
113    /// #
114    /// # Ok(())
115    /// # }
116    /// ```
117    pub fn find_element(&self, selector: &str) -> Result<Self> {
118        self.parent
119            .run_query_selector_on_node(self.node_id, selector)
120    }
121
122    pub fn find_element_by_xpath(&self, query: &str) -> Result<Element<'_>> {
123        self.parent.get_document()?;
124
125        self.parent
126            .call_method(DOM::PerformSearch {
127                query: query.to_string(),
128                include_user_agent_shadow_dom: Some(true),
129            })
130            .and_then(|o| {
131                Ok(self
132                    .parent
133                    .call_method(DOM::GetSearchResults {
134                        search_id: o.search_id,
135                        from_index: 0,
136                        to_index: o.result_count,
137                    })?
138                    .node_ids[0])
139            })
140            .and_then(|id| {
141                if id == 0 {
142                    Err(NoElementFound {}.into())
143                } else {
144                    Ok(Element::new(self.parent, id)?)
145                }
146            })
147    }
148
149    /// Returns the first element in the document which matches the given CSS selector.
150    ///
151    /// Equivalent to the following JS:
152    ///
153    /// ```js
154    /// document.querySelector(selector)
155    /// ```
156    ///
157    /// ```rust
158    /// # use anyhow::Result;
159    /// # // Awful hack to get access to testing utils common between integration, doctest, and unit tests
160    /// # mod server {
161    /// #     include!("../../../testing_utils/server.rs");
162    /// # }
163    /// # fn main() -> Result<()> {
164    /// #
165    /// use headless_chrome::Browser;
166    ///
167    /// let browser = Browser::default()?;
168    /// let initial_tab = browser.new_tab()?;
169    ///
170    /// let file_server = server::Server::with_dumb_html(include_str!("../../../../tests/simple.html"));
171    /// let containing_element = initial_tab.navigate_to(&file_server.url())?
172    ///     .wait_until_navigated()?
173    ///     .find_element("div#position-test")?;
174    /// let inner_divs = containing_element.find_elements("div")?;
175    /// assert_eq!(inner_divs.len(), 5);
176    /// #
177    /// # Ok(())
178    /// # }
179    /// ```
180    pub fn find_elements(&self, selector: &str) -> Result<Vec<Self>> {
181        self.parent
182            .run_query_selector_all_on_node(self.node_id, selector)
183    }
184
185    pub fn find_elements_by_xpath(&self, query: &str) -> Result<Vec<Element<'_>>> {
186        self.parent.get_document()?;
187        self.parent
188            .call_method(DOM::PerformSearch {
189                query: query.to_string(),
190                include_user_agent_shadow_dom: Some(true),
191            })
192            .and_then(|o| {
193                Ok(self
194                    .parent
195                    .call_method(DOM::GetSearchResults {
196                        search_id: o.search_id,
197                        from_index: 0,
198                        to_index: o.result_count,
199                    })?
200                    .node_ids)
201            })
202            .and_then(|ids| {
203                ids.iter()
204                    .filter(|id| **id != 0)
205                    .map(|id| Element::new(self.parent, *id))
206                    .collect()
207            })
208    }
209
210    pub fn wait_for_element(&self, selector: &str) -> Result<Element<'_>> {
211        self.wait_for_element_with_custom_timeout(selector, Duration::from_secs(3))
212    }
213
214    pub fn wait_for_xpath(&self, selector: &str) -> Result<Element<'_>> {
215        self.wait_for_xpath_with_custom_timeout(selector, Duration::from_secs(3))
216    }
217
218    pub fn wait_for_element_with_custom_timeout(
219        &self,
220        selector: &str,
221        timeout: std::time::Duration,
222    ) -> Result<Element<'_>> {
223        debug!("Waiting for element with selector: {:?}", selector);
224        util::Wait::with_timeout(timeout).strict_until(
225            || self.find_element(selector),
226            Error::downcast::<NoElementFound>,
227        )
228    }
229
230    pub fn wait_for_xpath_with_custom_timeout(
231        &self,
232        selector: &str,
233        timeout: std::time::Duration,
234    ) -> Result<Element<'_>> {
235        debug!("Waiting for element with selector: {:?}", selector);
236        util::Wait::with_timeout(timeout).strict_until(
237            || self.find_element_by_xpath(selector),
238            Error::downcast::<NoElementFound>,
239        )
240    }
241
242    pub fn wait_for_elements(&self, selector: &str) -> Result<Vec<Element<'_>>> {
243        debug!("Waiting for element with selector: {:?}", selector);
244        util::Wait::with_timeout(Duration::from_secs(3)).strict_until(
245            || self.find_elements(selector),
246            Error::downcast::<NoElementFound>,
247        )
248    }
249
250    pub fn wait_for_elements_by_xpath(&self, selector: &str) -> Result<Vec<Element<'_>>> {
251        debug!("Waiting for element with selector: {:?}", selector);
252        util::Wait::with_timeout(Duration::from_secs(3)).strict_until(
253            || self.find_elements_by_xpath(selector),
254            Error::downcast::<NoElementFound>,
255        )
256    }
257
258    /// Moves the mouse to the middle of this element
259    pub fn move_mouse_over(&self) -> Result<&Self> {
260        self.scroll_into_view()?;
261        let midpoint = self.get_midpoint()?;
262        self.parent.move_mouse_to_point(midpoint)?;
263        Ok(self)
264    }
265
266    pub fn click(&self) -> Result<&Self> {
267        self.scroll_into_view()?;
268        debug!("Clicking element {:?}", &self);
269        let midpoint = self.get_midpoint()?;
270        self.parent.click_point(midpoint)?;
271        Ok(self)
272    }
273
274    pub fn type_into(&self, text: &str) -> Result<&Self> {
275        self.click()?;
276
277        debug!("Typing into element ( {:?} ): {}", &self, text);
278
279        self.parent.type_str(text)?;
280
281        Ok(self)
282    }
283
284    pub fn call_js_fn(
285        &self,
286        function_declaration: &str,
287        args: Vec<serde_json::Value>,
288        await_promise: bool,
289    ) -> Result<Runtime::RemoteObject> {
290        let mut args = args;
291        let result = self
292            .parent
293            .call_method(Runtime::CallFunctionOn {
294                object_id: Some(self.remote_object_id.clone()),
295                function_declaration: function_declaration.to_string(),
296                arguments: args
297                    .iter_mut()
298                    .map(|v| {
299                        Some(Runtime::CallArgument {
300                            value: Some(v.take()),
301                            unserializable_value: None,
302                            object_id: None,
303                        })
304                    })
305                    .collect(),
306                return_by_value: Some(false),
307                generate_preview: Some(true),
308                silent: Some(false),
309                await_promise: Some(await_promise),
310                user_gesture: None,
311                execution_context_id: None,
312                object_group: None,
313                throw_on_side_effect: None,
314                serialization_options: None,
315                unique_context_id: None,
316            })?
317            .result;
318
319        Ok(result)
320    }
321
322    pub fn focus(&self) -> Result<&Self> {
323        self.scroll_into_view()?;
324        self.parent.call_method(DOM::Focus {
325            backend_node_id: Some(self.backend_node_id),
326            node_id: None,
327            object_id: None,
328        })?;
329        Ok(self)
330    }
331
332    /// Returns the inner text of an HTML Element. Returns an empty string on elements with no text.
333    ///
334    /// Note: .innerText and .textContent are not the same thing. See:
335    /// <https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText>
336    ///
337    /// Note: if you somehow call this on a node that's not an HTML Element (e.g. `document`), this
338    /// will fail.
339    /// ```rust
340    /// # use anyhow::Result;
341    /// # fn main() -> Result<()> {
342    /// #
343    /// use headless_chrome::Browser;
344    /// use std::time::Duration;
345    /// let browser = Browser::default()?;
346    /// let url = "https://web.archive.org/web/20190403224553/https://en.wikipedia.org/wiki/JavaScript";
347    /// let inner_text_content = browser.new_tab()?
348    ///     .navigate_to(url)?
349    ///     .wait_for_element_with_custom_timeout("#Misplaced_trust_in_developers", Duration::from_secs(10))?
350    ///     .get_inner_text()?;
351    /// assert_eq!(inner_text_content, "Misplaced trust in developers");
352    /// #
353    /// # Ok(())
354    /// # }
355    /// ```
356    pub fn get_inner_text(&self) -> Result<String> {
357        let text: String = serde_json::from_value(
358            self.call_js_fn("function() { return this.innerText }", vec![], false)?
359                .value
360                .unwrap(),
361        )?;
362        Ok(text)
363    }
364
365    /// Get the full HTML contents of the element.
366    ///
367    /// Equivalent to the following JS: ```element.outerHTML```.
368    pub fn get_content(&self) -> Result<String> {
369        let html = self
370            .call_js_fn("function() { return this.outerHTML }", vec![], false)?
371            .value
372            .unwrap();
373
374        Ok(String::from(html.as_str().unwrap()))
375    }
376
377    pub fn get_computed_styles(&self) -> Result<Vec<CSSComputedStyleProperty>> {
378        let styles = self
379            .parent
380            .call_method(CSS::GetComputedStyleForNode {
381                node_id: self.node_id,
382            })?
383            .computed_style;
384
385        Ok(styles)
386    }
387
388    pub fn get_description(&self) -> Result<DOM::Node> {
389        let node = self
390            .parent
391            .call_method(DOM::DescribeNode {
392                node_id: None,
393                backend_node_id: Some(self.backend_node_id),
394                depth: Some(100),
395                object_id: None,
396                pierce: None,
397            })?
398            .node;
399        Ok(node)
400    }
401
402    /// Capture a screenshot of this element.
403    ///
404    /// The screenshot is taken from the surface using this element's content-box.
405    ///
406    /// ```rust,no_run
407    /// # use anyhow::Result;
408    /// # fn main() -> Result<()> {
409    /// #
410    /// use headless_chrome::{protocol::page::ScreenshotFormat, Browser};
411    /// let browser = Browser::default()?;
412    /// let png_data = browser.new_tab()?
413    ///     .navigate_to("https://en.wikipedia.org/wiki/WebKit")?
414    ///     .wait_for_element("#mw-content-text > div > table.infobox.vevent")?
415    ///     .capture_screenshot(ScreenshotFormat::PNG)?;
416    /// #
417    /// # Ok(())
418    /// # }
419    /// ```
420    pub fn capture_screenshot(
421        &self,
422        format: Page::CaptureScreenshotFormatOption,
423    ) -> Result<Vec<u8>> {
424        self.scroll_into_view()?;
425        self.parent.capture_screenshot(
426            format,
427            Some(90),
428            Some(self.get_box_model()?.content_viewport()),
429            true,
430        )
431    }
432
433    pub fn set_input_files(&self, file_paths: &[&str]) -> Result<&Self> {
434        self.parent.call_method(DOM::SetFileInputFiles {
435            files: file_paths
436                .to_vec()
437                .iter()
438                .map(std::string::ToString::to_string)
439                .collect(),
440            backend_node_id: Some(self.backend_node_id),
441            node_id: None,
442            object_id: None,
443        })?;
444        Ok(self)
445    }
446
447    /// Scrolls the current element into view
448    ///
449    /// Used prior to any action applied to the current element to ensure action is duable.
450    pub fn scroll_into_view(&self) -> Result<&Self> {
451        let result = self.call_js_fn(
452            "async function() {
453                if (!this.isConnected)
454                    return 'Node is detached from document';
455                if (this.nodeType !== Node.ELEMENT_NODE)
456                    return 'Node is not of type HTMLElement';
457
458                const visibleRatio = await new Promise(resolve => {
459                    const observer = new IntersectionObserver(entries => {
460                        resolve(entries[0].intersectionRatio);
461                        observer.disconnect();
462                    });
463                    observer.observe(this);
464                });
465
466                if (visibleRatio !== 1.0)
467                    this.scrollIntoView({
468                        block: 'center',
469                        inline: 'center',
470                        behavior: 'instant'
471                    });
472                return false;
473            }",
474            vec![],
475            true,
476        )?;
477
478        if result.Type == Runtime::RemoteObjectType::String {
479            let error_text = result.value.unwrap().as_str().unwrap().to_string();
480            return Err(ScrollFailed { error_text }.into());
481        }
482
483        Ok(self)
484    }
485
486    pub fn get_attributes(&self) -> Result<Option<Vec<String>>> {
487        let description = self.get_description()?;
488        Ok(description.attributes)
489    }
490
491    pub fn get_attribute_value(&self, attribute_name: &str) -> Result<Option<String>> {
492        let js_fn = format!("function() {{ return this.getAttribute('{attribute_name}'); }}");
493
494        Ok(
495            if let Some(attribute_value) = self.call_js_fn(&js_fn, Vec::new(), true)?.value {
496                Some(serde_json::from_value(attribute_value)?)
497            } else {
498                None
499            },
500        )
501    }
502
503    /// Get boxes for this element
504    pub fn get_box_model(&self) -> Result<BoxModel> {
505        let model = self
506            .parent
507            .call_method(DOM::GetBoxModel {
508                node_id: None,
509                backend_node_id: Some(self.backend_node_id),
510                object_id: None,
511            })?
512            .model;
513        Ok(BoxModel {
514            content: ElementQuad::from_raw_points(&model.content),
515            padding: ElementQuad::from_raw_points(&model.padding),
516            border: ElementQuad::from_raw_points(&model.border),
517            margin: ElementQuad::from_raw_points(&model.margin),
518            width: model.width as f64,
519            height: model.height as f64,
520        })
521    }
522
523    pub fn get_midpoint(&self) -> Result<Point> {
524        if let Ok(e) = self
525            .parent
526            .call_method(DOM::GetContentQuads {
527                node_id: None,
528                backend_node_id: Some(self.backend_node_id),
529                object_id: None,
530            })
531            .and_then(|quad| {
532                quad.quads
533                    .first()
534                    .map(|raw_quad| ElementQuad::from_raw_points(raw_quad))
535                    .map(|input_quad| (input_quad.bottom_right + input_quad.top_left) / 2.0)
536                    .ok_or_else(|| {
537                        anyhow::anyhow!(
538                            "tried to get the midpoint of an element which is not visible"
539                        )
540                    })
541            })
542        {
543            return Ok(e);
544        }
545        // let mut p = Point { x: 0.0, y: 0.0 }; FIX FOR CLIPPY `value assigned to `p` is never read`
546        let p = util::Wait::with_timeout(Duration::from_secs(20)).until(|| {
547            let r = self
548                .call_js_fn(
549                    r"
550                    function() {
551                        let rect = this.getBoundingClientRect();
552
553                        if(rect.x != 0) {
554                            this.scrollIntoView();
555                        }
556
557                        return this.getBoundingClientRect();
558                    }
559                    ",
560                    vec![],
561                    false,
562                )
563                .unwrap();
564
565            let res = util::extract_midpoint(r);
566
567            match res {
568                Ok(v) => {
569                    if v.x == 0.0 {
570                        None
571                    } else {
572                        Some(v)
573                    }
574                }
575                _ => None,
576            }
577        })?;
578
579        Ok(p)
580    }
581
582    pub fn get_js_midpoint(&self) -> Result<Point> {
583        let result = self.call_js_fn(
584            "function(){return this.getBoundingClientRect(); }",
585            vec![],
586            false,
587        )?;
588
589        util::extract_midpoint(result)
590    }
591}
592
593#[derive(Debug, Error)]
594#[error("Scrolling element into view failed: {}", error_text)]
595struct ScrollFailed {
596    error_text: String,
597}