1use std::fmt;
31use std::sync::Arc;
32
33use serde_json::Value;
34use tracing::debug;
35
36use crate::error::{Error, Result};
37use crate::identifiers::{ElementId, FrameId, SessionId, TabId};
38use crate::protocol::{Command, ElementCommand, InputCommand, Request, Response};
39
40use super::Window;
41use super::keyboard::Key;
42use super::selector::By;
43
44pub(crate) struct ElementInner {
50 pub id: ElementId,
52
53 pub tab_id: TabId,
55
56 pub frame_id: FrameId,
58
59 pub session_id: SessionId,
61
62 pub window: Option<Window>,
64}
65
66#[derive(Clone)]
85pub struct Element {
86 pub(crate) inner: Arc<ElementInner>,
88}
89
90impl fmt::Debug for Element {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.debug_struct("Element")
97 .field("id", &self.inner.id)
98 .field("tab_id", &self.inner.tab_id)
99 .field("frame_id", &self.inner.frame_id)
100 .finish_non_exhaustive()
101 }
102}
103
104impl Element {
109 pub(crate) fn new(
111 id: ElementId,
112 tab_id: TabId,
113 frame_id: FrameId,
114 session_id: SessionId,
115 window: Option<Window>,
116 ) -> Self {
117 Self {
118 inner: Arc::new(ElementInner {
119 id,
120 tab_id,
121 frame_id,
122 session_id,
123 window,
124 }),
125 }
126 }
127}
128
129impl Element {
134 #[inline]
136 #[must_use]
137 pub fn id(&self) -> &ElementId {
138 &self.inner.id
139 }
140
141 #[inline]
143 #[must_use]
144 pub fn tab_id(&self) -> TabId {
145 self.inner.tab_id
146 }
147
148 #[inline]
150 #[must_use]
151 pub fn frame_id(&self) -> FrameId {
152 self.inner.frame_id
153 }
154}
155
156impl Element {
161 pub async fn click(&self) -> Result<()> {
165 debug!(element_id = %self.inner.id, "Clicking element");
166 self.call_method("click", vec![]).await?;
167 Ok(())
168 }
169
170 pub async fn focus(&self) -> Result<()> {
172 debug!(element_id = %self.inner.id, "Focusing element");
173 self.call_method("focus", vec![]).await?;
174 Ok(())
175 }
176
177 pub async fn blur(&self) -> Result<()> {
179 debug!(element_id = %self.inner.id, "Blurring element");
180 self.call_method("blur", vec![]).await?;
181 Ok(())
182 }
183
184 pub async fn clear(&self) -> Result<()> {
188 debug!(element_id = %self.inner.id, "Clearing element");
189 self.set_property("value", Value::String(String::new()))
190 .await
191 }
192}
193
194impl Element {
199 pub async fn get_text(&self) -> Result<String> {
201 let value = self.get_property("textContent").await?;
202 Ok(value.as_str().unwrap_or("").to_string())
203 }
204
205 pub async fn get_inner_html(&self) -> Result<String> {
207 let value = self.get_property("innerHTML").await?;
208 Ok(value.as_str().unwrap_or("").to_string())
209 }
210
211 pub async fn get_value(&self) -> Result<String> {
213 let value = self.get_property("value").await?;
214 Ok(value.as_str().unwrap_or("").to_string())
215 }
216
217 pub async fn set_value(&self, value: &str) -> Result<()> {
219 self.set_property("value", Value::String(value.to_string()))
220 .await
221 }
222
223 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
227 let result = self
228 .call_method("getAttribute", vec![Value::String(name.to_string())])
229 .await?;
230 Ok(result.as_str().map(|s| s.to_string()))
231 }
232
233 pub async fn is_displayed(&self) -> Result<bool> {
237 let offset_parent = self.get_property("offsetParent").await?;
238 Ok(!offset_parent.is_null())
239 }
240
241 pub async fn is_enabled(&self) -> Result<bool> {
245 let disabled = self.get_property("disabled").await?;
246 Ok(!disabled.as_bool().unwrap_or(false))
247 }
248}
249
250impl Element {
255 pub async fn press(&self, key: Key) -> Result<()> {
269 let (key_str, code, key_code, printable) = key.properties();
270 self.type_key(
271 key_str, code, key_code, printable, false, false, false, false,
272 )
273 .await
274 }
275
276 #[allow(clippy::too_many_arguments)]
293 pub async fn type_key(
294 &self,
295 key: &str,
296 code: &str,
297 key_code: u32,
298 printable: bool,
299 ctrl: bool,
300 shift: bool,
301 alt: bool,
302 meta: bool,
303 ) -> Result<()> {
304 let command = Command::Input(InputCommand::TypeKey {
305 element_id: self.inner.id.clone(),
306 key: key.to_string(),
307 code: code.to_string(),
308 key_code,
309 printable,
310 ctrl,
311 shift,
312 alt,
313 meta,
314 });
315
316 self.send_command(command).await?;
317 Ok(())
318 }
319
320 pub async fn type_char(&self, c: char) -> Result<()> {
324 self.type_text(&c.to_string()).await
325 }
326
327 pub async fn type_text(&self, text: &str) -> Result<()> {
338 debug!(element_id = %self.inner.id, text_len = text.len(), "Typing text");
339
340 let command = Command::Input(InputCommand::TypeText {
341 element_id: self.inner.id.clone(),
342 text: text.to_string(),
343 });
344
345 self.send_command(command).await?;
346 Ok(())
347 }
348}
349
350impl Element {
355 pub async fn mouse_click(&self, button: u8) -> Result<()> {
364 debug!(element_id = %self.inner.id, button = button, "Mouse clicking element");
365
366 let command = Command::Input(InputCommand::MouseClick {
367 element_id: Some(self.inner.id.clone()),
368 x: None,
369 y: None,
370 button,
371 });
372
373 self.send_command(command).await?;
374 Ok(())
375 }
376
377 pub async fn double_click(&self) -> Result<()> {
381 debug!(element_id = %self.inner.id, "Double clicking element");
382
383 self.call_method(
384 "dispatchEvent",
385 vec![serde_json::json!({"type": "dblclick", "bubbles": true, "cancelable": true})],
386 )
387 .await?;
388 Ok(())
389 }
390
391 pub async fn context_click(&self) -> Result<()> {
395 debug!(element_id = %self.inner.id, "Context clicking element");
396 self.mouse_click(2).await
397 }
398
399 pub async fn hover(&self) -> Result<()> {
403 debug!(element_id = %self.inner.id, "Hovering over element");
404 self.mouse_move().await
405 }
406
407 pub async fn mouse_move(&self) -> Result<()> {
409 debug!(element_id = %self.inner.id, "Moving mouse to element");
410
411 let command = Command::Input(InputCommand::MouseMove {
412 element_id: Some(self.inner.id.clone()),
413 x: None,
414 y: None,
415 });
416
417 self.send_command(command).await?;
418 Ok(())
419 }
420
421 pub async fn mouse_down(&self, button: u8) -> Result<()> {
430 debug!(element_id = %self.inner.id, button = button, "Mouse down on element");
431
432 let command = Command::Input(InputCommand::MouseDown {
433 element_id: Some(self.inner.id.clone()),
434 x: None,
435 y: None,
436 button,
437 });
438
439 self.send_command(command).await?;
440 Ok(())
441 }
442
443 pub async fn mouse_up(&self, button: u8) -> Result<()> {
452 debug!(element_id = %self.inner.id, button = button, "Mouse up on element");
453
454 let command = Command::Input(InputCommand::MouseUp {
455 element_id: Some(self.inner.id.clone()),
456 x: None,
457 y: None,
458 button,
459 });
460
461 self.send_command(command).await?;
462 Ok(())
463 }
464}
465
466impl Element {
471 pub async fn scroll_into_view(&self) -> Result<()> {
475 debug!(element_id = %self.inner.id, "Scrolling element into view");
476
477 self.call_method(
478 "scrollIntoView",
479 vec![serde_json::json!({"behavior": "smooth", "block": "center"})],
480 )
481 .await?;
482 Ok(())
483 }
484
485 pub async fn scroll_into_view_instant(&self) -> Result<()> {
487 debug!(element_id = %self.inner.id, "Scrolling element into view (instant)");
488
489 self.call_method(
490 "scrollIntoView",
491 vec![serde_json::json!({"behavior": "instant", "block": "center"})],
492 )
493 .await?;
494 Ok(())
495 }
496
497 pub async fn get_bounding_rect(&self) -> Result<(f64, f64, f64, f64)> {
503 let result = self.call_method("getBoundingClientRect", vec![]).await?;
504
505 let x = result.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
506 let y = result.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
507 let width = result.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0);
508 let height = result.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0);
509
510 debug!(element_id = %self.inner.id, x = x, y = y, width = width, height = height, "Got bounding rect");
511 Ok((x, y, width, height))
512 }
513}
514
515impl Element {
520 pub async fn is_checked(&self) -> Result<bool> {
522 let value = self.get_property("checked").await?;
523 Ok(value.as_bool().unwrap_or(false))
524 }
525
526 pub async fn check(&self) -> Result<()> {
530 if !self.is_checked().await? {
531 self.click().await?;
532 }
533 Ok(())
534 }
535
536 pub async fn uncheck(&self) -> Result<()> {
540 if self.is_checked().await? {
541 self.click().await?;
542 }
543 Ok(())
544 }
545
546 pub async fn toggle(&self) -> Result<()> {
548 self.click().await
549 }
550
551 pub async fn set_checked(&self, checked: bool) -> Result<()> {
553 if checked {
554 self.check().await
555 } else {
556 self.uncheck().await
557 }
558 }
559}
560
561impl Element {
566 pub async fn select_by_text(&self, text: &str) -> Result<()> {
575 if let Ok(options) = self.find_elements(By::tag("option")).await {
577 for option in options {
578 if let Ok(option_text) = option.get_text().await
579 && option_text.trim() == text
580 {
581 option.set_property("selected", Value::Bool(true)).await?;
582 self.call_method(
584 "dispatchEvent",
585 vec![serde_json::json!({"type": "change", "bubbles": true})],
586 )
587 .await?;
588 return Ok(());
589 }
590 }
591 }
592
593 Err(Error::invalid_argument(format!(
594 "Option with text '{}' not found",
595 text
596 )))
597 }
598
599 pub async fn select_by_value(&self, value: &str) -> Result<()> {
601 self.set_property("value", Value::String(value.to_string()))
602 .await?;
603 self.call_method(
604 "dispatchEvent",
605 vec![serde_json::json!({"type": "change", "bubbles": true})],
606 )
607 .await?;
608 Ok(())
609 }
610
611 pub async fn select_by_index(&self, index: usize) -> Result<()> {
613 self.set_property("selectedIndex", Value::Number(index.into()))
614 .await?;
615 self.call_method(
616 "dispatchEvent",
617 vec![serde_json::json!({"type": "change", "bubbles": true})],
618 )
619 .await?;
620 Ok(())
621 }
622
623 pub async fn get_selected_value(&self) -> Result<Option<String>> {
625 let value = self.get_property("value").await?;
626 Ok(value.as_str().map(|s| s.to_string()))
627 }
628
629 pub async fn get_selected_index(&self) -> Result<i64> {
631 let value = self.get_property("selectedIndex").await?;
632 Ok(value.as_i64().unwrap_or(-1))
633 }
634
635 pub async fn get_selected_text(&self) -> Result<Option<String>> {
637 let options = self.find_elements(By::css("option:checked")).await?;
638 if let Some(option) = options.first() {
639 let text = option.get_text().await?;
640 return Ok(Some(text));
641 }
642 Ok(None)
643 }
644
645 pub async fn is_multiple(&self) -> Result<bool> {
647 let value = self.get_property("multiple").await?;
648 Ok(value.as_bool().unwrap_or(false))
649 }
650}
651
652impl Element {
657 pub async fn find_element(&self, by: By) -> Result<Element> {
668 let command = Command::Element(ElementCommand::Find {
669 strategy: by.strategy().to_string(),
670 value: by.value().to_string(),
671 parent_id: Some(self.inner.id.clone()),
672 });
673
674 let response = self.send_command(command).await?;
675
676 let element_id = response
677 .result
678 .as_ref()
679 .and_then(|v| v.get("elementId"))
680 .and_then(|v| v.as_str())
681 .ok_or_else(|| {
682 Error::element_not_found(
683 format!("{}:{}", by.strategy(), by.value()),
684 self.inner.tab_id,
685 self.inner.frame_id,
686 )
687 })?;
688
689 Ok(Element::new(
690 ElementId::new(element_id),
691 self.inner.tab_id,
692 self.inner.frame_id,
693 self.inner.session_id,
694 self.inner.window.clone(),
695 ))
696 }
697
698 pub async fn find_elements(&self, by: By) -> Result<Vec<Element>> {
709 let command = Command::Element(ElementCommand::FindAll {
710 strategy: by.strategy().to_string(),
711 value: by.value().to_string(),
712 parent_id: Some(self.inner.id.clone()),
713 });
714
715 let response = self.send_command(command).await?;
716
717 let element_ids = response
718 .result
719 .as_ref()
720 .and_then(|v| v.get("elementIds"))
721 .and_then(|v| v.as_array())
722 .map(|arr| {
723 arr.iter()
724 .filter_map(|v| v.as_str())
725 .map(|id| {
726 Element::new(
727 ElementId::new(id),
728 self.inner.tab_id,
729 self.inner.frame_id,
730 self.inner.session_id,
731 self.inner.window.clone(),
732 )
733 })
734 .collect()
735 })
736 .unwrap_or_default();
737
738 Ok(element_ids)
739 }
740}
741
742impl Element {
747 pub async fn get_property(&self, name: &str) -> Result<Value> {
753 let command = Command::Element(ElementCommand::GetProperty {
754 element_id: self.inner.id.clone(),
755 name: name.to_string(),
756 });
757
758 let response = self.send_command(command).await?;
759
760 Ok(response
761 .result
762 .and_then(|v| v.get("value").cloned())
763 .unwrap_or(Value::Null))
764 }
765
766 pub async fn set_property(&self, name: &str, value: Value) -> Result<()> {
773 let command = Command::Element(ElementCommand::SetProperty {
774 element_id: self.inner.id.clone(),
775 name: name.to_string(),
776 value,
777 });
778
779 self.send_command(command).await?;
780 Ok(())
781 }
782
783 pub async fn call_method(&self, name: &str, args: Vec<Value>) -> Result<Value> {
790 let command = Command::Element(ElementCommand::CallMethod {
791 element_id: self.inner.id.clone(),
792 name: name.to_string(),
793 args,
794 });
795
796 let response = self.send_command(command).await?;
797
798 Ok(response
799 .result
800 .and_then(|v| v.get("value").cloned())
801 .unwrap_or(Value::Null))
802 }
803}
804
805impl Element {
810 pub async fn screenshot(&self) -> Result<String> {
821 self.screenshot_with_format("png", None).await
822 }
823
824 pub async fn screenshot_jpeg(&self, quality: u8) -> Result<String> {
830 self.screenshot_with_format("jpeg", Some(quality.min(100)))
831 .await
832 }
833
834 async fn screenshot_with_format(&self, format: &str, quality: Option<u8>) -> Result<String> {
839 use base64::Engine;
840 use base64::engine::general_purpose::STANDARD as Base64Standard;
841 use image::GenericImageView;
842
843 let command = Command::Element(ElementCommand::CaptureScreenshot {
844 element_id: self.inner.id.clone(),
845 format: format.to_string(),
846 quality,
847 });
848
849 let response = self.send_command(command).await?;
850
851 tracing::debug!(response = ?response, "Element screenshot response");
852
853 let result = response.result.as_ref().ok_or_else(|| {
854 let error_str = response.error.as_deref().unwrap_or("none");
855 let msg_str = response.message.as_deref().unwrap_or("none");
856 Error::script_error(format!(
857 "Element screenshot failed. error={}, message={}",
858 error_str, msg_str
859 ))
860 })?;
861
862 let data = result
863 .get("data")
864 .and_then(|v| v.as_str())
865 .ok_or_else(|| Error::script_error("Screenshot response missing data field"))?;
866
867 if let Some(clip) = result.get("clip") {
869 let x = clip.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
870 let y = clip.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
871 let width = clip.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
872 let height = clip.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0) as u32;
873 let scale = clip.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0);
874
875 let x = (x as f64 * scale) as u32;
877 let y = (y as f64 * scale) as u32;
878 let width = (width as f64 * scale) as u32;
879 let height = (height as f64 * scale) as u32;
880
881 if width == 0 || height == 0 {
882 return Err(Error::script_error("Element has zero dimensions"));
883 }
884
885 let image_bytes = Base64Standard
887 .decode(data)
888 .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
889
890 let img = image::load_from_memory(&image_bytes)
891 .map_err(|e| Error::script_error(format!("Failed to load image: {}", e)))?;
892
893 let (img_width, img_height) = img.dimensions();
895 let x = x.min(img_width.saturating_sub(1));
896 let y = y.min(img_height.saturating_sub(1));
897 let width = width.min(img_width.saturating_sub(x));
898 let height = height.min(img_height.saturating_sub(y));
899
900 let cropped = img.crop_imm(x, y, width, height);
902
903 let mut output = std::io::Cursor::new(Vec::new());
905 match format {
906 "jpeg" => {
907 let q = quality.unwrap_or(85);
908 cropped
909 .write_to(&mut output, image::ImageFormat::Jpeg)
910 .map_err(|e| {
911 Error::script_error(format!("Failed to encode JPEG: {}", e))
912 })?;
913 let _ = q; }
917 _ => {
918 cropped
919 .write_to(&mut output, image::ImageFormat::Png)
920 .map_err(|e| Error::script_error(format!("Failed to encode PNG: {}", e)))?;
921 }
922 }
923
924 Ok(Base64Standard.encode(output.into_inner()))
925 } else {
926 Ok(data.to_string())
928 }
929 }
930
931 pub async fn screenshot_bytes(&self) -> Result<Vec<u8>> {
933 use base64::Engine;
934 use base64::engine::general_purpose::STANDARD as Base64Standard;
935
936 let base64_data = self.screenshot().await?;
937 Base64Standard
938 .decode(&base64_data)
939 .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))
940 }
941
942 pub async fn save_screenshot(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
946 use base64::Engine;
947 use base64::engine::general_purpose::STANDARD as Base64Standard;
948
949 let path = path.as_ref();
950 let ext = path
951 .extension()
952 .and_then(|e| e.to_str())
953 .unwrap_or("png")
954 .to_lowercase();
955
956 let base64_data = match ext.as_str() {
957 "jpg" | "jpeg" => self.screenshot_jpeg(85).await?,
958 _ => self.screenshot().await?,
959 };
960
961 let bytes = Base64Standard
962 .decode(&base64_data)
963 .map_err(|e| Error::script_error(format!("Failed to decode base64: {}", e)))?;
964
965 std::fs::write(path, bytes).map_err(Error::Io)?;
966 Ok(())
967 }
968}
969
970impl Element {
975 async fn send_command(&self, command: Command) -> Result<Response> {
977 let window = self
978 .inner
979 .window
980 .as_ref()
981 .ok_or_else(|| Error::protocol("Element has no associated window"))?;
982
983 let request = Request::new(self.inner.tab_id, self.inner.frame_id, command);
984
985 window
986 .inner
987 .pool
988 .send(window.inner.session_id, request)
989 .await
990 }
991}
992
993#[cfg(test)]
998mod tests {
999 use super::Element;
1000
1001 #[test]
1002 fn test_element_is_clone() {
1003 fn assert_clone<T: Clone>() {}
1004 assert_clone::<Element>();
1005 }
1006
1007 #[test]
1008 fn test_element_is_debug() {
1009 fn assert_debug<T: std::fmt::Debug>() {}
1010 assert_debug::<Element>();
1011 }
1012}