1use std::collections::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(100)
45 .build(),
46 )
47 .await?
48 .node
49 .backend_node_id;
50
51 let resp = tab
52 .execute(
53 ResolveNodeParams::builder()
54 .backend_node_id(backend_node_id)
55 .build(),
56 )
57 .await?;
58
59 let remote_object_id = resp
60 .result
61 .object
62 .object_id
63 .ok_or_else(|| CdpError::msg(format!("No object Id found for {node_id:?}")))?;
64 Ok(Self {
65 remote_object_id,
66 backend_node_id,
67 node_id,
68 tab,
69 })
70 }
71
72 pub(crate) async fn from_nodes(tab: &Arc<PageInner>, node_ids: &[NodeId]) -> Result<Vec<Self>> {
74 future::join_all(
75 node_ids
76 .iter()
77 .copied()
78 .map(|id| Element::new(Arc::clone(tab), id)),
79 )
80 .await
81 .into_iter()
82 .collect::<Result<Vec<_>, _>>()
83 }
84
85 pub async fn find_element(&self, selector: impl Into<String>) -> Result<Self> {
88 let node_id = self.tab.find_element(selector, self.node_id).await?;
89 Element::new(Arc::clone(&self.tab), node_id).await
90 }
91
92 pub async fn find_elements(&self, selector: impl Into<String>) -> Result<Vec<Element>> {
94 Element::from_nodes(
95 &self.tab,
96 &self.tab.find_elements(selector, self.node_id).await?,
97 )
98 .await
99 }
100
101 async fn box_model(&self) -> Result<BoxModel> {
102 let model = self
103 .tab
104 .execute(
105 GetBoxModelParams::builder()
106 .backend_node_id(self.backend_node_id)
107 .build(),
108 )
109 .await?
110 .result
111 .model;
112 Ok(BoxModel {
113 content: ElementQuad::from_quad(&model.content),
114 padding: ElementQuad::from_quad(&model.padding),
115 border: ElementQuad::from_quad(&model.border),
116 margin: ElementQuad::from_quad(&model.margin),
117 width: model.width as u32,
118 height: model.height as u32,
119 })
120 }
121
122 pub async fn bounding_box(&self) -> Result<BoundingBox> {
124 let bounds = self.box_model().await?;
125 let quad = bounds.border;
126
127 let x = quad.most_left();
128 let y = quad.most_top();
129 let width = quad.most_right() - x;
130 let height = quad.most_bottom() - y;
131
132 Ok(BoundingBox {
133 x,
134 y,
135 width,
136 height,
137 })
138 }
139
140 pub async fn clickable_point(&self) -> Result<Point> {
142 let content_quads = self
143 .tab
144 .execute(
145 GetContentQuadsParams::builder()
146 .backend_node_id(self.backend_node_id)
147 .build(),
148 )
149 .await?;
150 content_quads
151 .quads
152 .iter()
153 .filter(|q| q.inner().len() == 8)
154 .map(ElementQuad::from_quad)
155 .filter(|q| q.quad_area() > 1.)
156 .map(|q| q.quad_center())
157 .next()
158 .ok_or_else(|| CdpError::msg("Node is either not visible or not an HTMLElement"))
159 }
160
161 pub async fn call_js_fn(
188 &self,
189 function_declaration: impl Into<String>,
190 await_promise: bool,
191 ) -> Result<CallFunctionOnReturns> {
192 self.tab
193 .call_js_fn(
194 function_declaration,
195 await_promise,
196 self.remote_object_id.clone(),
197 )
198 .await
199 }
200
201 pub async fn json_value(&self) -> Result<serde_json::Value> {
203 let element_json = self
204 .call_js_fn("function() { return this; }", false)
205 .await?;
206 element_json.result.value.ok_or(CdpError::NotFound)
207 }
208
209 pub async fn focus(&self) -> Result<&Self> {
211 self.call_js_fn("function() { this.focus(); }", true)
212 .await?;
213 Ok(self)
214 }
215
216 pub async fn hover(&self) -> Result<&Self> {
219 self.scroll_into_view().await?;
220 self.tab.move_mouse(self.clickable_point().await?).await?;
221 Ok(self)
222 }
223
224 pub async fn scroll_into_view(&self) -> Result<&Self> {
229 let resp = self
230 .call_js_fn(
231 "async function() {
232 if (!this.isConnected)
233 return 'Node is detached from document';
234 if (this.nodeType !== Node.ELEMENT_NODE)
235 return 'Node is not of type HTMLElement';
236
237 const visibleRatio = await new Promise(resolve => {
238 const observer = new IntersectionObserver(entries => {
239 resolve(entries[0].intersectionRatio);
240 observer.disconnect();
241 });
242 observer.observe(this);
243 });
244
245 if (visibleRatio !== 1.0)
246 this.scrollIntoView({
247 block: 'center',
248 inline: 'center',
249 behavior: 'instant'
250 });
251 return false;
252 }",
253 true,
254 )
255 .await?;
256
257 if resp.result.r#type == RemoteObjectType::String {
258 let error_text = resp.result.value.unwrap().as_str().unwrap().to_string();
259 return Err(CdpError::ScrollingFailed(error_text));
260 }
261 Ok(self)
262 }
263
264 pub async fn click(&self) -> Result<&Self> {
269 let center = self.scroll_into_view().await?.clickable_point().await?;
270 self.tab.click(center).await?;
271 Ok(self)
272 }
273
274 pub async fn type_str(&self, input: impl AsRef<str>) -> Result<&Self> {
288 self.tab.type_str(input).await?;
289 Ok(self)
290 }
291
292 pub async fn press_key(&self, key: impl AsRef<str>) -> Result<&Self> {
307 self.tab.press_key(key).await?;
308 Ok(self)
309 }
310
311 pub async fn description(&self) -> Result<Node> {
313 Ok(self
314 .tab
315 .execute(
316 DescribeNodeParams::builder()
317 .backend_node_id(self.backend_node_id)
318 .depth(100)
319 .build(),
320 )
321 .await?
322 .result
323 .node)
324 }
325
326 pub async fn attributes(&self) -> Result<Vec<String>> {
329 let node = self.description().await?;
330 Ok(node.attributes.unwrap_or_default())
331 }
332
333 pub async fn attribute(&self, attribute: impl AsRef<str>) -> Result<Option<String>> {
335 let js_fn = format!(
336 "function() {{ return this.getAttribute('{}'); }}",
337 attribute.as_ref()
338 );
339 let resp = self.call_js_fn(js_fn, false).await?;
340 if let Some(value) = resp.result.value {
341 Ok(serde_json::from_value(value)?)
342 } else {
343 Ok(None)
344 }
345 }
346
347 pub async fn iter_attributes(
349 &self,
350 ) -> Result<impl Stream<Item = (String, Result<Option<String>>)> + '_> {
351 let attributes = self.attributes().await?;
352 Ok(AttributeStream {
353 attributes,
354 fut: None,
355 element: self,
356 })
357 }
358
359 pub async fn inner_text(&self) -> Result<Option<String>> {
361 self.string_property("innerText").await
362 }
363
364 pub async fn inner_html(&self) -> Result<Option<String>> {
366 self.string_property("innerHTML").await
367 }
368
369 pub async fn outer_html(&self) -> Result<Option<String>> {
371 self.string_property("outerHTML").await
372 }
373
374 pub async fn string_property(&self, property: impl AsRef<str>) -> Result<Option<String>> {
378 let property = property.as_ref();
379 let value = self.property(property).await?.ok_or(CdpError::NotFound)?;
380 let txt: String = serde_json::from_value(value)?;
381 if !txt.is_empty() {
382 Ok(Some(txt))
383 } else {
384 Ok(None)
385 }
386 }
387
388 pub async fn property(&self, property: impl AsRef<str>) -> Result<Option<serde_json::Value>> {
393 let js_fn = format!("function() {{ return this.{}; }}", property.as_ref());
394 let resp = self.call_js_fn(js_fn, false).await?;
395 Ok(resp.result.value)
396 }
397
398 pub async fn properties(&self) -> Result<HashMap<String, PropertyDescriptor>> {
401 let mut params = GetPropertiesParams::new(self.remote_object_id.clone());
402 params.own_properties = Some(true);
403
404 let properties = self.tab.execute(params).await?;
405
406 Ok(properties
407 .result
408 .result
409 .into_iter()
410 .map(|p| (p.name.clone(), p))
411 .collect())
412 }
413
414 pub async fn screenshot(&self, format: CaptureScreenshotFormat) -> Result<Vec<u8>> {
416 let mut bounding_box = self.scroll_into_view().await?.bounding_box().await?;
417 let viewport = self.tab.layout_metrics().await?.css_layout_viewport;
418
419 bounding_box.x += viewport.page_x as f64;
420 bounding_box.y += viewport.page_y as f64;
421
422 let clip = Viewport {
423 x: viewport.page_x as f64 + bounding_box.x,
424 y: viewport.page_y as f64 + bounding_box.y,
425 width: bounding_box.width,
426 height: bounding_box.height,
427 scale: 1.,
428 };
429
430 self.tab
431 .screenshot(
432 CaptureScreenshotParams::builder()
433 .format(format)
434 .clip(clip)
435 .build(),
436 )
437 .await
438 }
439
440 pub async fn save_screenshot(
442 &self,
443 format: CaptureScreenshotFormat,
444 output: impl AsRef<Path>,
445 ) -> Result<Vec<u8>> {
446 let img = self.screenshot(format).await?;
447 utils::write(output.as_ref(), &img).await?;
448 Ok(img)
449 }
450}
451
452pub type AttributeValueFuture<'a> = Option<(
453 String,
454 Pin<Box<dyn Future<Output = Result<Option<String>>> + 'a>>,
455)>;
456
457#[must_use = "streams do nothing unless polled"]
459#[allow(missing_debug_implementations)]
460pub struct AttributeStream<'a> {
461 attributes: Vec<String>,
462 fut: AttributeValueFuture<'a>,
463 element: &'a Element,
464}
465
466impl<'a> Stream for AttributeStream<'a> {
467 type Item = (String, Result<Option<String>>);
468
469 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
470 let pin = self.get_mut();
471
472 if pin.fut.is_none() {
473 if let Some(name) = pin.attributes.pop() {
474 let fut = Box::pin(pin.element.attribute(name.clone()));
475 pin.fut = Some((name, fut));
476 } else {
477 return Poll::Ready(None);
478 }
479 }
480
481 if let Some((name, mut fut)) = pin.fut.take() {
482 if let Poll::Ready(res) = fut.poll_unpin(cx) {
483 return Poll::Ready(Some((name, res)));
484 } else {
485 pin.fut = Some((name, fut));
486 }
487 }
488 Poll::Pending
489 }
490}