1use std::collections::HashMap;
2use std::path::Path;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6
7use chromiumoxide_types::ClickOptions;
8use futures::{Future, FutureExt, Stream, 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, Clone)]
29pub struct Element {
30 pub remote_object_id: RemoteObjectId,
32 pub backend_node_id: BackendNodeId,
34 pub node_id: NodeId,
36 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(100)
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 Ok(Self {
66 remote_object_id,
67 backend_node_id,
68 node_id,
69 tab,
70 })
71 }
72
73 pub(crate) async fn from_nodes(tab: &Arc<PageInner>, node_ids: &[NodeId]) -> Result<Vec<Self>> {
75 future::join_all(
76 node_ids
77 .iter()
78 .copied()
79 .map(|id| Element::new(Arc::clone(tab), id)),
80 )
81 .await
82 .into_iter()
83 .collect::<Result<Vec<_>, _>>()
84 }
85
86 pub async fn find_element(&self, selector: impl Into<String>) -> Result<Self> {
89 let node_id = self.tab.find_element(selector, self.node_id).await?;
90 Element::new(Arc::clone(&self.tab), node_id).await
91 }
92
93 pub async fn find_elements(&self, selector: impl Into<String>) -> Result<Vec<Element>> {
95 Element::from_nodes(
96 &self.tab,
97 &self.tab.find_elements(selector, self.node_id).await?,
98 )
99 .await
100 }
101
102 async fn box_model(&self) -> Result<BoxModel> {
103 let model = self
104 .tab
105 .execute(
106 GetBoxModelParams::builder()
107 .backend_node_id(self.backend_node_id)
108 .build(),
109 )
110 .await?
111 .result
112 .model;
113 Ok(BoxModel {
114 content: ElementQuad::from_quad(&model.content),
115 padding: ElementQuad::from_quad(&model.padding),
116 border: ElementQuad::from_quad(&model.border),
117 margin: ElementQuad::from_quad(&model.margin),
118 width: model.width as u32,
119 height: model.height as u32,
120 })
121 }
122
123 pub async fn bounding_box(&self) -> Result<BoundingBox> {
125 let bounds = self.box_model().await?;
126 let quad = bounds.border;
127
128 let x = quad.most_left();
129 let y = quad.most_top();
130 let width = quad.most_right() - x;
131 let height = quad.most_bottom() - y;
132
133 Ok(BoundingBox {
134 x,
135 y,
136 width,
137 height,
138 })
139 }
140
141 pub async fn clickable_point(&self) -> Result<Point> {
143 let content_quads = self
144 .tab
145 .execute(
146 GetContentQuadsParams::builder()
147 .backend_node_id(self.backend_node_id)
148 .build(),
149 )
150 .await?;
151 content_quads
152 .quads
153 .iter()
154 .filter(|q| q.inner().len() == 8)
155 .map(ElementQuad::from_quad)
156 .filter(|q| q.quad_area() > 1.)
157 .map(|q| q.quad_center())
158 .next()
159 .ok_or_else(|| CdpError::msg("Node is either not visible or not an HTMLElement"))
160 }
161
162 pub async fn call_js_fn(
189 &self,
190 function_declaration: impl Into<String>,
191 await_promise: bool,
192 ) -> Result<CallFunctionOnReturns> {
193 self.tab
194 .call_js_fn(
195 function_declaration,
196 await_promise,
197 self.remote_object_id.clone(),
198 )
199 .await
200 }
201
202 pub async fn json_value(&self) -> Result<serde_json::Value> {
204 let element_json = self
205 .call_js_fn("function() { return this; }", false)
206 .await?;
207 element_json.result.value.ok_or(CdpError::NotFound)
208 }
209
210 pub async fn focus(&self) -> Result<&Self> {
212 self.call_js_fn("function() { this.focus(); }", true)
213 .await?;
214 Ok(self)
215 }
216
217 pub async fn hover(&self) -> Result<&Self> {
220 self.scroll_into_view().await?;
221 self.tab.move_mouse(self.clickable_point().await?).await?;
222 Ok(self)
223 }
224
225 pub async fn scroll_into_view(&self) -> Result<&Self> {
230 let resp = self
231 .call_js_fn(
232 "async function() {
233 if (!this.isConnected)
234 return 'Node is detached from document';
235 if (this.nodeType !== Node.ELEMENT_NODE)
236 return 'Node is not of type HTMLElement';
237
238 const visibleRatio = await new Promise(resolve => {
239 const observer = new IntersectionObserver(entries => {
240 resolve(entries[0].intersectionRatio);
241 observer.disconnect();
242 });
243 observer.observe(this);
244 });
245
246 if (visibleRatio !== 1.0)
247 this.scrollIntoView({
248 block: 'center',
249 inline: 'center',
250 behavior: 'instant'
251 });
252 return false;
253 }",
254 true,
255 )
256 .await?;
257
258 if resp.result.r#type == RemoteObjectType::String {
259 let error_text = resp.result.value.unwrap().as_str().unwrap().to_string();
260 return Err(CdpError::ScrollingFailed(error_text));
261 }
262 Ok(self)
263 }
264
265 pub async fn click(&self) -> Result<&Self> {
270 let center = self.scroll_into_view().await?.clickable_point().await?;
271 self.tab.click(center).await?;
272 Ok(self)
273 }
274
275 pub async fn click_with(&self, options: ClickOptions) -> Result<&Self> {
283 let center = self.scroll_into_view().await?.clickable_point().await?;
284 self.tab.click_with(center, options).await?;
285 Ok(self)
286 }
287
288 pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
302 self.tab.type_str(input).await?;
303 Ok(self)
304 }
305
306 pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
321 self.tab.press_key(key).await?;
322 Ok(self)
323 }
324
325 pub async fn description(&self) -> Result<Node> {
327 Ok(self
328 .tab
329 .execute(
330 DescribeNodeParams::builder()
331 .backend_node_id(self.backend_node_id)
332 .depth(100)
333 .build(),
334 )
335 .await?
336 .result
337 .node)
338 }
339
340 pub async fn attributes(&self) -> Result<Vec<String>> {
343 let node = self.description().await?;
344 Ok(node.attributes.unwrap_or_default())
345 }
346
347 pub async fn attribute(&self, attribute: impl AsRef<str>) -> Result<Option<String>> {
349 let js_fn = format!(
350 "function() {{ return this.getAttribute('{}'); }}",
351 attribute.as_ref()
352 );
353 let resp = self.call_js_fn(js_fn, false).await?;
354 if let Some(value) = resp.result.value {
355 Ok(serde_json::from_value(value)?)
356 } else {
357 Ok(None)
358 }
359 }
360
361 pub async fn iter_attributes(
363 &self,
364 ) -> Result<impl Stream<Item = (String, Result<Option<String>>)> + '_> {
365 let attributes = self.attributes().await?;
366 Ok(AttributeStream {
367 attributes,
368 fut: None,
369 element: self,
370 })
371 }
372
373 pub async fn inner_text(&self) -> Result<Option<String>> {
375 self.string_property("innerText").await
376 }
377
378 pub async fn inner_html(&self) -> Result<Option<String>> {
380 self.string_property("innerHTML").await
381 }
382
383 pub async fn outer_html(&self) -> Result<Option<String>> {
385 self.string_property("outerHTML").await
386 }
387
388 pub async fn string_property(&self, property: impl AsRef<str>) -> Result<Option<String>> {
392 let property = property.as_ref();
393 let value = self.property(property).await?.ok_or(CdpError::NotFound)?;
394 let txt: String = serde_json::from_value(value)?;
395 if !txt.is_empty() {
396 Ok(Some(txt))
397 } else {
398 Ok(None)
399 }
400 }
401
402 pub async fn property(&self, property: impl AsRef<str>) -> Result<Option<serde_json::Value>> {
407 let js_fn = format!("function() {{ return this.{}; }}", property.as_ref());
408 let resp = self.call_js_fn(js_fn, false).await?;
409 Ok(resp.result.value)
410 }
411
412 pub async fn properties(&self) -> Result<HashMap<String, PropertyDescriptor>> {
415 let mut params = GetPropertiesParams::new(self.remote_object_id.clone());
416 params.own_properties = Some(true);
417
418 let properties = self.tab.execute(params).await?;
419
420 Ok(properties
421 .result
422 .result
423 .into_iter()
424 .map(|p| (p.name.clone(), p))
425 .collect())
426 }
427
428 pub async fn screenshot(&self, format: CaptureScreenshotFormat) -> Result<Vec<u8>> {
430 let mut bounding_box = self.scroll_into_view().await?.bounding_box().await?;
431 let viewport = self.tab.layout_metrics().await?.css_layout_viewport;
432
433 bounding_box.x += viewport.page_x as f64;
434 bounding_box.y += viewport.page_y as f64;
435
436 let clip = Viewport {
437 x: viewport.page_x as f64 + bounding_box.x,
438 y: viewport.page_y as f64 + bounding_box.y,
439 width: bounding_box.width,
440 height: bounding_box.height,
441 scale: 1.,
442 };
443
444 self.tab
445 .screenshot(
446 CaptureScreenshotParams::builder()
447 .format(format)
448 .clip(clip)
449 .build(),
450 )
451 .await
452 }
453
454 pub async fn save_screenshot(
456 &self,
457 format: CaptureScreenshotFormat,
458 output: impl AsRef<Path>,
459 ) -> Result<Vec<u8>> {
460 let img = self.screenshot(format).await?;
461 utils::write(output.as_ref(), &img).await?;
462 Ok(img)
463 }
464}
465
466pub type AttributeValueFuture<'a> = Option<(
467 String,
468 Pin<Box<dyn Future<Output = Result<Option<String>>> + 'a>>,
469)>;
470
471#[must_use = "streams do nothing unless polled"]
473#[allow(missing_debug_implementations)]
474pub struct AttributeStream<'a> {
475 attributes: Vec<String>,
476 fut: AttributeValueFuture<'a>,
477 element: &'a Element,
478}
479
480impl<'a> Stream for AttributeStream<'a> {
481 type Item = (String, Result<Option<String>>);
482
483 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
484 let pin = self.get_mut();
485
486 if pin.fut.is_none() {
487 if let Some(name) = pin.attributes.pop() {
488 let fut = Box::pin(pin.element.attribute(name.clone()));
489 pin.fut = Some((name, fut));
490 } else {
491 return Poll::Ready(None);
492 }
493 }
494
495 if let Some((name, mut fut)) = pin.fut.take() {
496 if let Poll::Ready(res) = fut.poll_unpin(cx) {
497 return Poll::Ready(Some((name, res)));
498 } else {
499 pin.fut = Some((name, fut));
500 }
501 }
502 Poll::Pending
503 }
504}