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