1use crate::error::{Error, Result};
5use crate::options::{
6 CheckOptions, ClickOptions, DragToOptions, FillOptions, FilterOptions, HoverOptions,
7 PressOptions, PressSequentiallyOptions, ScreenshotOptions, SelectOption, SelectOptions,
8 WaitForOptions,
9};
10use crate::page::Page;
11use crate::selectors;
12use crate::types::BoundingBox;
13use base64::Engine;
14use serde_json::{json, Value};
15use std::time::Duration;
16
17#[derive(Clone)]
19pub struct Locator {
20 page: Page,
21 selector: String,
22 strict: bool,
23 forced_index: Option<usize>,
24 frame_chain: Vec<String>,
29}
30
31impl Locator {
32 pub(crate) fn new(page: Page, selector: String, strict: bool, forced_index: Option<usize>) -> Self {
33 Self {
34 page,
35 selector,
36 strict,
37 forced_index,
38 frame_chain: Vec::new(),
39 }
40 }
41
42 pub(crate) fn new_in_frame(
46 page: Page,
47 frame_chain: Vec<String>,
48 selector: String,
49 strict: bool,
50 forced_index: Option<usize>,
51 ) -> Self {
52 Self {
53 page,
54 selector,
55 strict,
56 forced_index,
57 frame_chain,
58 }
59 }
60
61 pub fn selector(&self) -> &str {
62 &self.selector
63 }
64
65 pub fn nth(&self, index: i32) -> Locator {
67 let mut l = self.clone();
68 l.strict = false;
69 l.forced_index = Some(if index < 0 { 0 } else { index as usize });
70 l
71 }
72
73 pub fn first(&self) -> Locator {
74 self.nth(0)
75 }
76
77 pub fn last(&self) -> Locator {
78 let mut l = self.clone();
79 l.strict = false;
80 l.forced_index = Some(usize::MAX); l
82 }
83
84 pub async fn count(&self) -> Result<usize> {
86 self.count_query().await
87 }
88
89 async fn count_query(&self) -> Result<usize> {
91 let ctx = self.page.ctx().await;
92 if self.frame_chain.is_empty() {
93 selectors::count(self.page.session(), ctx, &self.selector).await
94 } else {
95 selectors::count_in(self.page.session(), ctx, &self.frame_chain, &self.selector).await
96 }
97 }
98
99 async fn element_query(&self, index: usize) -> Result<Option<String>> {
102 let ctx = self.page.ctx().await;
103 if self.frame_chain.is_empty() {
104 selectors::element_at(self.page.session(), ctx, &self.selector, index).await
105 } else {
106 selectors::element_at_in(self.page.session(), ctx, &self.frame_chain, &self.selector, index).await
107 }
108 }
109
110 async fn resolve(&self) -> Result<String> {
112 let n = self.count_query().await?;
113 if n == 0 {
114 return Err(Error::ElementNotFound(format!(
115 "no element matched '{}'",
116 self.selector
117 )));
118 }
119 if self.strict && self.forced_index.is_none() && n > 1 {
120 return Err(Error::ElementNotFound(format!(
121 "strict mode violation: {n} elements matched '{}'",
122 self.selector
123 )));
124 }
125 let index = match self.forced_index {
126 Some(usize::MAX) => n - 1,
127 Some(i) => i,
128 None => 0,
129 };
130 self.element_query(index)
131 .await?
132 .ok_or_else(|| Error::ElementNotFound(format!("no element at index {index} for '{}'", self.selector)))
133 }
134
135 async fn release(&self, object_id: &str) {
136 let _ = self
137 .page
138 .session()
139 .send("Runtime.releaseObject", json!({ "objectId": object_id }))
140 .await;
141 }
142
143 pub async fn click(&self, opts: Option<ClickOptions>) -> Result<()> {
146 let opts = opts.unwrap_or_default();
147 let oid = self.resolve().await?;
148 let result = click_element(&self.page, &oid, &opts).await;
149 self.release(&oid).await;
150 result
151 }
152
153 pub async fn fill(&self, text: &str, _opts: Option<FillOptions>) -> Result<()> {
154 let oid = self.resolve().await?;
155 let result = selectors::eval_object(
156 self.page.session(),
157 &oid,
158 "(el, text) => { el.focus(); el.value = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }",
159 json!(text),
160 )
161 .await
162 .map(|_| ());
163 self.release(&oid).await;
164 result
165 }
166
167 pub async fn type_text(&self, text: &str) -> Result<()> {
168 self.fill(text, None).await
169 }
170
171 pub async fn type_(&self, text: &str, _options: Option<PressSequentiallyOptions>) -> Result<()> {
174 self.type_text(text).await
175 }
176
177 pub async fn press(&self, key: &str, options: Option<PressOptions>) -> Result<()> {
178 let oid = self.resolve().await?;
179 let _ = self
180 .page
181 .session()
182 .send("DOM.focus", json!({ "objectId": oid }))
183 .await;
184 let delay = options.and_then(|o| o.delay).map(|ms| Duration::from_millis(ms as u64));
185 let down = self
186 .page
187 .session()
188 .send("Input.dispatchKeyEvent", json!({ "type": "keyDown", "key": key }))
189 .await;
190 if let Some(d) = delay {
191 tokio::time::sleep(d).await;
192 }
193 let up = self
194 .page
195 .session()
196 .send("Input.dispatchKeyEvent", json!({ "type": "keyUp", "key": key }))
197 .await;
198 self.release(&oid).await;
199 down.map(|_: Value| ())?;
200 up.map(|_: Value| ())
201 }
202
203 pub async fn hover(&self, opts: Option<HoverOptions>) -> Result<()> {
204 let opts = opts.unwrap_or_default();
205 let oid = self.resolve().await?;
206 let _ = self
207 .page
208 .session()
209 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": oid }))
210 .await;
211 let (bx, by, bw, bh) = element_box(&self.page, &oid).await?;
212 let (x, y) = match &opts.position {
213 Some(p) => (bx + p.x, by + p.y),
214 None => (bx + bw / 2.0, by + bh / 2.0),
215 };
216 let result = self
217 .page
218 .session()
219 .send(
220 "Input.dispatchMouseEvent",
221 json!({ "type": "mouseMoved", "x": x, "y": y, "button": "none", "buttons": 0 }),
222 )
223 .await
224 .map(|_: Value| ());
225 self.release(&oid).await;
226 result
227 }
228
229 pub async fn focus(&self) -> Result<()> {
230 let oid = self.resolve().await?;
231 let result = self
232 .page
233 .session()
234 .send("DOM.focus", json!({ "objectId": oid }))
235 .await
236 .map(|_: Value| ());
237 self.release(&oid).await;
238 result
239 }
240
241 pub async fn blur(&self) -> Result<()> {
243 let oid = self.resolve().await?;
244 let r = selectors::eval_object(self.page.session(), &oid, "(el) => { el.blur(); }", Value::Null)
245 .await
246 .map(|_| ());
247 self.release(&oid).await;
248 r
249 }
250
251 pub async fn input_value(&self, _options: Option<()>) -> Result<String> {
253 let oid = self.resolve().await?;
254 let v: Value = {
255 let res = selectors::eval_object(
256 self.page.session(),
257 &oid,
258 "(el) => el.value",
259 Value::Null,
260 )
261 .await;
262 self.release(&oid).await;
263 res?
264 };
265 Ok(v.as_str().unwrap_or("").to_string())
266 }
267
268 pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
270 let oid = self.resolve().await?;
271 let r = self
272 .page
273 .session()
274 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": oid }))
275 .await
276 .map(|_: Value| ());
277 self.release(&oid).await;
278 r
279 }
280
281 pub async fn dispatch_event(&self, type_: &str, init: Option<Value>) -> Result<()> {
283 let oid = self.resolve().await?;
284 let init = init.unwrap_or(Value::Null);
285 let r = selectors::eval_object(
286 self.page.session(),
287 &oid,
288 "(el, args) => { el.dispatchEvent(new Event(args.type, args.init || undefined)); }",
289 json!({ "type": type_, "init": init }),
290 )
291 .await
292 .map(|_| ());
293 self.release(&oid).await;
294 r
295 }
296
297 pub async fn evaluate<R: serde::de::DeserializeOwned>(
299 &self,
300 expression: &str,
301 arg: Option<Value>,
302 ) -> Result<R> {
303 let oid = self.resolve().await?;
304 let function = format!("(el, arg) => {{ return ({expression}); }}");
305 let v = selectors::eval_object(self.page.session(), &oid, &function, arg.unwrap_or(Value::Null))
306 .await?;
307 self.release(&oid).await;
308 serde_json::from_value::<R>(v).map_err(Error::from)
309 }
310
311 pub async fn evaluate_all<R: serde::de::DeserializeOwned>(
313 &self,
314 expression: &str,
315 arg: Option<Value>,
316 ) -> Result<R> {
317 let function = if self.frame_chain.is_empty() {
318 format!(
319 "(arg) => {{ const els = self.__pwcdpInjected.querySelectorAll(document, {selector:?}); return ({expression}); }}",
320 selector = self.selector,
321 expression = expression
322 )
323 } else {
324 format!(
326 "(arg) => {{ let root = document; for (const fs of {chain:?}) {{ const f = self.__pwcdpInjected.querySelectorAll(root, fs)[0]; root = f && f.contentDocument; if (!root) {{ const els = []; return ({expression}); }} }} const els = self.__pwcdpInjected.querySelectorAll(root, {selector:?}); return ({expression}); }}",
327 chain = self.frame_chain,
328 selector = self.selector,
329 expression = expression
330 )
331 };
332 let v = selectors::eval_context(
333 self.page.session(),
334 self.page.ctx().await,
335 &function,
336 arg.unwrap_or(Value::Null),
337 )
338 .await?;
339 serde_json::from_value::<R>(v).map_err(Error::from)
340 }
341
342 pub async fn text_content(&self) -> Result<Option<String>> {
345 self.read("(el) => el.textContent").await
346 }
347
348 pub async fn inner_text(&self) -> Result<String> {
349 self.read("(el) => el.innerText")
350 .await
351 .map(|v: Option<String>| v.unwrap_or_default())
352 }
353
354 pub async fn inner_html(&self) -> Result<String> {
355 self.read("(el) => el.innerHTML")
356 .await
357 .map(|v: Option<String>| v.unwrap_or_default())
358 }
359
360 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
361 let oid = self.resolve().await?;
362 let v = selectors::eval_object(
363 self.page.session(),
364 &oid,
365 "(el, name) => el.getAttribute(name)",
366 json!(name),
367 )
368 .await?;
369 self.release(&oid).await;
370 Ok(v.as_str().map(String::from))
371 }
372
373 async fn read<R: serde::de::DeserializeOwned>(&self, function: &str) -> Result<R> {
374 let oid = self.resolve().await?;
375 let v = selectors::eval_object(self.page.session(), &oid, function, Value::Null).await?;
376 self.release(&oid).await;
377 serde_json::from_value::<R>(v).map_err(Error::from)
378 }
379
380 pub async fn is_visible(&self) -> Result<bool> {
381 self.state("visible").await
382 }
383 pub async fn is_enabled(&self) -> Result<bool> {
384 self.state("enabled").await
385 }
386 pub async fn is_checked(&self) -> Result<bool> {
387 self.state("checked").await
388 }
389 pub async fn is_editable(&self) -> Result<bool> {
390 self.state("editable").await
391 }
392
393 async fn state(&self, field: &str) -> Result<bool> {
394 let oid = self.resolve().await?;
395 let function = format!(
396 "(el) => self.__pwcdpInjected && self.__pwcdpInjected.elementState(el).{field}"
397 );
398 let v = selectors::eval_object(self.page.session(), &oid, &function, Value::Null).await?;
399 self.release(&oid).await;
400 Ok(v.as_bool().unwrap_or(false))
401 }
402
403 pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
404 let oid = self.resolve().await?;
405 let r = element_box(&self.page, &oid).await;
406 self.release(&oid).await;
407 match r {
408 Ok((x, y, w, h)) => Ok(Some(BoundingBox { x, y, width: w, height: h })),
409 Err(_) => Ok(None),
410 }
411 }
412
413 pub async fn wait_for(&self, opts: Option<WaitForOptions>) -> Result<()> {
415 let timeout = opts
416 .and_then(|o| o.timeout)
417 .map(|ms| Duration::from_millis(ms as u64))
418 .unwrap_or_else(|| Duration::from_secs(30));
419 let deadline = tokio::time::Instant::now() + timeout;
420 loop {
421 if self.count().await? > 0 {
422 return Ok(());
423 }
424 if tokio::time::Instant::now() >= deadline {
425 return Err(Error::Timeout(format!(
426 "wait_for '{}' timed out after {}ms",
427 self.selector,
428 timeout.as_millis()
429 )));
430 }
431 tokio::time::sleep(Duration::from_millis(100)).await;
432 }
433 }
434
435 pub async fn clear(&self, options: Option<FillOptions>) -> Result<()> {
439 self.fill("", options).await
440 }
441
442 pub async fn dblclick(&self, options: Option<ClickOptions>) -> Result<()> {
444 let mut opts = options.unwrap_or_default();
445 opts.click_count = Some(opts.click_count.unwrap_or(2));
446 self.click(Some(opts)).await
447 }
448
449 pub async fn check(&self, options: Option<CheckOptions>) -> Result<()> {
451 self.set_checked(true, options).await
452 }
453
454 pub async fn uncheck(&self, options: Option<CheckOptions>) -> Result<()> {
456 self.set_checked(false, options).await
457 }
458
459 pub async fn set_checked(&self, checked: bool, options: Option<CheckOptions>) -> Result<()> {
461 let opts = options.unwrap_or_default();
462 let current = self.is_checked().await?;
463 if current != checked {
464 let click_opts = ClickOptions {
465 force: opts.force,
466 position: opts.position,
467 timeout: opts.timeout,
468 trial: opts.trial,
469 ..Default::default()
470 };
471 self.click(Some(click_opts)).await?;
472 }
473 Ok(())
474 }
475
476 pub async fn select_option(
478 &self,
479 value: impl Into<SelectOption>,
480 _options: Option<SelectOptions>,
481 ) -> Result<Vec<String>> {
482 let value = value.into();
483 let arg: Value = match value {
484 SelectOption::Value(v) => json!({ "Value": v }),
485 SelectOption::Label(v) => json!({ "Label": v }),
486 SelectOption::Index(i) => json!({ "Index": i }),
487 };
488 let oid = self.resolve().await?;
489 let v: Value = selectors::eval_object(
490 self.page.session(),
491 &oid,
492 "(el, sel) => {
493 const opts = Array.from(el.options || []);
494 const matched = [];
495 for (const o of opts) {
496 const hit = sel.Value != null ? o.value === sel.Value
497 : sel.Label != null ? o.label === sel.Label
498 : sel.Index != null ? o.index === sel.Index : false;
499 if (hit) { o.selected = true; matched.push(o.value); }
500 }
501 el.dispatchEvent(new Event('input', { bubbles: true }));
502 el.dispatchEvent(new Event('change', { bubbles: true }));
503 return matched;
504 }",
505 arg,
506 )
507 .await?;
508 self.release(&oid).await;
509 Ok(v.as_array()
510 .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
511 .unwrap_or_default())
512 }
513
514 pub async fn set_input_files(&self, files: &[&str]) -> Result<()> {
516 let oid = self.resolve().await?;
517 let desc = self
519 .page
520 .session()
521 .send("DOM.requestNode", json!({ "objectId": oid }))
522 .await?;
523 let node_id = desc
524 .get("nodeId")
525 .and_then(|v| v.as_i64())
526 .ok_or_else(|| Error::ProtocolError("requestNode missing nodeId".into()))?;
527 let r = self
528 .page
529 .session()
530 .send(
531 "DOM.setFileInputFiles",
532 json!({ "nodeId": node_id, "files": files }),
533 )
534 .await
535 .map(|_: Value| ());
536 self.release(&oid).await;
537 r
538 }
539
540 pub async fn tap(&self, options: Option<ClickOptions>) -> Result<()> {
542 self.click(options).await
543 }
544
545 pub async fn drag_to(&self, target: &Locator, _options: Option<DragToOptions>) -> Result<()> {
547 let oid = self.resolve().await?;
548 let _ = self
549 .page
550 .session()
551 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": oid }))
552 .await;
553 let (sx, sy) = element_center(&self.page, &oid).await?;
554 let target_oid = target.resolve().await?;
555 let (tx, ty) = element_center(&target.page, &target_oid).await?;
556 target.release(&target_oid).await;
557
558 self.page
559 .session()
560 .send(
561 "Input.dispatchMouseEvent",
562 json!({ "type": "mouseMoved", "x": sx, "y": sy, "button": "none", "buttons": 0 }),
563 )
564 .await?;
565 self.page
566 .session()
567 .send(
568 "Input.dispatchMouseEvent",
569 json!({ "type": "mousePressed", "x": sx, "y": sy, "button": "left", "buttons": 1, "clickCount": 1 }),
570 )
571 .await?;
572 let steps = 10;
573 for i in 1..=steps {
574 let x = sx + (tx - sx) * (i as f64) / (steps as f64);
575 let y = sy + (ty - sy) * (i as f64) / (steps as f64);
576 self.page
577 .session()
578 .send(
579 "Input.dispatchMouseEvent",
580 json!({ "type": "mouseMoved", "x": x, "y": y, "button": "none", "buttons": 1 }),
581 )
582 .await?;
583 }
584 let r = self
585 .page
586 .session()
587 .send(
588 "Input.dispatchMouseEvent",
589 json!({ "type": "mouseReleased", "x": tx, "y": ty, "button": "left", "buttons": 0, "clickCount": 1 }),
590 )
591 .await
592 .map(|_: Value| ());
593 self.release(&oid).await;
594 r
595 }
596
597 pub async fn is_hidden(&self) -> Result<bool> {
600 Ok(!self.is_visible().await?)
601 }
602
603 pub async fn is_disabled(&self) -> Result<bool> {
604 let oid = self.resolve().await?;
605 let v = selectors::eval_object(
606 self.page.session(),
607 &oid,
608 "(el) => el.disabled === true",
609 Value::Null,
610 )
611 .await?;
612 self.release(&oid).await;
613 Ok(v.as_bool().unwrap_or(false))
614 }
615
616 pub async fn is_focused(&self) -> Result<bool> {
617 let oid = self.resolve().await?;
618 let v = selectors::eval_object(
619 self.page.session(),
620 &oid,
621 "(el) => el === document.activeElement",
622 Value::Null,
623 )
624 .await?;
625 self.release(&oid).await;
626 Ok(v.as_bool().unwrap_or(false))
627 }
628
629 pub async fn all(&self) -> Result<Vec<Locator>> {
631 let n = self.count().await?;
632 let mut out = Vec::with_capacity(n);
633 for i in 0..n {
634 let mut l = self.clone();
635 l.strict = false;
636 l.forced_index = Some(i);
637 out.push(l);
638 }
639 Ok(out)
640 }
641
642 pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
643 let n = self.count().await?;
644 let mut out = Vec::with_capacity(n);
645 for i in 0..n {
646 if let Some(oid) = self.element_query(i).await? {
647 let v = selectors::eval_object(
648 self.page.session(),
649 &oid,
650 "(el) => el.innerText",
651 Value::Null,
652 )
653 .await
654 .ok();
655 let _ = self
656 .page
657 .session()
658 .send("Runtime.releaseObject", json!({ "objectId": oid }))
659 .await;
660 out.push(v.and_then(|x| x.as_str().map(String::from)).unwrap_or_default());
661 }
662 }
663 Ok(out)
664 }
665
666 pub async fn all_text_contents(&self) -> Result<Vec<String>> {
667 let n = self.count().await?;
668 let mut out = Vec::with_capacity(n);
669 for i in 0..n {
670 if let Some(oid) = self.element_query(i).await? {
671 let v = selectors::eval_object(
672 self.page.session(),
673 &oid,
674 "(el) => el.textContent",
675 Value::Null,
676 )
677 .await
678 .ok();
679 let _ = self
680 .page
681 .session()
682 .send("Runtime.releaseObject", json!({ "objectId": oid }))
683 .await;
684 out.push(v.and_then(|x| x.as_str().map(String::from)).unwrap_or_default());
685 }
686 }
687 Ok(out)
688 }
689
690 pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
692 let oid = self.resolve().await?;
693 let (x, y, w, h) = element_box(&self.page, &oid).await?;
694 self.release(&oid).await;
695
696 let opts = opts.unwrap_or_default();
697 let format = match opts.r#type.unwrap_or_default() {
698 crate::types::ScreenshotType::Png => "png",
699 crate::types::ScreenshotType::Jpeg => "jpeg",
700 crate::types::ScreenshotType::Webp => "webp",
701 };
702 let mut params = json!({
703 "format": format,
704 "clip": { "x": x, "y": y, "width": w, "height": h, "scale": 1 },
705 });
706 if opts.omit_background.unwrap_or(false) && format == "png" {
707 params["omitBackground"] = json!(true);
708 }
709 if let Some(q) = opts.quality {
710 if format == "jpeg" {
711 params["quality"] = json!(q);
712 }
713 }
714 let resp = self
715 .page
716 .session()
717 .send("Page.captureScreenshot", params)
718 .await?;
719 let data = resp
720 .get("data")
721 .and_then(|v| v.as_str())
722 .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
723 let bytes = base64::engine::general_purpose::STANDARD
724 .decode(data)
725 .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
726 if let Some(path) = &opts.path {
727 tokio::fs::write(path, &bytes).await?;
728 }
729 Ok(bytes)
730 }
731
732 pub fn filter(&self, options: FilterOptions) -> Locator {
736 let mut sel = self.selector.clone();
737 if let Some(t) = options.has_text {
738 sel.push_str(&format!(" >> text={t}"));
739 }
740 let mut l = self.clone();
741 l.selector = sel;
742 l
743 }
744
745 pub fn and_(&self, other: &Locator) -> Locator {
747 let mut l = self.clone();
748 l.selector = format!("{} >> {}", self.selector, other.selector);
749 l
750 }
751
752 pub fn or_(&self, other: &Locator) -> Locator {
754 let mut l = self.clone();
755 l.selector = format!("{}, {}", self.selector, other.selector);
756 l
757 }
758
759 fn chain(&self, sub: impl Into<String>) -> Locator {
762 let mut l = self.clone();
763 l.selector = format!("{} >> {}", self.selector, sub.into());
764 l.forced_index = None;
765 l.strict = true;
766 l
767 }
768
769 pub fn locator(&self, selector: impl Into<String>) -> Locator {
771 self.chain(selector)
772 }
773
774 pub fn get_by_text(&self, text: &str, _exact: bool) -> Locator {
775 self.chain(format!("text={text}"))
776 }
777
778 pub fn get_by_label(&self, text: &str) -> Locator {
779 self.chain(format!("[aria-label=\"{}\"]", esc(text)))
780 }
781
782 pub fn get_by_placeholder(&self, text: &str) -> Locator {
783 self.chain(format!("[placeholder=\"{}\"]", esc(text)))
784 }
785
786 pub fn get_by_alt_text(&self, text: &str) -> Locator {
787 self.chain(format!("[alt=\"{}\"]", esc(text)))
788 }
789
790 pub fn get_by_title(&self, text: &str) -> Locator {
791 self.chain(format!("[title=\"{}\"]", esc(text)))
792 }
793
794 pub fn get_by_test_id(&self, test_id: &str) -> Locator {
795 self.chain(format!("[data-testid=\"{}\"]", esc(test_id)))
796 }
797
798 pub fn get_by_role(
799 &self,
800 role: crate::types::AriaRole,
801 opts: Option<crate::options::GetByRoleOptions>,
802 ) -> Locator {
803 let opts = opts.unwrap_or_default();
804 let mut sel = format!("role={}", role.as_str());
805 if let Some(name) = &opts.name {
806 sel.push_str(&format!("[name=\"{name}\"]"));
807 }
808 if opts.exact == Some(true) {
809 sel.push_str("[exact=\"true\"]");
810 }
811 self.chain(sel)
812 }
813
814 pub async fn aria_snapshot(&self) -> Result<String> {
823 let oid = self.resolve().await?;
824
825 let desc = self
831 .page
832 .session()
833 .send("DOM.describeNode", json!({ "objectId": oid }))
834 .await;
835 self.release(&oid).await;
836 let desc = desc?;
837 let backend_node_id = desc
838 .get("node")
839 .and_then(|n| n.get("backendNodeId"))
840 .and_then(|v| v.as_i64())
841 .ok_or_else(|| {
842 Error::ProtocolError("DOM.describeNode missing backendNodeId".into())
843 })?;
844
845 let _ = self
847 .page
848 .session()
849 .send("Accessibility.enable", json!({}))
850 .await;
851 let resp = self
852 .page
853 .session()
854 .send("Accessibility.getFullAXTree", json!({}))
855 .await?;
856 let nodes = resp
857 .get("nodes")
858 .and_then(|v| v.as_array())
859 .cloned()
860 .unwrap_or_default();
861 Ok(crate::aria_snapshot::serialize(&nodes, Some(backend_node_id)))
862 }
863}
864
865fn esc(s: &str) -> String {
866 s.replace('\\', "\\\\").replace('"', "\\\"")
867}
868
869pub(crate) async fn element_box(page: &Page, object_id: &str) -> Result<(f64, f64, f64, f64)> {
874 let resp = page
875 .session()
876 .send("DOM.getBoxModel", json!({ "objectId": object_id }))
877 .await?;
878 let model = resp
879 .get("model")
880 .ok_or_else(|| Error::ProtocolError("getBoxModel missing model".into()))?;
881 let (x, y, w, h) = quad_box(model.get("content"))
882 .ok_or_else(|| Error::ProtocolError("getBoxModel content quad malformed".into()))?;
883 let mw = model.get("width").and_then(|v| v.as_f64());
885 let mh = model.get("height").and_then(|v| v.as_f64());
886 Ok((x, y, mw.unwrap_or(w), mh.unwrap_or(h)))
887}
888
889pub(crate) async fn element_center(page: &Page, object_id: &str) -> Result<(f64, f64)> {
891 let (x, y, w, h) = element_box(page, object_id).await?;
892 Ok((x + w / 2.0, y + h / 2.0))
893}
894
895async fn wait_actionable(
897 page: &Page,
898 object_id: &str,
899 timeout: Duration,
900) -> Result<()> {
901 let function = "(el) => self.__pwcdpInjected && self.__pwcdpInjected.elementState(el).visible";
902 let deadline = tokio::time::Instant::now() + timeout;
903 loop {
904 let v = selectors::eval_object(page.session(), object_id, function, Value::Null).await?;
905 if v.as_bool() == Some(true) {
906 return Ok(());
907 }
908 if tokio::time::Instant::now() >= deadline {
909 return Err(Error::Timeout(format!(
910 "element not visible within {}ms",
911 timeout.as_millis()
912 )));
913 }
914 tokio::time::sleep(Duration::from_millis(100)).await;
915 }
916}
917
918fn modifier_key(m: &crate::types::KeyboardModifier) -> &'static str {
919 use crate::types::KeyboardModifier::*;
920 match m {
921 Alt => "Alt",
922 Control => "Control",
923 ControlOrMeta => "Control",
924 Meta => "Meta",
925 Shift => "Shift",
926 }
927}
928
929pub(crate) async fn click_element(
933 page: &Page,
934 object_id: &str,
935 opts: &ClickOptions,
936) -> Result<()> {
937 let _ = page
938 .session()
939 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": object_id }))
940 .await;
941
942 let force = opts.force.unwrap_or(false);
943 let trial = opts.trial.unwrap_or(false);
944 if !force {
945 let timeout = opts
946 .timeout
947 .map(|ms| Duration::from_millis(ms as u64))
948 .unwrap_or_else(|| page.default_timeout());
949 wait_actionable(page, object_id, timeout).await?;
950 }
951
952 let (bx, by, bw, bh) = element_box(page, object_id).await?;
953 let (x, y) = match &opts.position {
954 Some(p) => (bx + p.x, by + p.y),
955 None => (bx + bw / 2.0, by + bh / 2.0),
956 };
957 let button = opts.button.unwrap_or_default();
958 let count = opts.click_count.unwrap_or(1);
959 let delay = opts.delay.map(|ms| Duration::from_millis(ms as u64));
960
961 if let Some(mods) = &opts.modifiers {
963 for m in mods {
964 let _ = page
965 .session()
966 .send(
967 "Input.dispatchKeyEvent",
968 json!({ "type": "keyDown", "key": modifier_key(m) }),
969 )
970 .await;
971 }
972 }
973
974 let result = if trial {
975 Ok(())
976 } else {
977 let mut out = Ok(());
978 page.session()
979 .send(
980 "Input.dispatchMouseEvent",
981 json!({ "type": "mouseMoved", "x": x, "y": y, "button": "none", "buttons": 0 }),
982 )
983 .await?;
984 for i in 0..count {
985 if let Err(e) = page
986 .session()
987 .send(
988 "Input.dispatchMouseEvent",
989 json!({ "type": "mousePressed", "x": x, "y": y, "button": button.as_str(), "buttons": button.bitmask(), "clickCount": i + 1 }),
990 )
991 .await
992 {
993 out = Err(e);
994 break;
995 }
996 if let Err(e) = page
997 .session()
998 .send(
999 "Input.dispatchMouseEvent",
1000 json!({ "type": "mouseReleased", "x": x, "y": y, "button": button.as_str(), "buttons": 0, "clickCount": i + 1 }),
1001 )
1002 .await
1003 {
1004 out = Err(e);
1005 break;
1006 }
1007 if let Some(d) = delay {
1008 if i + 1 < count {
1009 tokio::time::sleep(d).await;
1010 }
1011 }
1012 }
1013 out
1014 };
1015
1016 if let Some(mods) = &opts.modifiers {
1018 for m in mods {
1019 let _ = page
1020 .session()
1021 .send(
1022 "Input.dispatchKeyEvent",
1023 json!({ "type": "keyUp", "key": modifier_key(m) }),
1024 )
1025 .await;
1026 }
1027 }
1028
1029 result
1030}
1031
1032fn quad_box(quad: Option<&Value>) -> Option<(f64, f64, f64, f64)> {
1033 let arr = quad.and_then(|v| v.as_array())?;
1034 let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1035 if nums.len() < 8 {
1036 return None;
1037 }
1038 let xs = [nums[0], nums[2], nums[4], nums[6]];
1039 let ys = [nums[1], nums[3], nums[5], nums[7]];
1040 let xmin = xs.iter().cloned().fold(f64::INFINITY, f64::min);
1041 let xmax = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1042 let ymin = ys.iter().cloned().fold(f64::INFINITY, f64::min);
1043 let ymax = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1044 Some((xmin, ymin, xmax - xmin, ymax - ymin))
1045}