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#[derive(Debug)]
28pub struct Element {
29 pub remote_object_id: RemoteObjectId,
31 pub backend_node_id: BackendNodeId,
33 pub node_id: NodeId,
35 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 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 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 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 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 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 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 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 pub async fn focus(&self) -> Result<&Self> {
214 self.call_js_fn("function() { this.focus(); }", true)
215 .await?;
216 Ok(self)
217 }
218
219 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 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 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 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 pub async fn type_str_with_modifier(
294 &self,
295 input: impl AsRef<str>,
296 modifiers: i64,
297 ) -> Result<&Self> {
298 self.tab
299 .type_str_with_modifier(input, Some(modifiers))
300 .await?;
301 Ok(self)
302 }
303
304 pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
319 self.tab.press_key(key).await?;
320 Ok(self)
321 }
322
323 pub async fn description(&self) -> Result<Node> {
325 Ok(self
326 .tab
327 .execute(
328 DescribeNodeParams::builder()
329 .backend_node_id(self.backend_node_id)
330 .depth(100)
331 .build(),
332 )
333 .await?
334 .result
335 .node)
336 }
337
338 pub async fn attributes(&self) -> Result<Vec<String>> {
341 let node = self.description().await?;
342 Ok(node.attributes.unwrap_or_default())
343 }
344
345 pub async fn attribute(&self, attribute: impl AsRef<str>) -> Result<Option<String>> {
347 let js_fn = format!(
348 "function() {{ return this.getAttribute('{}'); }}",
349 attribute.as_ref()
350 );
351 let resp = self.call_js_fn(js_fn, false).await?;
352 if let Some(value) = resp.result.value {
353 Ok(serde_json::from_value(value)?)
354 } else {
355 Ok(None)
356 }
357 }
358
359 pub async fn iter_attributes(
361 &self,
362 ) -> Result<impl Stream<Item = (String, Result<Option<String>>)> + '_> {
363 let attributes = self.attributes().await?;
364 Ok(AttributeStream {
365 attributes,
366 fut: None,
367 element: self,
368 })
369 }
370
371 pub async fn inner_text(&self) -> Result<Option<String>> {
373 self.string_property("innerText").await
374 }
375
376 pub async fn inner_html(&self) -> Result<Option<String>> {
378 self.string_property("innerHTML").await
379 }
380
381 pub async fn outer_html(&self) -> Result<Option<String>> {
383 self.string_property("outerHTML").await
384 }
385
386 pub async fn string_property(&self, property: impl AsRef<str>) -> Result<Option<String>> {
390 let property = property.as_ref();
391 let value = self.property(property).await?.ok_or(CdpError::NotFound)?;
392 let txt: String = serde_json::from_value(value)?;
393 if !txt.is_empty() {
394 Ok(Some(txt))
395 } else {
396 Ok(None)
397 }
398 }
399
400 pub async fn property(&self, property: impl AsRef<str>) -> Result<Option<serde_json::Value>> {
405 let js_fn = format!("function() {{ return this.{}; }}", property.as_ref());
406 let resp = self.call_js_fn(js_fn, false).await?;
407 Ok(resp.result.value)
408 }
409
410 pub async fn properties(&self) -> Result<HashMap<String, PropertyDescriptor>> {
413 let mut params = GetPropertiesParams::new(self.remote_object_id.clone());
414 params.own_properties = Some(true);
415
416 let properties = self.tab.execute(params).await?;
417
418 Ok(properties
419 .result
420 .result
421 .into_iter()
422 .map(|p| (p.name.clone(), p))
423 .collect())
424 }
425
426 pub async fn screenshot(&self, format: CaptureScreenshotFormat) -> Result<Vec<u8>> {
428 let mut bounding_box = self.scroll_into_view().await?.bounding_box().await?;
429 let viewport = self.tab.layout_metrics().await?.css_layout_viewport;
430
431 bounding_box.x += viewport.page_x as f64;
432 bounding_box.y += viewport.page_y as f64;
433
434 let clip = Viewport {
435 x: viewport.page_x as f64 + bounding_box.x,
436 y: viewport.page_y as f64 + bounding_box.y,
437 width: bounding_box.width,
438 height: bounding_box.height,
439 scale: 1.,
440 };
441
442 self.tab
443 .screenshot(
444 CaptureScreenshotParams::builder()
445 .format(format)
446 .clip(clip)
447 .build(),
448 )
449 .await
450 }
451
452 pub async fn save_screenshot(
454 &self,
455 format: CaptureScreenshotFormat,
456 output: impl AsRef<Path>,
457 ) -> Result<Vec<u8>> {
458 let img = self.screenshot(format).await?;
459 utils::write(output.as_ref(), &img).await?;
460 Ok(img)
461 }
462}
463
464pub type AttributeValueFuture<'a> = Option<(
465 String,
466 Pin<Box<dyn Future<Output = Result<Option<String>>> + 'a>>,
467)>;
468
469#[must_use = "streams do nothing unless polled"]
471#[allow(missing_debug_implementations)]
472pub struct AttributeStream<'a> {
473 attributes: Vec<String>,
474 fut: AttributeValueFuture<'a>,
475 element: &'a Element,
476}
477
478impl<'a> Stream for AttributeStream<'a> {
479 type Item = (String, Result<Option<String>>);
480
481 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
482 let pin = self.get_mut();
483
484 if pin.fut.is_none() {
485 if let Some(name) = pin.attributes.pop() {
486 let fut = Box::pin(pin.element.attribute(name.clone()));
487 pin.fut = Some((name, fut));
488 } else {
489 return Poll::Ready(None);
490 }
491 }
492
493 if let Some((name, mut fut)) = pin.fut.take() {
494 if let Poll::Ready(res) = fut.poll_unpin(cx) {
495 return Poll::Ready(Some((name, res)));
496 } else {
497 pin.fut = Some((name, fut));
498 }
499 }
500 Poll::Pending
501 }
502}