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 frame_ctx: Option<i64>,
35}
36
37impl Locator {
38 pub(crate) fn new(page: Page, selector: String, strict: bool, forced_index: Option<usize>) -> Self {
39 Self {
40 page,
41 selector,
42 strict,
43 forced_index,
44 frame_chain: Vec::new(),
45 frame_ctx: None,
46 }
47 }
48
49 pub(crate) fn new_in_frame_ctx(
53 page: Page,
54 frame_ctx: Option<i64>,
55 selector: String,
56 strict: bool,
57 forced_index: Option<usize>,
58 ) -> Self {
59 Self {
60 page,
61 selector,
62 strict,
63 forced_index,
64 frame_chain: Vec::new(),
65 frame_ctx,
66 }
67 }
68
69 pub(crate) fn new_in_frame(
73 page: Page,
74 frame_chain: Vec<String>,
75 selector: String,
76 strict: bool,
77 forced_index: Option<usize>,
78 ) -> Self {
79 Self {
80 page,
81 selector,
82 strict,
83 forced_index,
84 frame_chain,
85 frame_ctx: None,
86 }
87 }
88
89 pub fn selector(&self) -> &str {
90 &self.selector
91 }
92
93 pub fn nth(&self, index: i32) -> Locator {
95 let mut l = self.clone();
96 l.strict = false;
97 l.forced_index = Some(if index < 0 { 0 } else { index as usize });
98 l
99 }
100
101 pub fn first(&self) -> Locator {
102 self.nth(0)
103 }
104
105 pub fn last(&self) -> Locator {
106 let mut l = self.clone();
107 l.strict = false;
108 l.forced_index = Some(usize::MAX); l
110 }
111
112 pub async fn count(&self) -> Result<usize> {
114 self.count_query().await
115 }
116
117 async fn count_query(&self) -> Result<usize> {
119 if self.frame_chain.is_empty() {
120 let ctx = self.resolve_ctx().await;
121 selectors::count(self.page.session(), ctx, &self.selector).await
122 } else {
123 let ctx = self.page.ctx().await;
124 selectors::count_in(self.page.session(), ctx, &self.frame_chain, &self.selector).await
125 }
126 }
127
128 async fn element_query(&self, index: usize) -> Result<Option<String>> {
131 if self.frame_chain.is_empty() {
132 let ctx = self.resolve_ctx().await;
133 selectors::element_at(self.page.session(), ctx, &self.selector, index).await
134 } else {
135 let ctx = self.page.ctx().await;
136 selectors::element_at_in(self.page.session(), ctx, &self.frame_chain, &self.selector, index).await
137 }
138 }
139
140 async fn resolve_ctx(&self) -> Option<i64> {
144 match self.frame_ctx {
145 Some(id) => Some(id),
148 None => self.page.ctx().await,
149 }
150 }
151
152 async fn resolve(&self) -> Result<String> {
154 let n = self.count_query().await?;
155 if n == 0 {
156 return Err(Error::ElementNotFound(format!(
157 "no element matched '{}'",
158 self.selector
159 )));
160 }
161 if self.strict && self.forced_index.is_none() && n > 1 {
162 return Err(Error::ElementNotFound(format!(
163 "strict mode violation: {n} elements matched '{}'",
164 self.selector
165 )));
166 }
167 let index = match self.forced_index {
168 Some(usize::MAX) => n - 1,
169 Some(i) => i,
170 None => 0,
171 };
172 self.element_query(index)
173 .await?
174 .ok_or_else(|| Error::ElementNotFound(format!("no element at index {index} for '{}'", self.selector)))
175 }
176
177 async fn release(&self, object_id: &str) {
178 let _ = self
179 .page
180 .session()
181 .send("Runtime.releaseObject", json!({ "objectId": object_id }))
182 .await;
183 }
184
185 pub async fn click(&self, opts: Option<ClickOptions>) -> Result<()> {
188 let opts = opts.unwrap_or_default();
189 let oid = self.resolve().await?;
190 let result = click_element(&self.page, &oid, &opts).await;
191 self.release(&oid).await;
192 result
193 }
194
195 pub async fn fill(&self, text: &str, _opts: Option<FillOptions>) -> Result<()> {
196 let oid = self.resolve().await?;
197 let result = selectors::eval_object(
198 self.page.session(),
199 &oid,
200 "(el, text) => { el.focus(); el.value = text; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }",
201 json!(text),
202 )
203 .await
204 .map(|_| ());
205 self.release(&oid).await;
206 result
207 }
208
209 pub async fn type_text(&self, text: &str) -> Result<()> {
210 self.fill(text, None).await
211 }
212
213 pub async fn press_sequentially(&self, text: &str, opts: Option<PressSequentiallyOptions>) -> Result<()> {
220 let delay = opts
221 .and_then(|o| o.delay)
222 .map(|ms| Duration::from_millis(ms as u64));
223 let oid = self.resolve().await?;
224 let _ = self
226 .page
227 .session()
228 .send("DOM.focus", json!({ "objectId": oid }))
229 .await;
230 let result = press_text(&self.page, &oid, text, delay).await;
231 self.release(&oid).await;
232 result
233 }
234
235 pub async fn type_(&self, text: &str, opts: Option<PressSequentiallyOptions>) -> Result<()> {
238 self.press_sequentially(text, opts).await
239 }
240
241 pub async fn press(&self, key: &str, options: Option<PressOptions>) -> Result<()> {
242 let oid = self.resolve().await?;
243 let delay = options.and_then(|o| o.delay).map(|ms| Duration::from_millis(ms as u64));
244 let result = press_key(&self.page, &oid, key, delay).await;
245 self.release(&oid).await;
246 result
247 }
248
249 pub async fn hover(&self, opts: Option<HoverOptions>) -> Result<()> {
250 let opts = opts.unwrap_or_default();
251 let oid = self.resolve().await?;
252 let result = hover_element(&self.page, &oid, &opts).await;
253 self.release(&oid).await;
254 result
255 }
256
257 pub async fn focus(&self) -> Result<()> {
258 let oid = self.resolve().await?;
259 let result = self
260 .page
261 .session()
262 .send("DOM.focus", json!({ "objectId": oid }))
263 .await
264 .map(|_: Value| ());
265 self.release(&oid).await;
266 result
267 }
268
269 pub async fn blur(&self) -> Result<()> {
271 let oid = self.resolve().await?;
272 let r = selectors::eval_object(self.page.session(), &oid, "(el) => { el.blur(); }", Value::Null)
273 .await
274 .map(|_| ());
275 self.release(&oid).await;
276 r
277 }
278
279 pub async fn input_value(&self, _options: Option<()>) -> Result<String> {
281 let oid = self.resolve().await?;
282 let v: Value = {
283 let res = selectors::eval_object(
284 self.page.session(),
285 &oid,
286 "(el) => el.value",
287 Value::Null,
288 )
289 .await;
290 self.release(&oid).await;
291 res?
292 };
293 Ok(v.as_str().unwrap_or("").to_string())
294 }
295
296 pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
298 let oid = self.resolve().await?;
299 let r = self
300 .page
301 .session()
302 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": oid }))
303 .await
304 .map(|_: Value| ());
305 self.release(&oid).await;
306 r
307 }
308
309 pub async fn dispatch_event(&self, type_: &str, init: Option<Value>) -> Result<()> {
311 let oid = self.resolve().await?;
312 let init = init.unwrap_or(Value::Null);
313 let r = selectors::eval_object(
314 self.page.session(),
315 &oid,
316 "(el, args) => { el.dispatchEvent(new Event(args.type, args.init || undefined)); }",
317 json!({ "type": type_, "init": init }),
318 )
319 .await
320 .map(|_| ());
321 self.release(&oid).await;
322 r
323 }
324
325 pub async fn evaluate<R: serde::de::DeserializeOwned>(
327 &self,
328 expression: &str,
329 arg: Option<Value>,
330 ) -> Result<R> {
331 let oid = self.resolve().await?;
332 let function = format!("(el, arg) => {{ return ({expression}); }}");
333 let v = selectors::eval_object(self.page.session(), &oid, &function, arg.unwrap_or(Value::Null))
334 .await?;
335 self.release(&oid).await;
336 serde_json::from_value::<R>(v).map_err(Error::from)
337 }
338
339 pub async fn evaluate_handle(
348 &self,
349 page_function: &str,
350 arg: Option<Value>,
351 ) -> Result<crate::js_handle::JSHandle> {
352 let oid = self.resolve().await?;
353 let function = format!("(el, arg) => {{ return ({page_function}); }}");
354 let params = json!({
355 "objectId": oid,
356 "functionDeclaration": function,
357 "arguments": [{ "value": arg.unwrap_or(Value::Null) }],
358 "returnByValue": false,
359 "awaitPromise": true,
360 });
361 let resp = self.page.session().send("Runtime.callFunctionOn", params).await;
362 self.release(&oid).await;
364 let resp = resp?;
365 if let Some(exc) = resp.get("exceptionDetails") {
366 let msg = exc
367 .get("exception")
368 .and_then(|e| e.get("description"))
369 .and_then(|v| v.as_str())
370 .unwrap_or("evaluation threw");
371 return Err(Error::ProtocolError(format!("eval error: {msg}")));
372 }
373 let result_oid = resp
374 .get("result")
375 .and_then(|r| r.get("objectId"))
376 .and_then(|v| v.as_str())
377 .ok_or_else(|| {
378 Error::ProtocolError(
379 "callFunctionOn did not return a remote object (no objectId)".into(),
380 )
381 })?
382 .to_string();
383 Ok(crate::js_handle::JSHandle::new(self.page.session_arc(), result_oid))
384 }
385
386 pub async fn evaluate_all<R: serde::de::DeserializeOwned>(
388 &self,
389 expression: &str,
390 arg: Option<Value>,
391 ) -> Result<R> {
392 let function = if self.frame_chain.is_empty() {
393 format!(
394 "(arg) => {{ const els = self.__pwcdpInjected.querySelectorAll(document, {selector:?}); return ({expression}); }}",
395 selector = self.selector,
396 expression = expression
397 )
398 } else {
399 format!(
401 "(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}); }}",
402 chain = self.frame_chain,
403 selector = self.selector,
404 expression = expression
405 )
406 };
407 let ctx = if self.frame_chain.is_empty() {
408 self.resolve_ctx().await
409 } else {
410 self.page.ctx().await
411 };
412 let v = selectors::eval_context(
413 self.page.session(),
414 ctx,
415 &function,
416 arg.unwrap_or(Value::Null),
417 )
418 .await?;
419 serde_json::from_value::<R>(v).map_err(Error::from)
420 }
421
422 pub async fn text_content(&self) -> Result<Option<String>> {
425 self.read("(el) => el.textContent").await
426 }
427
428 pub async fn inner_text(&self) -> Result<String> {
429 self.read("(el) => el.innerText")
430 .await
431 .map(|v: Option<String>| v.unwrap_or_default())
432 }
433
434 pub async fn inner_html(&self) -> Result<String> {
435 self.read("(el) => el.innerHTML")
436 .await
437 .map(|v: Option<String>| v.unwrap_or_default())
438 }
439
440 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
441 let oid = self.resolve().await?;
442 let v = selectors::eval_object(
443 self.page.session(),
444 &oid,
445 "(el, name) => el.getAttribute(name)",
446 json!(name),
447 )
448 .await?;
449 self.release(&oid).await;
450 Ok(v.as_str().map(String::from))
451 }
452
453 async fn read<R: serde::de::DeserializeOwned>(&self, function: &str) -> Result<R> {
454 let oid = self.resolve().await?;
455 let v = selectors::eval_object(self.page.session(), &oid, function, Value::Null).await?;
456 self.release(&oid).await;
457 serde_json::from_value::<R>(v).map_err(Error::from)
458 }
459
460 pub async fn is_visible(&self) -> Result<bool> {
461 self.state("visible").await
462 }
463 pub async fn is_enabled(&self) -> Result<bool> {
464 self.state("enabled").await
465 }
466 pub async fn is_checked(&self) -> Result<bool> {
467 self.state("checked").await
468 }
469 pub async fn is_editable(&self) -> Result<bool> {
470 self.state("editable").await
471 }
472
473 async fn state(&self, field: &str) -> Result<bool> {
474 let oid = self.resolve().await?;
475 let function = format!(
476 "(el) => self.__pwcdpInjected && self.__pwcdpInjected.elementState(el).{field}"
477 );
478 let v = selectors::eval_object(self.page.session(), &oid, &function, Value::Null).await?;
479 self.release(&oid).await;
480 Ok(v.as_bool().unwrap_or(false))
481 }
482
483 pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
484 let oid = self.resolve().await?;
485 let r = element_box(&self.page, &oid).await;
486 self.release(&oid).await;
487 match r {
488 Ok((x, y, w, h)) => Ok(Some(BoundingBox { x, y, width: w, height: h })),
489 Err(_) => Ok(None),
490 }
491 }
492
493 pub async fn wait_for(&self, opts: Option<WaitForOptions>) -> Result<()> {
495 let timeout = opts
496 .and_then(|o| o.timeout)
497 .map(|ms| Duration::from_millis(ms as u64))
498 .unwrap_or_else(|| Duration::from_secs(30));
499 let deadline = tokio::time::Instant::now() + timeout;
500 loop {
501 if self.count().await? > 0 {
502 return Ok(());
503 }
504 if tokio::time::Instant::now() >= deadline {
505 return Err(Error::Timeout(format!(
506 "wait_for '{}' timed out after {}ms",
507 self.selector,
508 timeout.as_millis()
509 )));
510 }
511 tokio::time::sleep(Duration::from_millis(100)).await;
512 }
513 }
514
515 pub async fn clear(&self, options: Option<FillOptions>) -> Result<()> {
519 self.fill("", options).await
520 }
521
522 pub async fn dblclick(&self, options: Option<ClickOptions>) -> Result<()> {
524 let mut opts = options.unwrap_or_default();
525 opts.click_count = Some(opts.click_count.unwrap_or(2));
526 self.click(Some(opts)).await
527 }
528
529 pub async fn check(&self, options: Option<CheckOptions>) -> Result<()> {
531 self.set_checked(true, options).await
532 }
533
534 pub async fn uncheck(&self, options: Option<CheckOptions>) -> Result<()> {
536 self.set_checked(false, options).await
537 }
538
539 pub async fn set_checked(&self, checked: bool, options: Option<CheckOptions>) -> Result<()> {
541 let opts = options.unwrap_or_default();
542 let oid = self.resolve().await?;
543 let result = set_checked_element(&self.page, &oid, checked, &opts).await;
544 self.release(&oid).await;
545 result
546 }
547
548 pub async fn select_option(
550 &self,
551 value: impl Into<SelectOption>,
552 _options: Option<SelectOptions>,
553 ) -> Result<Vec<String>> {
554 let value = value.into();
555 let arg: Value = match value {
556 SelectOption::Value(v) => json!({ "Value": v }),
557 SelectOption::Label(v) => json!({ "Label": v }),
558 SelectOption::Index(i) => json!({ "Index": i }),
559 };
560 let oid = self.resolve().await?;
561 let result = select_option_element(&self.page, &oid, arg).await;
562 self.release(&oid).await;
563 result
564 }
565
566 pub async fn set_input_files(&self, files: &[&str]) -> Result<()> {
568 let oid = self.resolve().await?;
569 let desc = self
571 .page
572 .session()
573 .send("DOM.requestNode", json!({ "objectId": oid }))
574 .await?;
575 let node_id = desc
576 .get("nodeId")
577 .and_then(|v| v.as_i64())
578 .ok_or_else(|| Error::ProtocolError("requestNode missing nodeId".into()))?;
579 let r = self
580 .page
581 .session()
582 .send(
583 "DOM.setFileInputFiles",
584 json!({ "nodeId": node_id, "files": files }),
585 )
586 .await
587 .map(|_: Value| ());
588 self.release(&oid).await;
589 r
590 }
591
592 pub async fn tap(&self, options: Option<ClickOptions>) -> Result<()> {
594 self.click(options).await
595 }
596
597 pub async fn drag_to(&self, target: &Locator, _options: Option<DragToOptions>) -> Result<()> {
599 let oid = self.resolve().await?;
600 let target_oid = target.resolve().await?;
601 let (sx, sy) = element_center(&self.page, &oid).await?;
602 let (tx, ty) = element_center(&target.page, &target_oid).await?;
603 let r = drag_mouse(&self.page, sx, sy, tx, ty).await;
604 target.release(&target_oid).await;
605 self.release(&oid).await;
606 r
607 }
608
609 pub async fn highlight(&self) -> Result<()> {
617 let oid = self.resolve().await?;
618 let _ = self
620 .page
621 .session()
622 .send("Overlay.enable", json!({}))
623 .await;
624 let r = async {
625 let desc = self
627 .page
628 .session()
629 .send("DOM.requestNode", json!({ "objectId": oid }))
630 .await?;
631 let node_id = desc
632 .get("nodeId")
633 .and_then(|v| v.as_i64())
634 .ok_or_else(|| Error::ProtocolError("requestNode missing nodeId".into()))?;
635 self.page
638 .session()
639 .send(
640 "Overlay.highlightNode",
641 json!({
642 "highlightConfig": {
643 "showInfo": true,
644 "showStyles": false,
645 "contentColor": { "r": 111, "g": 168, "b": 220, "a": 0.66 },
646 "paddingColor": { "r": 147, "g": 196, "b": 125, "a": 0.55 },
647 "borderColor": { "r": 255, "g": 229, "b": 76, "a": 0.66 },
648 "marginColor": { "r": 246, "g": 178, "b": 107, "a": 0.66 }
649 },
650 "nodeId": node_id
651 }),
652 )
653 .await
654 .map(|_: Value| ())
655 }
656 .await;
657 self.release(&oid).await;
658 let _ = r;
661 Ok(())
662 }
663
664 pub async fn frame_locator(&self) -> Result<crate::frame_locator::FrameLocator> {
674 let oid = self.resolve().await?;
675 let fid = content_frame_id(&self.page, &oid).await;
676 self.release(&oid).await;
677 let fid = fid?;
678 if fid.is_none() {
679 return Err(Error::InvalidArgument(format!(
680 "element '{}' is not an iframe",
681 self.selector
682 )));
683 }
684 let frame_selector = match fid {
691 Some(fid) => format!("iframe >> internal:frame={fid}"),
692 None => "iframe".to_string(),
693 };
694 Ok(crate::frame_locator::FrameLocator::new(
695 self.page.clone(),
696 frame_selector,
697 ))
698 }
699
700 pub async fn drop(&self, target: &Locator) -> Result<()> {
709 let src_oid = self.resolve().await?;
710 let _ = self
711 .page
712 .session()
713 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": src_oid }))
714 .await;
715 let (sx, sy) = element_center(&self.page, &src_oid).await?;
716
717 let tgt_oid = target.resolve().await?;
718 let _ = self
719 .page
720 .session()
721 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": tgt_oid }))
722 .await;
723 let (tx, ty) = element_center(&target.page, &tgt_oid).await?;
724
725 let data = json!({
728 "items": [],
729 "files": [],
730 });
731 let mut out = Ok(());
732 if let Err(e) = self
734 .page
735 .session()
736 .send(
737 "Input.dispatchDragEvent",
738 json!({
739 "type": "dragStart",
740 "x": sx,
741 "y": sy,
742 "data": data
743 }),
744 )
745 .await
746 {
747 out = Err(e);
748 }
749 for ev in ["dragEnter", "dragOver", "drop"] {
751 if out.is_err() {
752 break;
753 }
754 if let Err(e) = self
755 .page
756 .session()
757 .send(
758 "Input.dispatchDragEvent",
759 json!({
760 "type": ev,
761 "x": tx,
762 "y": ty,
763 "data": data
764 }),
765 )
766 .await
767 {
768 out = Err(e);
769 }
770 }
771 target.release(&tgt_oid).await;
772 self.release(&src_oid).await;
773 out
774 }
775
776 pub async fn is_hidden(&self) -> Result<bool> {
779 Ok(!self.is_visible().await?)
780 }
781
782 pub async fn is_disabled(&self) -> Result<bool> {
783 let oid = self.resolve().await?;
784 let v = selectors::eval_object(
785 self.page.session(),
786 &oid,
787 "(el) => el.disabled === true",
788 Value::Null,
789 )
790 .await?;
791 self.release(&oid).await;
792 Ok(v.as_bool().unwrap_or(false))
793 }
794
795 pub async fn is_focused(&self) -> Result<bool> {
796 let oid = self.resolve().await?;
797 let v = selectors::eval_object(
798 self.page.session(),
799 &oid,
800 "(el) => el === document.activeElement",
801 Value::Null,
802 )
803 .await?;
804 self.release(&oid).await;
805 Ok(v.as_bool().unwrap_or(false))
806 }
807
808 pub async fn all(&self) -> Result<Vec<Locator>> {
810 let n = self.count().await?;
811 let mut out = Vec::with_capacity(n);
812 for i in 0..n {
813 let mut l = self.clone();
814 l.strict = false;
815 l.forced_index = Some(i);
816 out.push(l);
817 }
818 Ok(out)
819 }
820
821 pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
822 let n = self.count().await?;
823 let mut out = Vec::with_capacity(n);
824 for i in 0..n {
825 if let Some(oid) = self.element_query(i).await? {
826 let v = selectors::eval_object(
827 self.page.session(),
828 &oid,
829 "(el) => el.innerText",
830 Value::Null,
831 )
832 .await
833 .ok();
834 let _ = self
835 .page
836 .session()
837 .send("Runtime.releaseObject", json!({ "objectId": oid }))
838 .await;
839 out.push(v.and_then(|x| x.as_str().map(String::from)).unwrap_or_default());
840 }
841 }
842 Ok(out)
843 }
844
845 pub async fn all_text_contents(&self) -> Result<Vec<String>> {
846 let n = self.count().await?;
847 let mut out = Vec::with_capacity(n);
848 for i in 0..n {
849 if let Some(oid) = self.element_query(i).await? {
850 let v = selectors::eval_object(
851 self.page.session(),
852 &oid,
853 "(el) => el.textContent",
854 Value::Null,
855 )
856 .await
857 .ok();
858 let _ = self
859 .page
860 .session()
861 .send("Runtime.releaseObject", json!({ "objectId": oid }))
862 .await;
863 out.push(v.and_then(|x| x.as_str().map(String::from)).unwrap_or_default());
864 }
865 }
866 Ok(out)
867 }
868
869 pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
871 let oid = self.resolve().await?;
872 let (x, y, w, h) = element_box(&self.page, &oid).await?;
873 self.release(&oid).await;
874
875 let opts = opts.unwrap_or_default();
876 let format = match opts.r#type.unwrap_or_default() {
877 crate::types::ScreenshotType::Png => "png",
878 crate::types::ScreenshotType::Jpeg => "jpeg",
879 crate::types::ScreenshotType::Webp => "webp",
880 };
881 let mut params = json!({
882 "format": format,
883 "clip": { "x": x, "y": y, "width": w, "height": h, "scale": 1 },
884 });
885 if opts.omit_background.unwrap_or(false) && format == "png" {
886 params["omitBackground"] = json!(true);
887 }
888 if let Some(q) = opts.quality {
889 if format == "jpeg" {
890 params["quality"] = json!(q);
891 }
892 }
893 let resp = self
894 .page
895 .session()
896 .send("Page.captureScreenshot", params)
897 .await?;
898 let data = resp
899 .get("data")
900 .and_then(|v| v.as_str())
901 .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
902 let bytes = base64::engine::general_purpose::STANDARD
903 .decode(data)
904 .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
905 if let Some(path) = &opts.path {
906 tokio::fs::write(path, &bytes).await?;
907 }
908 Ok(bytes)
909 }
910
911 pub fn filter(&self, options: FilterOptions) -> Locator {
915 let mut sel = self.selector.clone();
916 if let Some(t) = options.has_text {
917 sel.push_str(&format!(" >> text={t}"));
918 }
919 let mut l = self.clone();
920 l.selector = sel;
921 l
922 }
923
924 pub fn and_(&self, other: &Locator) -> Locator {
926 let mut l = self.clone();
927 l.selector = format!("{} >> {}", self.selector, other.selector);
928 l
929 }
930
931 pub fn or_(&self, other: &Locator) -> Locator {
933 let mut l = self.clone();
934 l.selector = format!("{}, {}", self.selector, other.selector);
935 l
936 }
937
938 fn chain(&self, sub: impl Into<String>) -> Locator {
941 let mut l = self.clone();
942 l.selector = format!("{} >> {}", self.selector, sub.into());
943 l.forced_index = None;
944 l.strict = true;
945 l
946 }
947
948 pub fn locator(&self, selector: impl Into<String>) -> Locator {
950 self.chain(selector)
951 }
952
953 pub fn get_by_text(&self, text: &str, _exact: bool) -> Locator {
954 self.chain(format!("text={text}"))
955 }
956
957 pub fn get_by_label(&self, text: &str) -> Locator {
958 self.chain(format!("[aria-label=\"{}\"]", esc(text)))
959 }
960
961 pub fn get_by_placeholder(&self, text: &str) -> Locator {
962 self.chain(format!("[placeholder=\"{}\"]", esc(text)))
963 }
964
965 pub fn get_by_alt_text(&self, text: &str) -> Locator {
966 self.chain(format!("[alt=\"{}\"]", esc(text)))
967 }
968
969 pub fn get_by_title(&self, text: &str) -> Locator {
970 self.chain(format!("[title=\"{}\"]", esc(text)))
971 }
972
973 pub fn get_by_test_id(&self, test_id: &str) -> Locator {
974 self.chain(format!("[data-testid=\"{}\"]", esc(test_id)))
975 }
976
977 pub fn get_by_role(
978 &self,
979 role: crate::types::AriaRole,
980 opts: Option<crate::options::GetByRoleOptions>,
981 ) -> Locator {
982 let opts = opts.unwrap_or_default();
983 let mut sel = format!("role={}", role.as_str());
984 if let Some(name) = &opts.name {
985 sel.push_str(&format!("[name=\"{name}\"]"));
986 }
987 if opts.exact == Some(true) {
988 sel.push_str("[exact=\"true\"]");
989 }
990 self.chain(sel)
991 }
992
993 pub async fn aria_snapshot(&self) -> Result<String> {
1002 let oid = self.resolve().await?;
1003
1004 let desc = self
1010 .page
1011 .session()
1012 .send("DOM.describeNode", json!({ "objectId": oid }))
1013 .await;
1014 self.release(&oid).await;
1015 let desc = desc?;
1016 let backend_node_id = desc
1017 .get("node")
1018 .and_then(|n| n.get("backendNodeId"))
1019 .and_then(|v| v.as_i64())
1020 .ok_or_else(|| {
1021 Error::ProtocolError("DOM.describeNode missing backendNodeId".into())
1022 })?;
1023
1024 let _ = self
1026 .page
1027 .session()
1028 .send("Accessibility.enable", json!({}))
1029 .await;
1030 let resp = self
1031 .page
1032 .session()
1033 .send("Accessibility.getFullAXTree", json!({}))
1034 .await?;
1035 let nodes = resp
1036 .get("nodes")
1037 .and_then(|v| v.as_array())
1038 .cloned()
1039 .unwrap_or_default();
1040 Ok(crate::aria_snapshot::serialize(&nodes, Some(backend_node_id)))
1041 }
1042}
1043
1044fn esc(s: &str) -> String {
1045 s.replace('\\', "\\\\").replace('"', "\\\"")
1046}
1047
1048pub(crate) async fn element_box(page: &Page, object_id: &str) -> Result<(f64, f64, f64, f64)> {
1053 let resp = page
1054 .session()
1055 .send("DOM.getBoxModel", json!({ "objectId": object_id }))
1056 .await?;
1057 let model = resp
1058 .get("model")
1059 .ok_or_else(|| Error::ProtocolError("getBoxModel missing model".into()))?;
1060 let (x, y, w, h) = quad_box(model.get("content"))
1061 .ok_or_else(|| Error::ProtocolError("getBoxModel content quad malformed".into()))?;
1062 let mw = model.get("width").and_then(|v| v.as_f64());
1064 let mh = model.get("height").and_then(|v| v.as_f64());
1065 Ok((x, y, mw.unwrap_or(w), mh.unwrap_or(h)))
1066}
1067
1068pub(crate) async fn element_center(page: &Page, object_id: &str) -> Result<(f64, f64)> {
1070 let (x, y, w, h) = element_box(page, object_id).await?;
1071 Ok((x + w / 2.0, y + h / 2.0))
1072}
1073
1074pub(crate) async fn content_frame_id(page: &Page, object_id: &str) -> Result<Option<String>> {
1080 let desc = page
1081 .session()
1082 .send("DOM.describeNode", json!({ "objectId": object_id }))
1083 .await?;
1084 let node = desc
1085 .get("node")
1086 .ok_or_else(|| Error::ProtocolError("DOM.describeNode missing node".into()))?;
1087 let is_frame = node
1088 .get("localName")
1089 .and_then(|v| v.as_str())
1090 .map(|name| name.eq_ignore_ascii_case("iframe") || name.eq_ignore_ascii_case("frame"))
1091 .unwrap_or(false);
1092 if !is_frame {
1093 return Ok(None);
1094 }
1095 let fid = node
1096 .get("contentDocument")
1097 .and_then(|c| c.get("frameId"))
1098 .and_then(|v| v.as_str())
1099 .map(String::from);
1100 Ok(fid)
1101}
1102
1103pub(crate) async fn drag_mouse(
1107 page: &Page,
1108 sx: f64,
1109 sy: f64,
1110 tx: f64,
1111 ty: f64,
1112) -> Result<()> {
1113 page.session()
1114 .send(
1115 "Input.dispatchMouseEvent",
1116 json!({ "type": "mouseMoved", "x": sx, "y": sy, "button": "none", "buttons": 0 }),
1117 )
1118 .await?;
1119 page.session()
1120 .send(
1121 "Input.dispatchMouseEvent",
1122 json!({ "type": "mousePressed", "x": sx, "y": sy, "button": "left", "buttons": 1, "clickCount": 1 }),
1123 )
1124 .await?;
1125 let steps = 10;
1126 for i in 1..=steps {
1127 let x = sx + (tx - sx) * (i as f64) / (steps as f64);
1128 let y = sy + (ty - sy) * (i as f64) / (steps as f64);
1129 page.session()
1130 .send(
1131 "Input.dispatchMouseEvent",
1132 json!({ "type": "mouseMoved", "x": x, "y": y, "button": "none", "buttons": 1 }),
1133 )
1134 .await?;
1135 }
1136 page.session()
1137 .send(
1138 "Input.dispatchMouseEvent",
1139 json!({ "type": "mouseReleased", "x": tx, "y": ty, "button": "left", "buttons": 0, "clickCount": 1 }),
1140 )
1141 .await
1142 .map(|_: Value| ())
1143}
1144
1145pub(crate) async fn press_text(
1150 page: &Page,
1151 _object_id: &str,
1152 text: &str,
1153 delay: Option<Duration>,
1154) -> Result<()> {
1155 for ch in text.chars() {
1156 let key = ch.to_string();
1159 page.session()
1160 .send(
1161 "Input.dispatchKeyEvent",
1162 json!({
1163 "type": "char",
1164 "text": key,
1165 "key": key,
1166 }),
1167 )
1168 .await
1169 .map(|_: Value| ())?;
1170 if let Some(d) = delay {
1171 tokio::time::sleep(d).await;
1172 }
1173 }
1174 Ok(())
1175}
1176
1177pub(crate) async fn set_checked_element(
1180 page: &Page,
1181 object_id: &str,
1182 checked: bool,
1183 opts: &CheckOptions,
1184) -> Result<()> {
1185 let v = selectors::eval_object(
1187 page.session(),
1188 object_id,
1189 "(el) => el.checked === true",
1190 Value::Null,
1191 )
1192 .await?;
1193 let current = v.as_bool().unwrap_or(false);
1194 if current == checked {
1195 return Ok(());
1196 }
1197 let click_opts = ClickOptions {
1198 force: opts.force,
1199 position: opts.position,
1200 timeout: opts.timeout,
1201 trial: opts.trial,
1202 ..Default::default()
1203 };
1204 click_element(page, object_id, &click_opts).await
1205}
1206
1207pub(crate) async fn hover_element(page: &Page, object_id: &str, opts: &HoverOptions) -> Result<()> {
1210 let _ = page
1211 .session()
1212 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": object_id }))
1213 .await;
1214 let (bx, by, bw, bh) = element_box(page, object_id).await?;
1215 let (x, y) = match &opts.position {
1216 Some(p) => (bx + p.x, by + p.y),
1217 None => (bx + bw / 2.0, by + bh / 2.0),
1218 };
1219 page.session()
1220 .send(
1221 "Input.dispatchMouseEvent",
1222 json!({ "type": "mouseMoved", "x": x, "y": y, "button": "none", "buttons": 0 }),
1223 )
1224 .await
1225 .map(|_: Value| ())
1226}
1227
1228pub(crate) async fn press_key(
1231 page: &Page,
1232 object_id: &str,
1233 key: &str,
1234 delay: Option<Duration>,
1235) -> Result<()> {
1236 let _ = page
1237 .session()
1238 .send("DOM.focus", json!({ "objectId": object_id }))
1239 .await;
1240 page.session()
1241 .send("Input.dispatchKeyEvent", json!({ "type": "keyDown", "key": key }))
1242 .await
1243 .map(|_: Value| ())?;
1244 if let Some(d) = delay {
1245 tokio::time::sleep(d).await;
1246 }
1247 page.session()
1248 .send("Input.dispatchKeyEvent", json!({ "type": "keyUp", "key": key }))
1249 .await
1250 .map(|_: Value| ())
1251}
1252
1253pub(crate) async fn select_option_element(
1257 page: &Page,
1258 object_id: &str,
1259 arg: Value,
1260) -> Result<Vec<String>> {
1261 let v: Value = selectors::eval_object(
1262 page.session(),
1263 object_id,
1264 "(el, sel) => {
1265 const opts = Array.from(el.options || []);
1266 const matched = [];
1267 for (const o of opts) {
1268 const hit = sel.Value != null ? o.value === sel.Value
1269 : sel.Label != null ? o.label === sel.Label
1270 : sel.Index != null ? o.index === sel.Index : false;
1271 if (hit) { o.selected = true; matched.push(o.value); }
1272 }
1273 el.dispatchEvent(new Event('input', { bubbles: true }));
1274 el.dispatchEvent(new Event('change', { bubbles: true }));
1275 return matched;
1276 }",
1277 arg,
1278 )
1279 .await?;
1280 Ok(v.as_array()
1281 .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
1282 .unwrap_or_default())
1283}
1284
1285async fn wait_actionable(
1287 page: &Page,
1288 object_id: &str,
1289 timeout: Duration,
1290) -> Result<()> {
1291 let function = "(el) => self.__pwcdpInjected && self.__pwcdpInjected.elementState(el).visible";
1292 let deadline = tokio::time::Instant::now() + timeout;
1293 loop {
1294 let v = selectors::eval_object(page.session(), object_id, function, Value::Null).await?;
1295 if v.as_bool() == Some(true) {
1296 return Ok(());
1297 }
1298 if tokio::time::Instant::now() >= deadline {
1299 return Err(Error::Timeout(format!(
1300 "element not visible within {}ms",
1301 timeout.as_millis()
1302 )));
1303 }
1304 tokio::time::sleep(Duration::from_millis(100)).await;
1305 }
1306}
1307
1308fn modifier_key(m: &crate::types::KeyboardModifier) -> &'static str {
1309 use crate::types::KeyboardModifier::*;
1310 match m {
1311 Alt => "Alt",
1312 Control => "Control",
1313 ControlOrMeta => "Control",
1314 Meta => "Meta",
1315 Shift => "Shift",
1316 }
1317}
1318
1319pub(crate) async fn click_element(
1323 page: &Page,
1324 object_id: &str,
1325 opts: &ClickOptions,
1326) -> Result<()> {
1327 let _ = page
1328 .session()
1329 .send("DOM.scrollIntoViewIfNeeded", json!({ "objectId": object_id }))
1330 .await;
1331
1332 let force = opts.force.unwrap_or(false);
1333 let trial = opts.trial.unwrap_or(false);
1334 if !force {
1335 let timeout = opts
1336 .timeout
1337 .map(|ms| Duration::from_millis(ms as u64))
1338 .unwrap_or_else(|| page.default_timeout());
1339 wait_actionable(page, object_id, timeout).await?;
1340 }
1341
1342 let (bx, by, bw, bh) = element_box(page, object_id).await?;
1343 let (x, y) = match &opts.position {
1344 Some(p) => (bx + p.x, by + p.y),
1345 None => (bx + bw / 2.0, by + bh / 2.0),
1346 };
1347 let button = opts.button.unwrap_or_default();
1348 let count = opts.click_count.unwrap_or(1);
1349 let delay = opts.delay.map(|ms| Duration::from_millis(ms as u64));
1350
1351 if let Some(mods) = &opts.modifiers {
1353 for m in mods {
1354 let _ = page
1355 .session()
1356 .send(
1357 "Input.dispatchKeyEvent",
1358 json!({ "type": "keyDown", "key": modifier_key(m) }),
1359 )
1360 .await;
1361 }
1362 }
1363
1364 let result = if trial {
1365 Ok(())
1366 } else {
1367 let mut out = Ok(());
1368 page.session()
1369 .send(
1370 "Input.dispatchMouseEvent",
1371 json!({ "type": "mouseMoved", "x": x, "y": y, "button": "none", "buttons": 0 }),
1372 )
1373 .await?;
1374 for i in 0..count {
1375 if let Err(e) = page
1376 .session()
1377 .send(
1378 "Input.dispatchMouseEvent",
1379 json!({ "type": "mousePressed", "x": x, "y": y, "button": button.as_str(), "buttons": button.bitmask(), "clickCount": i + 1 }),
1380 )
1381 .await
1382 {
1383 out = Err(e);
1384 break;
1385 }
1386 if let Err(e) = page
1387 .session()
1388 .send(
1389 "Input.dispatchMouseEvent",
1390 json!({ "type": "mouseReleased", "x": x, "y": y, "button": button.as_str(), "buttons": 0, "clickCount": i + 1 }),
1391 )
1392 .await
1393 {
1394 out = Err(e);
1395 break;
1396 }
1397 if let Some(d) = delay {
1398 if i + 1 < count {
1399 tokio::time::sleep(d).await;
1400 }
1401 }
1402 }
1403 out
1404 };
1405
1406 if let Some(mods) = &opts.modifiers {
1408 for m in mods {
1409 let _ = page
1410 .session()
1411 .send(
1412 "Input.dispatchKeyEvent",
1413 json!({ "type": "keyUp", "key": modifier_key(m) }),
1414 )
1415 .await;
1416 }
1417 }
1418
1419 result
1420}
1421
1422fn quad_box(quad: Option<&Value>) -> Option<(f64, f64, f64, f64)> {
1423 let arr = quad.and_then(|v| v.as_array())?;
1424 let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
1425 if nums.len() < 8 {
1426 return None;
1427 }
1428 let xs = [nums[0], nums[2], nums[4], nums[6]];
1429 let ys = [nums[1], nums[3], nums[5], nums[7]];
1430 let xmin = xs.iter().cloned().fold(f64::INFINITY, f64::min);
1431 let xmax = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1432 let ymin = ys.iter().cloned().fold(f64::INFINITY, f64::min);
1433 let ymax = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1434 Some((xmin, ymin, xmax - xmin, ymax - ymin))
1435}