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>,
36}
37
38impl Element {
39 pub(crate) async fn new(tab: Arc<PageInner>, node_id: NodeId) -> Result<Self> {
40 let backend_node_id = tab
41 .execute(
42 DescribeNodeParams::builder()
43 .node_id(node_id)
44 .depth(-1)
45 .pierce(true)
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
66 Ok(Self {
67 remote_object_id,
68 backend_node_id,
69 node_id,
70 tab,
71 })
72 }
73
74 pub(crate) async fn from_nodes(tab: &Arc<PageInner>, node_ids: &[NodeId]) -> Result<Vec<Self>> {
76 future::join_all(
77 node_ids
78 .iter()
79 .copied()
80 .map(|id| Element::new(Arc::clone(tab), id)),
81 )
82 .await
83 .into_iter()
84 .collect::<Result<Vec<_>, _>>()
85 }
86
87 pub async fn find_element(&self, selector: impl Into<String>) -> Result<Self> {
90 let node_id = self.tab.find_element(selector, self.node_id).await?;
91 Element::new(Arc::clone(&self.tab), node_id).await
92 }
93
94 pub async fn find_elements(&self, selector: impl Into<String>) -> Result<Vec<Element>> {
96 Element::from_nodes(
97 &self.tab,
98 &self.tab.find_elements(selector, self.node_id).await?,
99 )
100 .await
101 }
102
103 async fn box_model(&self) -> Result<BoxModel> {
104 let model = self
105 .tab
106 .execute(
107 GetBoxModelParams::builder()
108 .backend_node_id(self.backend_node_id)
109 .build(),
110 )
111 .await?
112 .result
113 .model;
114 Ok(BoxModel {
115 content: ElementQuad::from_quad(&model.content),
116 padding: ElementQuad::from_quad(&model.padding),
117 border: ElementQuad::from_quad(&model.border),
118 margin: ElementQuad::from_quad(&model.margin),
119 width: model.width as u32,
120 height: model.height as u32,
121 })
122 }
123
124 pub async fn bounding_box(&self) -> Result<BoundingBox> {
126 let bounds = self.box_model().await?;
127 let quad = bounds.border;
128
129 let x = quad.most_left();
130 let y = quad.most_top();
131 let width = quad.most_right() - x;
132 let height = quad.most_bottom() - y;
133
134 Ok(BoundingBox {
135 x,
136 y,
137 width,
138 height,
139 })
140 }
141
142 pub async fn clickable_point(&self) -> Result<Point> {
144 let content_quads = self
145 .tab
146 .execute(
147 GetContentQuadsParams::builder()
148 .backend_node_id(self.backend_node_id)
149 .build(),
150 )
151 .await?;
152 content_quads
153 .quads
154 .iter()
155 .filter(|q| q.inner().len() == 8)
156 .map(ElementQuad::from_quad)
157 .filter(|q| q.quad_area() > 1.)
158 .map(|q| q.quad_center())
159 .next()
160 .ok_or_else(|| CdpError::msg("Node is either not visible or not an HTMLElement"))
161 }
162
163 pub async fn call_js_fn(
190 &self,
191 function_declaration: impl Into<String>,
192 await_promise: bool,
193 ) -> Result<CallFunctionOnReturns> {
194 self.tab
195 .call_js_fn(
196 function_declaration,
197 await_promise,
198 self.remote_object_id.clone(),
199 )
200 .await
201 }
202
203 pub async fn json_value(&self) -> Result<serde_json::Value> {
205 let element_json = self
206 .call_js_fn("function() { return this; }", false)
207 .await?;
208 element_json.result.value.ok_or(CdpError::NotFound)
209 }
210
211 pub async fn focus(&self) -> Result<&Self> {
213 self.call_js_fn("function() { this.focus(); }", true)
214 .await?;
215 Ok(self)
216 }
217
218 pub async fn hover(&self) -> Result<&Self> {
221 self.scroll_into_view().await?;
222 self.tab.move_mouse(self.clickable_point().await?).await?;
223 Ok(self)
224 }
225
226 pub async fn scroll_into_view(&self) -> Result<&Self> {
231 let resp = self
232 .call_js_fn(
233 "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}",
234 true,
235 )
236 .await?;
237
238 if resp.result.r#type == RemoteObjectType::String {
239 let error_text = resp
240 .result
241 .value
242 .unwrap_or_default()
243 .as_str()
244 .unwrap_or_default()
245 .to_string();
246 return Err(CdpError::ScrollingFailed(error_text));
247 }
248 Ok(self)
249 }
250
251 pub async fn click(&self) -> Result<&Self> {
256 let center = self.scroll_into_view().await?.clickable_point().await?;
257 self.tab.click(center).await?;
258 Ok(self)
259 }
260
261 pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
275 self.tab.type_str(input).await?;
276 Ok(self)
277 }
278
279 pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
294 self.tab.press_key(key).await?;
295 Ok(self)
296 }
297
298 pub async fn description(&self) -> Result<Node> {
300 Ok(self
301 .tab
302 .execute(
303 DescribeNodeParams::builder()
304 .backend_node_id(self.backend_node_id)
305 .depth(100)
306 .build(),
307 )
308 .await?
309 .result
310 .node)
311 }
312
313 pub async fn attributes(&self) -> Result<Vec<String>> {
316 let node = self.description().await?;
317 Ok(node.attributes.unwrap_or_default())
318 }
319
320 pub async fn attribute(&self, attribute: impl AsRef<str>) -> Result<Option<String>> {
322 let js_fn = format!(
323 "function() {{ return this.getAttribute('{}'); }}",
324 attribute.as_ref()
325 );
326 let resp = self.call_js_fn(js_fn, false).await?;
327 if let Some(value) = resp.result.value {
328 Ok(serde_json::from_value(value)?)
329 } else {
330 Ok(None)
331 }
332 }
333
334 pub async fn iter_attributes(
336 &self,
337 ) -> Result<impl Stream<Item = (String, Result<Option<String>>)> + '_> {
338 let attributes = self.attributes().await?;
339 Ok(AttributeStream {
340 attributes,
341 fut: None,
342 element: self,
343 })
344 }
345
346 pub async fn inner_text(&self) -> Result<Option<String>> {
348 self.string_property("innerText").await
349 }
350
351 pub async fn inner_html(&self) -> Result<Option<String>> {
353 self.string_property("innerHTML").await
354 }
355
356 pub async fn outer_html(&self) -> Result<Option<String>> {
358 self.string_property("outerHTML").await
359 }
360
361 pub async fn string_property(&self, property: impl AsRef<str>) -> Result<Option<String>> {
365 let property = property.as_ref();
366 let value = self.property(property).await?.ok_or(CdpError::NotFound)?;
367 let txt: String = serde_json::from_value(value)?;
368 if !txt.is_empty() {
369 Ok(Some(txt))
370 } else {
371 Ok(None)
372 }
373 }
374
375 pub async fn property(&self, property: impl AsRef<str>) -> Result<Option<serde_json::Value>> {
380 let js_fn = format!("function() {{ return this.{}; }}", property.as_ref());
381 let resp = self.call_js_fn(js_fn, false).await?;
382 Ok(resp.result.value)
383 }
384
385 pub async fn properties(&self) -> Result<HashMap<String, PropertyDescriptor>> {
388 let mut params = GetPropertiesParams::new(self.remote_object_id.clone());
389 params.own_properties = Some(true);
390
391 let properties = self.tab.execute(params).await?;
392
393 Ok(properties
394 .result
395 .result
396 .into_iter()
397 .map(|p| (p.name.clone(), p))
398 .collect())
399 }
400
401 pub async fn screenshot(&self, format: CaptureScreenshotFormat) -> Result<Vec<u8>> {
403 let mut bounding_box = self.scroll_into_view().await?.bounding_box().await?;
404 let viewport = self.tab.layout_metrics().await?.css_layout_viewport;
405
406 bounding_box.x += viewport.page_x as f64;
407 bounding_box.y += viewport.page_y as f64;
408
409 let clip = Viewport {
410 x: viewport.page_x as f64 + bounding_box.x,
411 y: viewport.page_y as f64 + bounding_box.y,
412 width: bounding_box.width,
413 height: bounding_box.height,
414 scale: 1.,
415 };
416
417 self.tab
418 .screenshot(
419 CaptureScreenshotParams::builder()
420 .format(format)
421 .clip(clip)
422 .build(),
423 )
424 .await
425 }
426
427 pub async fn save_screenshot(
429 &self,
430 format: CaptureScreenshotFormat,
431 output: impl AsRef<Path>,
432 ) -> Result<Vec<u8>> {
433 let img = self.screenshot(format).await?;
434 utils::write(output.as_ref(), &img).await?;
435 Ok(img)
436 }
437}
438
439pub type AttributeValueFuture<'a> = Option<(
440 String,
441 Pin<Box<dyn Future<Output = Result<Option<String>>> + 'a>>,
442)>;
443
444#[must_use = "streams do nothing unless polled"]
446#[allow(missing_debug_implementations)]
447pub struct AttributeStream<'a> {
448 attributes: Vec<String>,
449 fut: AttributeValueFuture<'a>,
450 element: &'a Element,
451}
452
453impl<'a> Stream for AttributeStream<'a> {
454 type Item = (String, Result<Option<String>>);
455
456 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
457 let pin = self.get_mut();
458
459 if pin.fut.is_none() {
460 if let Some(name) = pin.attributes.pop() {
461 let fut = Box::pin(pin.element.attribute(name.clone()));
462 pin.fut = Some((name, fut));
463 } else {
464 return Poll::Ready(None);
465 }
466 }
467
468 if let Some((name, mut fut)) = pin.fut.take() {
469 if let Poll::Ready(res) = fut.poll_unpin(cx) {
470 return Poll::Ready(Some((name, res)));
471 } else {
472 pin.fut = Some((name, fut));
473 }
474 }
475 Poll::Pending
476 }
477}