1use hashbrown::HashMap;
2use std::path::Path;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6
7use futures_util::{future, FutureExt, Stream};
8use std::future::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#[derive(Debug)]
29pub struct Element {
30 pub remote_object_id: RemoteObjectId,
32 pub backend_node_id: BackendNodeId,
34 pub node_id: NodeId,
36 tab: Arc<PageInner>,
38}
39
40impl Element {
41 pub(crate) async fn new(tab: Arc<PageInner>, node_id: NodeId) -> Result<Self> {
42 let backend_node_id = tab
43 .execute(
44 DescribeNodeParams::builder()
45 .node_id(node_id)
46 .depth(-1)
47 .pierce(true)
48 .build(),
49 )
50 .await?
51 .node
52 .backend_node_id;
53
54 let resp = tab
55 .execute(
56 ResolveNodeParams::builder()
57 .backend_node_id(backend_node_id)
58 .build(),
59 )
60 .await?;
61
62 let remote_object_id = resp
63 .result
64 .object
65 .object_id
66 .ok_or_else(|| CdpError::msg(format!("No object Id found for {node_id:?}")))?;
67
68 Ok(Self {
69 remote_object_id,
70 backend_node_id,
71 node_id,
72 tab,
73 })
74 }
75
76 pub(crate) async fn from_nodes(tab: &Arc<PageInner>, node_ids: &[NodeId]) -> Result<Vec<Self>> {
78 future::join_all(
79 node_ids
80 .iter()
81 .copied()
82 .map(|id| Element::new(Arc::clone(tab), id)),
83 )
84 .await
85 .into_iter()
86 .collect::<Result<Vec<_>, _>>()
87 }
88
89 pub async fn find_element(&self, selector: impl Into<String>) -> Result<Self> {
92 let node_id = self.tab.find_element(selector, self.node_id).await?;
93 Element::new(Arc::clone(&self.tab), node_id).await
94 }
95
96 pub async fn find_elements(&self, selector: impl Into<String>) -> Result<Vec<Element>> {
98 Element::from_nodes(
99 &self.tab,
100 &self.tab.find_elements(selector, self.node_id).await?,
101 )
102 .await
103 }
104
105 async fn box_model(&self) -> Result<BoxModel> {
106 let model = self
107 .tab
108 .execute(
109 GetBoxModelParams::builder()
110 .backend_node_id(self.backend_node_id)
111 .build(),
112 )
113 .await?
114 .result
115 .model;
116 Ok(BoxModel {
117 content: ElementQuad::from_quad(&model.content),
118 padding: ElementQuad::from_quad(&model.padding),
119 border: ElementQuad::from_quad(&model.border),
120 margin: ElementQuad::from_quad(&model.margin),
121 width: model.width as u32,
122 height: model.height as u32,
123 })
124 }
125
126 pub async fn bounding_box(&self) -> Result<BoundingBox> {
128 let bounds = self.box_model().await?;
129 let quad = bounds.border;
130
131 let x = quad.most_left();
132 let y = quad.most_top();
133 let width = quad.most_right() - x;
134 let height = quad.most_bottom() - y;
135
136 Ok(BoundingBox {
137 x,
138 y,
139 width,
140 height,
141 })
142 }
143
144 pub async fn clickable_point(&self) -> Result<Point> {
146 let content_quads = self
147 .tab
148 .execute(
149 GetContentQuadsParams::builder()
150 .backend_node_id(self.backend_node_id)
151 .build(),
152 )
153 .await?;
154 content_quads
155 .quads
156 .iter()
157 .filter(|q| q.inner().len() == 8)
158 .map(ElementQuad::from_quad)
159 .filter(|q| q.quad_area() > 1.)
160 .map(|q| q.quad_center())
161 .next()
162 .ok_or_else(|| CdpError::msg("Node is either not visible or not an HTMLElement"))
163 }
164
165 pub async fn call_js_fn(
192 &self,
193 function_declaration: impl Into<String>,
194 await_promise: bool,
195 ) -> Result<CallFunctionOnReturns> {
196 self.tab
197 .call_js_fn(
198 function_declaration,
199 await_promise,
200 self.remote_object_id.clone(),
201 )
202 .await
203 }
204
205 pub async fn json_value(&self) -> Result<serde_json::Value> {
207 let element_json = self
208 .call_js_fn("function() { return this; }", false)
209 .await?;
210 element_json.result.value.ok_or(CdpError::NotFound)
211 }
212
213 pub async fn focus(&self) -> Result<&Self> {
215 self.call_js_fn("function() { this.focus(); }", true)
216 .await?;
217 Ok(self)
218 }
219
220 pub async fn hover(&self) -> Result<&Self> {
223 self.scroll_into_view().await?;
224 self.tab.move_mouse(self.clickable_point().await?).await?;
225 Ok(self)
226 }
227
228 pub async fn scroll_into_view(&self) -> Result<&Self> {
233 let resp = self
234 .call_js_fn(
235 "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}",
236 true,
237 )
238 .await?;
239
240 if resp.result.r#type == RemoteObjectType::String {
241 let error_text = resp
242 .result
243 .value
244 .unwrap_or_default()
245 .as_str()
246 .unwrap_or_default()
247 .to_string();
248 return Err(CdpError::ScrollingFailed(error_text));
249 }
250 Ok(self)
251 }
252
253 pub async fn click(&self) -> Result<&Self> {
258 let center = self.scroll_into_view().await?.clickable_point().await?;
259 self.tab.click(center).await?;
260 Ok(self)
261 }
262
263 pub async fn click_smooth(&self) -> Result<&Self> {
274 let center = self.scroll_into_view().await?.clickable_point().await?;
275 self.tab.click_smooth(center).await?;
276 Ok(self)
277 }
278
279 pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
293 self.tab.type_str(input).await?;
294 Ok(self)
295 }
296
297 pub async fn type_str_with_modifier(
311 &self,
312 input: impl AsRef<str>,
313 modifiers: i64,
314 ) -> Result<&Self> {
315 self.tab
316 .type_str_with_modifier(input, Some(modifiers))
317 .await?;
318 Ok(self)
319 }
320
321 pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
336 self.tab.press_key(key).await?;
337 Ok(self)
338 }
339
340 pub async fn description(&self) -> Result<Node> {
342 Ok(self
343 .tab
344 .execute(
345 DescribeNodeParams::builder()
346 .backend_node_id(self.backend_node_id)
347 .depth(100)
348 .build(),
349 )
350 .await?
351 .result
352 .node)
353 }
354
355 pub async fn attributes(&self) -> Result<Vec<String>> {
358 let node = self.description().await?;
359 Ok(node.attributes.unwrap_or_default())
360 }
361
362 pub async fn attribute(&self, attribute: impl AsRef<str>) -> Result<Option<String>> {
364 let js_fn = format!(
365 "function() {{ return this.getAttribute('{}'); }}",
366 attribute.as_ref()
367 );
368 let resp = self.call_js_fn(js_fn, false).await?;
369 if let Some(value) = resp.result.value {
370 Ok(serde_json::from_value(value)?)
371 } else {
372 Ok(None)
373 }
374 }
375
376 pub async fn iter_attributes(
378 &self,
379 ) -> Result<impl Stream<Item = (String, Result<Option<String>>)> + '_> {
380 let attributes = self.attributes().await?;
381 Ok(AttributeStream {
382 attributes,
383 fut: None,
384 element: self,
385 })
386 }
387
388 pub async fn inner_text(&self) -> Result<Option<String>> {
390 self.string_property("innerText").await
391 }
392
393 pub async fn inner_html(&self) -> Result<Option<String>> {
395 self.string_property("innerHTML").await
396 }
397
398 pub async fn outer_html(&self) -> Result<Option<String>> {
400 self.string_property("outerHTML").await
401 }
402
403 pub async fn string_property(&self, property: impl AsRef<str>) -> Result<Option<String>> {
407 let property = property.as_ref();
408 let value = self.property(property).await?.ok_or(CdpError::NotFound)?;
409 let txt: String = serde_json::from_value(value)?;
410 if !txt.is_empty() {
411 Ok(Some(txt))
412 } else {
413 Ok(None)
414 }
415 }
416
417 pub async fn property(&self, property: impl AsRef<str>) -> Result<Option<serde_json::Value>> {
422 let js_fn = format!("function() {{ return this.{}; }}", property.as_ref());
423 let resp = self.call_js_fn(js_fn, false).await?;
424 Ok(resp.result.value)
425 }
426
427 pub async fn properties(&self) -> Result<HashMap<String, PropertyDescriptor>> {
430 let mut params = GetPropertiesParams::new(self.remote_object_id.clone());
431 params.own_properties = Some(true);
432
433 let properties = self.tab.execute(params).await?;
434
435 Ok(properties
436 .result
437 .result
438 .into_iter()
439 .map(|p| (p.name.clone(), p))
440 .collect())
441 }
442
443 pub async fn screenshot(&self, format: CaptureScreenshotFormat) -> Result<Vec<u8>> {
445 let mut bounding_box = self.scroll_into_view().await?.bounding_box().await?;
446 let viewport = self.tab.layout_metrics().await?.css_layout_viewport;
447
448 bounding_box.x += viewport.page_x as f64;
449 bounding_box.y += viewport.page_y as f64;
450
451 let clip = Viewport {
452 x: viewport.page_x as f64 + bounding_box.x,
453 y: viewport.page_y as f64 + bounding_box.y,
454 width: bounding_box.width,
455 height: bounding_box.height,
456 scale: 1.,
457 };
458
459 self.tab
460 .screenshot(
461 CaptureScreenshotParams::builder()
462 .format(format)
463 .clip(clip)
464 .build(),
465 )
466 .await
467 }
468
469 pub async fn save_screenshot(
471 &self,
472 format: CaptureScreenshotFormat,
473 output: impl AsRef<Path>,
474 ) -> Result<Vec<u8>> {
475 let img = self.screenshot(format).await?;
476 utils::write(output.as_ref(), &img).await?;
477 Ok(img)
478 }
479}
480
481pub type AttributeValueFuture<'a> = Option<(
482 String,
483 Pin<Box<dyn Future<Output = Result<Option<String>>> + 'a>>,
484)>;
485
486#[must_use = "streams do nothing unless polled"]
488#[allow(missing_debug_implementations)]
489pub struct AttributeStream<'a> {
490 attributes: Vec<String>,
491 fut: AttributeValueFuture<'a>,
492 element: &'a Element,
493}
494
495impl<'a> Stream for AttributeStream<'a> {
496 type Item = (String, Result<Option<String>>);
497
498 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
499 let pin = self.get_mut();
500
501 if pin.fut.is_none() {
502 if let Some(name) = pin.attributes.pop() {
503 let fut = Box::pin(pin.element.attribute(name.clone()));
504 pin.fut = Some((name, fut));
505 } else {
506 return Poll::Ready(None);
507 }
508 }
509
510 if let Some((name, mut fut)) = pin.fut.take() {
511 if let Poll::Ready(res) = fut.poll_unpin(cx) {
512 return Poll::Ready(Some((name, res)));
513 } else {
514 pin.fut = Some((name, fut));
515 }
516 }
517 Poll::Pending
518 }
519}