headless_chrome/browser/tab/element/
mod.rs1use 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 {}
23pub 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 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 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 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 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 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 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 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 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 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 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}