1use crate::browser::Browser;
5use crate::api_request::APIRequestContext;
6use crate::cdp::session::CdpSession;
7use crate::download::{Download, DownloadState, DownloadStateCell};
8use crate::element_handle::ElementHandle;
9use crate::error::{Error, Result};
10use crate::file_chooser::FileChooser;
11use crate::frame::Frame;
12use crate::frame_locator::FrameLocator;
13use crate::keyboard::Keyboard;
14use crate::locator::Locator;
15use crate::mouse::Mouse;
16use crate::network::{network_tracker, NetworkStore, RequestHandler, ResponseHandler};
17use crate::options::{
18 DragToOptions, EmulateMediaOptions, GotoOptions, ScreenshotOptions, WaitForFunctionOptions, WaitUntil,
19};
20use crate::request::Request;
21use crate::response::Response;
22use crate::route::{route_listener, Route, RouteEntry};
23use crate::selectors;
24use crate::touchscreen::Touchscreen;
25use crate::types::{AriaRole, ConsoleMessage, Headers, Viewport};
26use crate::worker::Worker;
27use base64::Engine;
28use parking_lot::Mutex;
29use serde::de::DeserializeOwned;
30use serde_json::{json, Value};
31use std::collections::HashMap;
32use std::future::Future;
33use std::path::PathBuf;
34use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
35use std::sync::Arc;
36use std::time::Duration;
37use tokio::sync::broadcast;
38
39#[derive(Clone)]
41pub struct Page {
42 inner: Arc<PageInner>,
43}
44
45type CloseHandler =
46 Arc<dyn Fn() -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
47
48type ExposedBindingHandler =
50 Arc<dyn Fn(Vec<Value>) -> std::pin::Pin<Box<dyn Future<Output = Value> + Send>> + Send + Sync>;
51
52struct PageInner {
53 browser: Browser,
54 session: Arc<CdpSession>,
55 target_id: String,
56 main_world_ctx: Arc<Mutex<Option<i64>>>,
57 default_timeout_ms: AtomicU64,
58 default_navigation_timeout_ms: AtomicU64,
59 viewport: Mutex<Option<Viewport>>,
60 mouse_pos: Arc<Mutex<(f64, f64)>>,
61 network_store: Arc<NetworkStore>,
62 on_request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
63 on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
64 on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
65 frames: Arc<Mutex<HashMap<String, FrameData>>>,
66 main_frame_id: Arc<Mutex<Option<String>>>,
67 route_handlers: Arc<Mutex<Vec<RouteEntry>>>,
68 route_started: AtomicBool,
69 on_close_handlers: Mutex<Vec<CloseHandler>>,
70 closed: Mutex<bool>,
71 download_dir: Mutex<Option<Arc<tempfile::TempDir>>>,
74 download_path: Mutex<Option<PathBuf>>,
75 download_states: Arc<Mutex<HashMap<String, DownloadStateCell>>>,
76 download_started: AtomicBool,
77 filechooser_started: AtomicBool,
79 worker_started: AtomicBool,
81 touch_emulation_started: Arc<AtomicBool>,
84}
85
86#[derive(Clone)]
88pub(crate) struct FrameData {
89 pub url: String,
90 pub name: String,
91 pub parent_id: Option<String>,
92 pub detached: bool,
93}
94
95impl Page {
96 #[allow(clippy::too_many_arguments)]
99 pub(crate) async fn attach(
100 browser: Browser,
101 session_id: String,
102 target_id: String,
103 init_scripts: &[String],
104 default_timeout_ms: u64,
105 extra_headers: Option<&Headers>,
106 user_agent: Option<&str>,
107 viewport: Option<Viewport>,
108 ) -> Result<Page> {
109 let session = Arc::new(CdpSession::target(
110 browser.connection().clone(),
111 session_id,
112 ));
113 session.set_default_timeout_ms(default_timeout_ms);
114
115 let main_world_ctx: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
116 let ctx_rx = session.subscribe();
117 {
120 let session_for_task = Arc::clone(&session);
121 let ctx_cell = Arc::clone(&main_world_ctx);
122 tokio::spawn(async move {
123 context_tracker(ctx_rx, session_for_task, ctx_cell).await;
124 });
125 }
126
127 let network_store = NetworkStore::new();
128 let on_request_handlers: Arc<Mutex<Vec<RequestHandler>>> = Arc::new(Mutex::new(Vec::new()));
130 let on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>> =
131 Arc::new(Mutex::new(Vec::new()));
132 let on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>> =
133 Arc::new(Mutex::new(Vec::new()));
134
135 {
137 let net_rx = session.subscribe();
138 let session_for_task = Arc::clone(&session);
139 let store_for_task = Arc::clone(&network_store);
140 let on_request = Arc::clone(&on_request_handlers);
141 let on_response = Arc::clone(&on_response_handlers);
142 let on_failed = Arc::clone(&on_requestfailed_handlers);
143 tokio::spawn(async move {
144 network_tracker(
145 net_rx,
146 session_for_task,
147 store_for_task,
148 on_request,
149 on_response,
150 on_failed,
151 )
152 .await;
153 });
154 }
155
156 let page = Page {
157 inner: Arc::new(PageInner {
158 browser,
159 session: Arc::clone(&session),
160 target_id,
161 main_world_ctx: Arc::clone(&main_world_ctx),
162 default_timeout_ms: AtomicU64::new(default_timeout_ms),
163 default_navigation_timeout_ms: AtomicU64::new(default_timeout_ms),
164 viewport: Mutex::new(viewport),
165 mouse_pos: Arc::new(Mutex::new((0.0, 0.0))),
166 network_store,
167 on_request_handlers,
168 on_response_handlers,
169 on_requestfailed_handlers,
170 frames: Arc::new(Mutex::new(HashMap::new())),
171 main_frame_id: Arc::new(Mutex::new(None)),
172 route_handlers: Arc::new(Mutex::new(Vec::new())),
173 route_started: AtomicBool::new(false),
174 on_close_handlers: Mutex::new(Vec::new()),
175 closed: Mutex::new(false),
176 download_dir: Mutex::new(None),
177 download_path: Mutex::new(None),
178 download_states: Arc::new(Mutex::new(HashMap::new())),
179 download_started: AtomicBool::new(false),
180 filechooser_started: AtomicBool::new(false),
181 worker_started: AtomicBool::new(false),
182 touch_emulation_started: Arc::new(AtomicBool::new(false)),
183 }),
184 };
185
186 {
188 let rx = session.subscribe();
189 let frames_store = Arc::clone(&page.inner.frames);
190 let main_id = Arc::clone(&page.inner.main_frame_id);
191 tokio::spawn(async move {
192 frame_tracker(rx, frames_store, main_id).await;
193 });
194 }
195 let _ = page.refresh_frame_tree().await;
197
198 let _ = session.send("Page.enable", json!({})).await;
201 let _ = session.send("Runtime.enable", json!({})).await;
202 let _ = session.send("Network.enable", json!({})).await;
203 let _ = session.send("Log.enable", json!({})).await;
204
205 let mut source = String::from(selectors::INJECTED_SCRIPT);
207 for s in init_scripts {
208 source.push_str("\n");
209 source.push_str(s);
210 }
211 let _ = session
212 .send(
213 "Page.addScriptToEvaluateOnNewDocument",
214 json!({ "source": source }),
215 )
216 .await;
217
218 page.ensure_engine_in_current_context().await;
220
221 if let Some(headers) = extra_headers {
222 let _ = page.set_extra_http_headers(headers.clone()).await;
223 }
224 if let Some(ua) = user_agent {
225 let _ = session
226 .send("Emulation.setUserAgentOverride", json!({ "userAgent": ua }))
227 .await;
228 }
229 if let Some(vp) = page.inner.viewport.lock().as_ref().copied() {
230 let _ = page.set_viewport_size(vp).await;
231 }
232
233 Ok(page)
234 }
235
236 pub(crate) fn session(&self) -> &CdpSession {
239 &self.inner.session
240 }
241
242 pub(crate) fn context_id(&self) -> Option<i64> {
243 self.inner.main_world_ctx.lock().as_ref().copied()
244 }
245
246 pub(crate) async fn ctx(&self) -> Option<i64> {
250 let deadline = tokio::time::Instant::now() + Duration::from_millis(2000);
251 loop {
252 if let Some(id) = self.context_id() {
253 return Some(id);
254 }
255 if tokio::time::Instant::now() >= deadline {
256 return self.context_id();
257 }
258 tokio::time::sleep(Duration::from_millis(20)).await;
259 }
260 }
261
262 pub fn browser(&self) -> Browser {
263 self.inner.browser.clone()
264 }
265
266 pub fn target_id(&self) -> &str {
267 &self.inner.target_id
268 }
269
270 pub fn is_closed(&self) -> bool {
271 *self.inner.closed.lock()
272 }
273
274 pub(crate) fn set_default_timeout(&self, ms: u64) {
275 self.inner.default_timeout_ms.store(ms, Ordering::Relaxed);
276 self.inner.session.set_default_timeout_ms(ms);
277 }
278
279 pub(crate) fn default_timeout(&self) -> Duration {
280 Duration::from_millis(self.inner.default_timeout_ms.load(Ordering::Relaxed))
281 }
282
283 pub(crate) fn default_navigation_timeout(&self) -> Duration {
284 Duration::from_millis(self.inner.default_navigation_timeout_ms.load(Ordering::Relaxed))
285 }
286
287 async fn ensure_engine_in_current_context(&self) {
288 let _ = selectors::eval_context(
290 &self.inner.session,
291 self.ctx().await,
292 "(arg) => { void arg; }",
293 Value::Null,
294 )
295 .await;
296 }
297
298 pub async fn goto(
302 &self,
303 url: &str,
304 opts: Option<GotoOptions>,
305 ) -> Result<Option<Response>> {
306 let opts = opts.unwrap_or_default();
307 let wait = opts.wait_until_or_default();
308
309 let mut rx = self.inner.session.subscribe();
311 let mut params = json!({ "url": url });
312 if let Some(referer) = &opts.referer {
313 params["referer"] = json!(referer);
314 }
315 let nav = self.inner.session.send("Page.navigate", params).await?;
316 if let Some(err) = nav.get("errorText").and_then(|v| v.as_str()) {
317 return Err(Error::ProtocolError(format!("navigation to {url} failed: {err}")));
318 }
319 let loader_id = nav.get("loaderId").and_then(|v| v.as_str()).map(String::from);
320
321 let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
322 self.wait_lifecycle(&mut rx, loader_id.as_deref(), wait, timeout, url)
323 .await?;
324
325 let response = match &loader_id {
327 Some(lid) => self.wait_for_nav_response(lid).await,
328 None => None,
329 };
330 Ok(response)
331 }
332
333 async fn wait_for_nav_response(&self, loader_id: &str) -> Option<Response> {
335 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
336 loop {
337 if let Some(r) = self.inner.network_store.response_for_loader(loader_id) {
338 return Some(r);
339 }
340 if tokio::time::Instant::now() >= deadline {
341 return None;
342 }
343 tokio::time::sleep(Duration::from_millis(50)).await;
344 }
345 }
346
347 async fn wait_network_idle(&self, deadline: tokio::time::Instant) {
349 let window = Duration::from_millis(500);
350 loop {
351 if self.inner.network_store.inflight() == 0 {
352 let settled_at = tokio::time::Instant::now();
353 loop {
354 if self.inner.network_store.inflight() != 0 {
355 break; }
357 if tokio::time::Instant::now().duration_since(settled_at) >= window {
358 return; }
360 if tokio::time::Instant::now() >= deadline {
361 return; }
363 tokio::time::sleep(Duration::from_millis(50)).await;
364 }
365 }
366 if tokio::time::Instant::now() >= deadline {
367 return;
368 }
369 tokio::time::sleep(Duration::from_millis(50)).await;
370 }
371 }
372
373 async fn wait_lifecycle(
374 &self,
375 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
376 loader_id: Option<&str>,
377 wait: WaitUntil,
378 timeout: Duration,
379 url: &str,
380 ) -> Result<()> {
381 if matches!(wait, WaitUntil::Commit) {
382 return Ok(()); }
384 let target_name = match wait {
385 WaitUntil::Load => "load",
386 WaitUntil::DomContentLoaded => "DOMContentLoaded",
387 WaitUntil::NetworkIdle => "load", WaitUntil::Commit => "load",
389 };
390
391 let deadline = tokio::time::Instant::now() + timeout;
392 loop {
393 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
394 match tokio::time::timeout(remaining, rx.recv()).await {
395 Err(_) => {
396 return Err(Error::NavigationTimeout {
397 url: url.to_string(),
398 duration_ms: timeout.as_millis() as u64,
399 })
400 }
401 Ok(Err(broadcast::error::RecvError::Closed)) => {
402 return Err(Error::ChannelClosed);
403 }
404 Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
405 Ok(Ok(ev)) => {
406 let matched = match ev.method.as_str() {
411 "Page.lifecycleEvent" => {
412 let same_loader = loader_id
413 .map(|l| {
414 ev.params.get("loaderId").and_then(|v| v.as_str()) == Some(l)
415 })
416 .unwrap_or(true);
417 let name = ev.params.get("name").and_then(|v| v.as_str()).unwrap_or("");
418 same_loader && name == target_name
419 }
420 "Page.loadEventFired" => target_name == "load",
421 "Page.domContentEventFired" => target_name == "DOMContentLoaded",
422 _ => false,
423 };
424 if matched {
425 if matches!(wait, WaitUntil::NetworkIdle) {
426 self.wait_network_idle(deadline).await;
429 }
430 return Ok(());
431 }
432 }
433 }
434 }
435 }
436
437 pub async fn reload(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
439 let opts = opts.unwrap_or_default();
440 let wait = opts.wait_until_or_default();
441 let mut rx = self.inner.session.subscribe();
442 let _ = self.inner.session.send("Page.reload", json!({})).await?;
443 let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
444 self.wait_lifecycle(&mut rx, None, wait, timeout, "reload").await?;
445 Ok(None)
446 }
447
448 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
450 let state = state.unwrap_or_default();
451 let mut rx = self.inner.session.subscribe();
452 self.wait_lifecycle(&mut rx, None, state, self.default_timeout(), "wait_for_load_state")
453 .await
454 }
455
456 pub async fn go_back(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
458 self.history_nav("history.back()", opts).await
459 }
460
461 pub async fn go_forward(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
463 self.history_nav("history.forward()", opts).await
464 }
465
466 async fn history_nav(
467 &self,
468 expr: &str,
469 opts: Option<GotoOptions>,
470 ) -> Result<Option<Response>> {
471 let opts = opts.unwrap_or_default();
472 let navigated: bool = self.evaluate(expr).await.unwrap_or(false);
474 if navigated {
475 let wait = opts.wait_until_or_default();
476 let mut rx = self.inner.session.subscribe();
477 let timeout = opts
478 .timeout
479 .unwrap_or_else(|| self.default_navigation_timeout());
480 let _ = self
481 .wait_lifecycle(&mut rx, None, wait, timeout, "history navigation")
482 .await;
483 }
484 Ok(None)
485 }
486
487 pub async fn wait_for_url(&self, url: &str, opts: Option<GotoOptions>) -> Result<()> {
490 let opts = opts.unwrap_or_default();
491 let timeout = opts
492 .timeout
493 .unwrap_or_else(|| self.default_navigation_timeout());
494 let deadline = tokio::time::Instant::now() + timeout;
495 loop {
496 let cur = self.url().await.unwrap_or_default();
497 if glob_matches(url, &cur) {
498 break;
499 }
500 if tokio::time::Instant::now() >= deadline {
501 return Err(Error::Timeout(format!(
502 "wait_for_url '{url}' timed out after {}ms (last: '{cur}')",
503 timeout.as_millis()
504 )));
505 }
506 tokio::time::sleep(Duration::from_millis(100)).await;
507 }
508 if let Some(state) = opts.wait_until {
509 self.wait_for_load_state(Some(state)).await?;
510 }
511 Ok(())
512 }
513
514 pub fn set_default_navigation_timeout(&self, ms: u64) {
516 self.inner
517 .default_navigation_timeout_ms
518 .store(ms, Ordering::Relaxed);
519 }
520
521 pub async fn emulate_media(&self, opts: Option<EmulateMediaOptions>) -> Result<()> {
523 use crate::options::{ColorScheme, Media, ReducedMotion};
524 let opts = opts.unwrap_or_default();
525 let mut params = json!({});
526 if let Some(m) = opts.media {
527 params["media"] = json!(match m {
528 Media::Screen => "screen",
529 Media::Print => "print",
530 });
531 }
532 let mut features = Vec::new();
533 if let Some(cs) = opts.color_scheme {
534 features.push(json!({
535 "name": "prefers-color-scheme",
536 "value": match cs {
537 ColorScheme::Light => "light",
538 ColorScheme::Dark => "dark",
539 ColorScheme::NoPreference => "no-preference",
540 }
541 }));
542 }
543 if let Some(rm) = opts.reduced_motion {
544 features.push(json!({
545 "name": "prefers-reduced-motion",
546 "value": match rm {
547 ReducedMotion::Reduce => "reduce",
548 ReducedMotion::NoPreference => "no-preference",
549 }
550 }));
551 }
552 if !features.is_empty() {
553 params["features"] = json!(features);
554 }
555 self.inner
556 .session
557 .send("Emulation.setEmulatedMedia", params)
558 .await
559 .map(|_: Value| ())
560 }
561
562 pub async fn set_offline(&self, offline: bool) -> Result<()> {
564 let params = if offline {
565 json!({ "offline": true, "latency": 0, "downloadThroughput": 0, "uploadThroughput": 0 })
566 } else {
567 json!({ "offline": false, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1 })
568 };
569 self.inner
570 .session
571 .send("Network.emulateNetworkConditions", params)
572 .await
573 .map(|_: Value| ())
574 }
575
576 pub async fn drag_and_drop(
578 &self,
579 source: &str,
580 target: &str,
581 options: Option<DragToOptions>,
582 ) -> Result<()> {
583 let src = self.locator(source);
584 let tgt = self.locator(target);
585 src.drag_to(&tgt, options).await
586 }
587
588 pub async fn url(&self) -> Result<String> {
591 self.evaluate::<String>("location.href").await
592 }
593
594 pub async fn title(&self) -> Result<String> {
595 self.evaluate::<String>("document.title").await
596 }
597
598 pub async fn content(&self) -> Result<String> {
599 self.evaluate::<String>("document.documentElement.outerHTML").await
600 }
601
602 pub async fn set_content(&self, html: &str) -> Result<()> {
603 let _ = selectors::eval_context(
604 &self.inner.session,
605 self.ctx().await,
606 "(html) => { document.open(); document.write(html); document.close(); }",
607 json!(html),
608 )
609 .await?;
610 Ok(())
611 }
612
613 pub async fn evaluate<R: DeserializeOwned>(&self, expression: &str) -> Result<R> {
617 let function = format!("(arg) => {{ return ({expression}); }}");
618 let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, Value::Null)
619 .await?;
620 serde_json::from_value::<R>(v).map_err(Error::from)
621 }
622
623 pub async fn evaluate_with_arg<R: DeserializeOwned, T: serde::Serialize>(
625 &self,
626 expression: &str,
627 arg: &T,
628 ) -> Result<R> {
629 let function = format!("(arg) => {{ return ({expression}); }}");
630 let arg_val = serde_json::to_value(arg)?;
631 let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val)
632 .await?;
633 serde_json::from_value::<R>(v).map_err(Error::from)
634 }
635
636 pub async fn wait_for_function<R: DeserializeOwned>(
644 &self,
645 expression: &str,
646 arg: Option<Value>,
647 options: Option<WaitForFunctionOptions>,
648 ) -> Result<R> {
649 let opts = options.unwrap_or_default();
650 let timeout = Duration::from_millis(opts.timeout.unwrap_or_else(|| self.default_timeout().as_millis() as f64) as u64);
651 let poll = Duration::from_millis(opts.polling_interval.unwrap_or(100.0) as u64);
652 let function = format!("(arg) => {{ return ({expression}); }}");
653 let arg_val = arg.unwrap_or(Value::Null);
654
655 let deadline = tokio::time::Instant::now() + timeout;
656 loop {
657 let v =
658 selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val.clone())
659 .await?;
660 if is_truthy(&v) {
661 return serde_json::from_value::<R>(v).map_err(Error::from);
662 }
663 if tokio::time::Instant::now() >= deadline {
664 return Err(Error::Timeout(format!(
665 "wait_for_function timed out after {}ms (expression: {expression})",
666 timeout.as_millis()
667 )));
668 }
669 tokio::time::sleep(poll).await;
670 }
671 }
672
673 pub async fn eval_on_selector<R: DeserializeOwned>(
675 &self,
676 selector: &str,
677 expression: &str,
678 ) -> Result<R> {
679 let object_id = self
680 .resolve_strict(selector, Some(0))
681 .await?
682 .ok_or_else(|| Error::ElementNotFound(selector.to_string()))?;
683 let function = format!("(el) => {{ return ({expression}); }}");
684 let v = selectors::eval_object(&self.inner.session, &object_id, &function, Value::Null)
685 .await?;
686 self.release_object(&object_id).await;
687 serde_json::from_value::<R>(v).map_err(Error::from)
688 }
689
690 pub async fn eval_on_selector_all<R: DeserializeOwned>(
692 &self,
693 selector: &str,
694 expression: &str,
695 ) -> Result<Vec<R>> {
696 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
697 let mut out = Vec::with_capacity(n);
698 for i in 0..n {
699 if let Some(oid) = selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await? {
700 let function = format!("(el) => {{ return ({expression}); }}");
701 let v = selectors::eval_object(&self.inner.session, &oid, &function, Value::Null).await?;
702 self.release_object(&oid).await;
703 out.push(serde_json::from_value::<R>(v).map_err(Error::from)?);
704 }
705 }
706 Ok(out)
707 }
708
709 async fn release_object(&self, object_id: &str) {
710 let _ = self
711 .inner
712 .session
713 .send("Runtime.releaseObject", json!({ "objectId": object_id }))
714 .await;
715 }
716
717 pub async fn set_viewport_size(&self, viewport: Viewport) -> Result<()> {
720 *self.inner.viewport.lock() = Some(viewport);
721 self.inner
722 .session
723 .send(
724 "Emulation.setDeviceMetricsOverride",
725 json!({
726 "width": viewport.width,
727 "height": viewport.height,
728 "deviceScaleFactor": 1,
729 "mobile": false,
730 }),
731 )
732 .await?;
733 Ok(())
734 }
735
736 pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
737 let opts = opts.unwrap_or_default();
738 let format = match opts.r#type.unwrap_or_default() {
739 crate::types::ScreenshotType::Png => "png",
740 crate::types::ScreenshotType::Jpeg => "jpeg",
741 crate::types::ScreenshotType::Webp => "webp",
742 };
743 let mut params = json!({ "format": format });
744 if opts.full_page.unwrap_or(false) {
745 params["captureBeyondViewport"] = json!(true);
746 }
747 if opts.omit_background.unwrap_or(false) && format == "png" {
748 params["omitBackground"] = json!(true);
749 }
750 let resp = self
751 .inner
752 .session
753 .send("Page.captureScreenshot", params)
754 .await?;
755 let data = resp
756 .get("data")
757 .and_then(|v| v.as_str())
758 .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
759 let bytes = base64::engine::general_purpose::STANDARD
760 .decode(data)
761 .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
762 if let Some(path) = &opts.path {
763 tokio::fs::write(path, &bytes).await?;
764 }
765 Ok(bytes)
766 }
767
768 pub async fn add_script_tag(
772 &self,
773 content: Option<&str>,
774 url: Option<&str>,
775 ) -> Result<()> {
776 let arg = json!({ "content": content, "url": url });
777 let _ = selectors::eval_context(
778 &self.inner.session,
779 self.ctx().await,
780 "(a) => { const s = document.createElement('script'); if (a.url) s.src = a.url; if (a.content) s.textContent = a.content; document.head.appendChild(s); }",
781 arg,
782 )
783 .await?;
784 Ok(())
785 }
786
787 pub async fn add_style_tag(&self, content: &str) -> Result<()> {
789 let _ = selectors::eval_context(
790 &self.inner.session,
791 self.ctx().await,
792 "(a) => { const s = document.createElement('style'); s.textContent = a; document.head.appendChild(s); }",
793 json!(content),
794 )
795 .await?;
796 Ok(())
797 }
798
799 pub async fn set_cache_disabled(&self, disabled: bool) -> Result<()> {
801 self.inner
802 .session
803 .send("Network.setCacheDisabled", json!({ "cacheDisabled": disabled }))
804 .await
805 .map(|_: Value| ())
806 }
807
808 pub async fn bring_to_front(&self) -> Result<()> {
810 self.inner
811 .session
812 .send("Page.bringToFront", json!({}))
813 .await
814 .map(|_: Value| ())
815 }
816
817 pub async fn set_geolocation(&self, geolocation: Option<(f64, f64)>) -> Result<()> {
820 let params = match geolocation {
821 Some((lat, lon)) => json!({ "latitude": lat, "longitude": lon }),
822 None => json!({}),
823 };
824 self.inner
825 .session
826 .send("Emulation.setGeolocationOverride", params)
827 .await
828 .map(|_: Value| ())
829 }
830
831 pub fn request(&self) -> APIRequestContext {
840 APIRequestContext::new(Headers::new())
841 }
842
843 pub async fn set_extra_http_headers(&self, headers: Headers) -> Result<()> {
844 let list: Vec<Value> = headers
846 .iter()
847 .map(|(k, v)| json!({ "name": k, "value": v }))
848 .collect();
849 self.inner
850 .session
851 .send("Network.setExtraHTTPHeaders", json!({ "headers": list }))
852 .await?;
853 Ok(())
854 }
855
856 pub async fn add_init_script(&self, script: &str) -> Result<()> {
857 let _ = self
858 .inner
859 .session
860 .send(
861 "Page.addScriptToEvaluateOnNewDocument",
862 json!({ "source": script }),
863 )
864 .await;
865 let _ = selectors::eval_context(
867 &self.inner.session,
868 self.ctx().await,
869 "(s) => { (0, eval)(s); }",
870 json!(script),
871 )
872 .await;
873 Ok(())
874 }
875
876 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
903 where
904 F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
905 Fut: Future<Output = Value> + Send + 'static,
906 {
907 let handler: ExposedBindingHandler =
910 Arc::new(move |args| Box::pin(callback(args)));
911
912 let binding_name = internal_binding_name(name);
915 let _ = self
916 .inner
917 .session
918 .send("Runtime.addBinding", json!({ "name": binding_name }))
919 .await;
920
921 let wrapper = binding_wrapper_source(name);
923 let _ = self
924 .inner
925 .session
926 .send(
927 "Page.addScriptToEvaluateOnNewDocument",
928 json!({ "source": wrapper }),
929 )
930 .await;
931 let _ = selectors::eval_context(
933 &self.inner.session,
934 self.ctx().await,
935 "(s) => { (0, eval)(s); }",
936 json!(wrapper),
937 )
938 .await;
939
940 let mut rx = self.inner.session.subscribe();
942 let session = Arc::clone(&self.inner.session);
943 let name_owned = name.to_string();
944 tokio::spawn(async move {
945 binding_listener(&mut rx, &session, &name_owned, &handler).await;
946 });
947
948 Ok(())
949 }
950
951 pub fn locator(&self, selector: impl Into<String>) -> Locator {
954 Locator::new(self.clone(), selector.into(), true, None)
955 }
956
957 pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator {
966 FrameLocator::new(self.clone(), selector.into())
967 }
968
969 pub fn get_by_text(&self, text: &str, _exact: bool) -> Locator {
970 self.locator(format!("text={text}"))
972 }
973
974 pub fn get_by_label(&self, text: &str) -> Locator {
975 self.locator(format!("[aria-label=\"{}\"]", attr_escape(text)))
976 }
977
978 pub fn get_by_placeholder(&self, text: &str) -> Locator {
979 self.locator(format!("[placeholder=\"{}\"]", attr_escape(text)))
980 }
981
982 pub fn get_by_alt_text(&self, text: &str) -> Locator {
983 self.locator(format!("[alt=\"{}\"]", attr_escape(text)))
984 }
985
986 pub fn get_by_title(&self, text: &str) -> Locator {
987 self.locator(format!("[title=\"{}\"]", attr_escape(text)))
988 }
989
990 pub fn get_by_test_id(&self, text: &str) -> Locator {
991 self.locator(format!("[data-testid=\"{}\"]", attr_escape(text)))
992 }
993
994 pub fn get_by_role(&self, role: AriaRole, opts: Option<crate::options::GetByRoleOptions>) -> Locator {
995 let opts = opts.unwrap_or_default();
996 let mut sel = format!("role={}", role.as_str());
997 if let Some(name) = &opts.name {
998 sel.push_str(&format!("[name=\"{name}\"]"));
999 }
1000 if opts.exact == Some(true) {
1001 sel.push_str("[exact=\"true\"]");
1002 }
1003 Locator::new(self.clone(), sel, true, None)
1004 }
1005
1006 pub(crate) async fn resolve_strict(
1009 &self,
1010 selector: &str,
1011 forced_index: Option<usize>,
1012 ) -> Result<Option<String>> {
1013 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1014 if n == 0 {
1015 return Ok(None);
1016 }
1017 let index = forced_index.unwrap_or_else(|| {
1018 0
1021 });
1022 selectors::element_at(&self.inner.session, self.ctx().await, selector, index).await
1023 }
1024
1025 pub async fn query_selector(&self, selector: &str) -> Result<Option<ElementHandle>> {
1027 let oid = self.resolve_strict(selector, Some(0)).await?;
1028 Ok(oid.map(|oid| ElementHandle::new(self.clone(), oid)))
1029 }
1030
1031 pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementHandle>> {
1033 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1034 let mut out = Vec::with_capacity(n);
1035 for i in 0..n {
1036 if let Some(oid) =
1037 selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await?
1038 {
1039 out.push(ElementHandle::new(self.clone(), oid));
1040 }
1041 }
1042 Ok(out)
1043 }
1044
1045 pub fn keyboard(&self) -> Keyboard {
1048 Keyboard::new(self.clone())
1049 }
1050
1051 pub fn mouse(&self) -> Mouse {
1052 Mouse::with_pos(self.clone(), Arc::clone(&self.inner.mouse_pos))
1053 }
1054
1055 pub fn touchscreen(&self) -> Touchscreen {
1056 Touchscreen::with_flag(self.clone(), Arc::clone(&self.inner.touch_emulation_started))
1057 }
1058
1059 pub fn main_frame(&self) -> Frame {
1060 Frame::main(self.clone())
1061 }
1062
1063 pub async fn frames(&self) -> Result<Vec<Frame>> {
1065 self.refresh_frame_tree().await?;
1066 let ids: Vec<String> = self.inner.frames.lock().keys().cloned().collect();
1067 Ok(ids.into_iter().map(|id| Frame::new(self.clone(), id)).collect())
1068 }
1069
1070 pub(crate) async fn refresh_frame_tree(&self) -> Result<()> {
1072 let resp = self
1073 .inner
1074 .session
1075 .send("Page.getFrameTree", json!({}))
1076 .await?;
1077 if let Some(tree) = resp.get("frameTree") {
1078 let mut frames = self.inner.frames.lock();
1079 frames.clear();
1080 walk_frame_tree(tree, &mut frames);
1081 if let Some(id) = tree
1082 .get("frame")
1083 .and_then(|f| f.get("id"))
1084 .and_then(|v| v.as_str())
1085 {
1086 *self.inner.main_frame_id.lock() = Some(id.to_string());
1087 }
1088 }
1089 Ok(())
1090 }
1091
1092 pub(crate) fn main_frame_id(&self) -> Option<String> {
1093 self.inner.main_frame_id.lock().clone()
1094 }
1095
1096 pub(crate) fn frame_data(&self, frame_id: &str) -> Option<FrameData> {
1097 self.inner.frames.lock().get(frame_id).cloned()
1098 }
1099
1100 pub(crate) fn frame_ids_with_parent(&self, parent: &str) -> Vec<String> {
1101 self.inner
1102 .frames
1103 .lock()
1104 .iter()
1105 .filter(|(_, d)| d.parent_id.as_deref() == Some(parent))
1106 .map(|(k, _)| k.clone())
1107 .collect()
1108 }
1109
1110 pub fn on_console<F, Fut>(&self, handler: F)
1113 where
1114 F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
1115 Fut: std::future::Future<Output = ()> + Send + 'static,
1116 {
1117 let mut rx = self.inner.session.subscribe();
1118 tokio::spawn(async move {
1119 while let Ok(ev) = rx.recv().await {
1120 if ev.method == "Runtime.consoleAPICalled" {
1121 let msg = parse_console(&ev.params);
1122 handler(msg).await;
1123 }
1124 }
1125 });
1126 }
1127
1128 pub fn on_dialog<F, Fut>(&self, handler: F)
1129 where
1130 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1131 Fut: std::future::Future<Output = ()> + Send + 'static,
1132 {
1133 let mut rx = self.inner.session.subscribe();
1134 let session = Arc::clone(&self.inner.session);
1135 tokio::spawn(async move {
1136 while let Ok(ev) = rx.recv().await {
1137 if ev.method == "Page.javascriptDialogOpening" {
1138 let dialog = Dialog::from_event(&ev.params, &session);
1139 handler(dialog).await;
1140 }
1141 }
1142 });
1143 }
1144
1145 pub fn on_request<F, Fut>(&self, handler: F)
1147 where
1148 F: Fn(Request) -> Fut + Send + Sync + 'static,
1149 Fut: Future<Output = ()> + Send + 'static,
1150 {
1151 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1152 self.inner.on_request_handlers.lock().push(h);
1153 }
1154
1155 pub fn on_response<F, Fut>(&self, handler: F)
1157 where
1158 F: Fn(Response) -> Fut + Send + Sync + 'static,
1159 Fut: Future<Output = ()> + Send + 'static,
1160 {
1161 let h: ResponseHandler = Arc::new(move |r| Box::pin(handler(r)));
1162 self.inner.on_response_handlers.lock().push(h);
1163 }
1164
1165 pub fn on_requestfailed<F, Fut>(&self, handler: F)
1167 where
1168 F: Fn(Request) -> Fut + Send + Sync + 'static,
1169 Fut: Future<Output = ()> + Send + 'static,
1170 {
1171 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1172 self.inner.on_requestfailed_handlers.lock().push(h);
1173 }
1174
1175 pub fn on_pageerror<F, Fut>(&self, handler: F)
1177 where
1178 F: Fn(String) -> Fut + Send + Sync + 'static,
1179 Fut: Future<Output = ()> + Send + 'static,
1180 {
1181 let mut rx = self.inner.session.subscribe();
1182 tokio::spawn(async move {
1183 while let Ok(ev) = rx.recv().await {
1184 if ev.method == "Runtime.exceptionThrown" {
1185 let msg = ev
1186 .params
1187 .get("exceptionDetails")
1188 .and_then(|d| d.get("exception"))
1189 .and_then(|e| e.get("description"))
1190 .and_then(|v| v.as_str())
1191 .unwrap_or("unknown error")
1192 .to_string();
1193 handler(msg).await;
1194 }
1195 }
1196 });
1197 }
1198
1199 pub fn on_requestfinished<F, Fut>(&self, handler: F)
1201 where
1202 F: Fn(Request) -> Fut + Send + Sync + 'static,
1203 Fut: Future<Output = ()> + Send + 'static,
1204 {
1205 let mut rx = self.inner.session.subscribe();
1206 let store = Arc::clone(&self.inner.network_store);
1207 tokio::spawn(async move {
1208 while let Ok(ev) = rx.recv().await {
1209 if ev.method == "Network.loadingFinished" {
1210 if let Some(rid) = ev.params.get("requestId").and_then(|v| v.as_str()) {
1211 if let Some(req) = store.get_request(rid) {
1212 handler(req).await;
1213 }
1214 }
1215 }
1216 }
1217 });
1218 }
1219
1220 pub fn on_close<F, Fut>(&self, handler: F)
1222 where
1223 F: Fn() -> Fut + Send + Sync + 'static,
1224 Fut: Future<Output = ()> + Send + 'static,
1225 {
1226 let h: CloseHandler = Arc::new(move || Box::pin(handler()));
1227 self.inner.on_close_handlers.lock().push(h);
1228 }
1229
1230 pub fn on_download<F, Fut>(&self, handler: F)
1237 where
1238 F: Fn(Download) -> Fut + Send + Sync + 'static,
1239 Fut: Future<Output = ()> + Send + 'static,
1240 {
1241 if self
1243 .inner
1244 .download_started
1245 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1246 .is_ok()
1247 {
1248 let dir = match tempfile::TempDir::new() {
1249 Ok(d) => Arc::new(d),
1250 Err(e) => {
1251 tracing::error!("failed to create download temp dir: {e}");
1252 return;
1253 }
1254 };
1255 let dir_path = dir.path().to_path_buf();
1256 *self.inner.download_dir.lock() = Some(Arc::clone(&dir));
1257 *self.inner.download_path.lock() = Some(dir_path.clone());
1258
1259 let session_for_setup = Arc::clone(&self.inner.session);
1261 let setup_path = dir_path.clone();
1262 tokio::spawn(async move {
1263 let _ = session_for_setup
1264 .send(
1265 "Page.setDownloadBehavior",
1266 json!({ "behavior": "allow", "downloadPath": setup_path }),
1267 )
1268 .await;
1269 });
1270
1271 let mut rx = self.inner.session.subscribe();
1273 let session = Arc::clone(&self.inner.session);
1274 let states = Arc::clone(&self.inner.download_states);
1275 let download_path = dir_path.clone();
1276 let page = self.clone();
1279 let handler: Arc<dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync> =
1280 Arc::new(move |d| Box::pin(handler(d)));
1281 tokio::spawn(async move {
1282 download_listener(
1283 &mut rx,
1284 &session,
1285 &states,
1286 &download_path,
1287 page.clone(),
1288 &handler,
1289 )
1290 .await;
1291 });
1292 }
1293 }
1294
1295 pub async fn on_filechooser<F, Fut>(&self, handler: F)
1303 where
1304 F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
1305 Fut: Future<Output = ()> + Send + 'static,
1306 {
1307 if self
1309 .inner
1310 .filechooser_started
1311 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1312 .is_ok()
1313 {
1314 let mut rx = self.inner.session.subscribe();
1317 let _ = self
1320 .inner
1321 .session
1322 .send(
1323 "Page.setInterceptFileChooserDialog",
1324 json!({ "enabled": true }),
1325 )
1326 .await;
1327
1328 let page = self.clone();
1330 let handler: Arc<
1331 dyn Fn(FileChooser) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>>
1332 + Send
1333 + Sync,
1334 > = Arc::new(move |fc| Box::pin(handler(fc)));
1335 tokio::spawn(async move {
1336 while let Ok(ev) = rx.recv().await {
1337 if ev.method == "Page.fileChooserOpened" {
1338 let multiple = ev
1339 .params
1340 .get("mode")
1341 .and_then(|v| v.as_str())
1342 == Some("selectMultiple");
1343 let backend_node_id = ev
1344 .params
1345 .get("backendNodeId")
1346 .and_then(|v| v.as_i64());
1347 let fc = FileChooser::new(page.clone(), backend_node_id, multiple);
1348 let h = Arc::clone(&handler);
1349 tokio::spawn(async move {
1350 (h)(fc).await;
1351 });
1352 }
1353 }
1354 });
1355 }
1356 }
1357
1358 pub async fn on_worker<F, Fut>(&self, handler: F)
1369 where
1370 F: Fn(Worker) -> Fut + Send + Sync + 'static,
1371 Fut: Future<Output = ()> + Send + 'static,
1372 {
1373 if self
1375 .inner
1376 .worker_started
1377 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1378 .is_ok()
1379 {
1380 let mut rx = self.inner.session.subscribe();
1383 let _ = self
1384 .inner
1385 .session
1386 .send(
1387 "Target.setAutoAttach",
1388 json!({ "autoAttach": true, "waitForDebuggerOnStart": false, "flatten": true }),
1389 )
1390 .await;
1391
1392 let connection = self.inner.session.connection().clone();
1393 let handler: Arc<
1394 dyn Fn(Worker) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
1395 > = Arc::new(move |w| Box::pin(handler(w)));
1396 tokio::spawn(async move {
1397 while let Ok(ev) = rx.recv().await {
1398 if ev.method != "Target.attachedToTarget" {
1399 continue;
1400 }
1401 let session_id = ev
1403 .params
1404 .get("sessionId")
1405 .or_else(|| ev.params.get("session_id"))
1406 .and_then(|v| v.as_str());
1407 let target_info = match ev.params.get("targetInfo") {
1408 Some(t) => t,
1409 None => continue,
1410 };
1411 let target_type = target_info
1412 .get("type")
1413 .and_then(|v| v.as_str())
1414 .unwrap_or("");
1415 if !matches!(target_type, "worker" | "service_worker" | "shared_worker") {
1417 continue;
1418 }
1419 let (sid, url) = match (session_id, target_info.get("url").and_then(|v| v.as_str())) {
1420 (Some(sid), Some(url)) => (sid.to_string(), url.to_string()),
1421 _ => continue,
1422 };
1423 let worker = Worker::new(connection.clone(), sid, url);
1424 worker.enable_runtime().await;
1426 let h = Arc::clone(&handler);
1427 tokio::spawn(async move {
1428 (h)(worker).await;
1429 });
1430 }
1431 });
1432 }
1433 }
1434
1435 pub async fn close(&self) -> Result<()> {
1438 if *self.inner.closed.lock() {
1439 return Ok(());
1440 }
1441 *self.inner.closed.lock() = true;
1442 let handlers = std::mem::take(&mut *self.inner.on_close_handlers.lock());
1443 for h in handlers {
1444 tokio::spawn(async move { (h)().await; });
1445 }
1446 let _ = self
1447 .inner
1448 .browser
1449 .browser_session()
1450 .send("Target.closeTarget", json!({ "targetId": self.inner.target_id }))
1451 .await;
1452 Ok(())
1453 }
1454
1455 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1461 where
1462 F: Fn(Route) -> Fut + Send + Sync + 'static,
1463 Fut: Future<Output = ()> + Send + 'static,
1464 {
1465 let entry = RouteEntry {
1466 pattern: pattern.to_string(),
1467 handler: Arc::new(move |r| Box::pin(handler(r))),
1468 };
1469 self.inner.route_handlers.lock().push(entry);
1470 self.ensure_route_listener().await;
1471 self.refresh_fetch_patterns().await
1472 }
1473
1474 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1476 let mut handlers = self.inner.route_handlers.lock();
1477 if let Some(pos) = handlers.iter().position(|e| e.pattern == pattern) {
1478 handlers.remove(pos);
1479 }
1480 drop(handlers);
1481 self.refresh_fetch_patterns().await
1482 }
1483
1484 pub async fn unroute_all(&self) -> Result<()> {
1486 self.inner.route_handlers.lock().clear();
1487 let _ = self.inner.session.send("Fetch.disable", json!({})).await;
1488 Ok(())
1489 }
1490
1491 async fn ensure_route_listener(&self) {
1492 if self
1493 .inner
1494 .route_started
1495 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1496 .is_ok()
1497 {
1498 let rx = self.inner.session.subscribe();
1499 let session = Arc::clone(&self.inner.session);
1500 let store = Arc::clone(&self.inner.network_store);
1501 let handlers = Arc::clone(&self.inner.route_handlers);
1502 tokio::spawn(async move {
1503 route_listener(rx, session, store, handlers).await;
1504 });
1505 }
1506 }
1507
1508 async fn refresh_fetch_patterns(&self) -> Result<()> {
1509 let patterns: Vec<Value> = self
1510 .inner
1511 .route_handlers
1512 .lock()
1513 .iter()
1514 .map(|e| json!({ "urlPattern": e.pattern }))
1515 .collect();
1516 if patterns.is_empty() {
1517 let _ = self.inner.session.send("Fetch.disable", json!({})).await;
1518 } else {
1519 self.inner
1520 .session
1521 .send("Fetch.enable", json!({ "patterns": patterns }))
1522 .await?;
1523 }
1524 Ok(())
1525 }
1526
1527 pub async fn pdf(&self, opts: Option<crate::options::PdfOptions>) -> Result<Vec<u8>> {
1530 let opts = opts.unwrap_or_default();
1531 let mut params = json!({ "printBackground": opts.print_background.unwrap_or(true) });
1532 if let Some(fmt) = opts.format {
1533 let (w, h) = fmt.inches();
1534 params["paperWidth"] = json!(w);
1535 params["paperHeight"] = json!(h);
1536 }
1537 if let Some(l) = opts.landscape {
1538 params["landscape"] = json!(l);
1539 }
1540 if let Some(s) = opts.scale {
1541 params["scale"] = json!(s);
1542 }
1543 if let Some(p) = opts.prefer_css_page_size {
1544 params["preferCSSPageSize"] = json!(p);
1545 }
1546 if let Some(m) = opts.margin {
1547 if let Some(v) = m.top {
1548 params["marginTop"] = json!(v);
1549 }
1550 if let Some(v) = m.bottom {
1551 params["marginBottom"] = json!(v);
1552 }
1553 if let Some(v) = m.left {
1554 params["marginLeft"] = json!(v);
1555 }
1556 if let Some(v) = m.right {
1557 params["marginRight"] = json!(v);
1558 }
1559 }
1560 let resp = self.inner.session.send("Page.printToPDF", params).await?;
1561 let data = resp
1562 .get("data")
1563 .and_then(|v| v.as_str())
1564 .ok_or_else(|| Error::ProtocolError("pdf missing data".into()))?;
1565 Ok(base64::engine::general_purpose::STANDARD
1566 .decode(data)
1567 .map_err(|e| Error::ProtocolError(format!("pdf base64 decode: {e}")))?)
1568 }
1569
1570 pub async fn aria_snapshot(&self) -> Result<String> {
1577 let _ = self
1578 .inner
1579 .session
1580 .send("Accessibility.enable", json!({}))
1581 .await;
1582 let resp = self
1583 .inner
1584 .session
1585 .send("Accessibility.getFullAXTree", json!({}))
1586 .await?;
1587 let nodes = resp
1588 .get("nodes")
1589 .and_then(|v| v.as_array())
1590 .cloned()
1591 .unwrap_or_default();
1592 Ok(crate::aria_snapshot::serialize(&nodes, None))
1593 }
1594}
1595
1596fn attr_escape(s: &str) -> String {
1597 s.replace('\\', "\\\\").replace('"', "\\\"")
1598}
1599
1600fn is_truthy(v: &Value) -> bool {
1604 match v {
1605 Value::Null => false,
1606 Value::Bool(b) => *b,
1607 Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
1608 Value::String(s) => !s.is_empty(),
1609 _ => true,
1610 }
1611}
1612
1613fn glob_matches(pattern: &str, value: &str) -> bool {
1616 if pattern == value {
1617 return true;
1618 }
1619 if pattern.len() >= 2 && pattern.starts_with('*') && pattern.ends_with('*') {
1621 return value.contains(&pattern[1..pattern.len() - 1]);
1622 }
1623 if let Some(rest) = pattern.strip_prefix('*') {
1624 if value.ends_with(rest) {
1625 return true;
1626 }
1627 }
1628 if let Some(rest) = pattern.strip_suffix('*') {
1629 if value.starts_with(rest) {
1630 return true;
1631 }
1632 }
1633 false
1634}
1635
1636fn walk_frame_tree(node: &Value, frames: &mut HashMap<String, FrameData>) {
1638 let frame = match node.get("frame") {
1639 Some(f) => f,
1640 None => return,
1641 };
1642 let id = match frame.get("id").and_then(|v| v.as_str()) {
1643 Some(s) => s.to_string(),
1644 None => return,
1645 };
1646 let data = FrameData {
1647 url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
1648 name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
1649 parent_id: frame
1650 .get("parentId")
1651 .and_then(|v| v.as_str())
1652 .map(String::from),
1653 detached: false,
1654 };
1655 frames.insert(id, data);
1656 if let Some(children) = node.get("childFrames").and_then(|v| v.as_array()) {
1657 for child in children {
1658 walk_frame_tree(child, frames);
1659 }
1660 }
1661}
1662
1663async fn frame_tracker(
1665 mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
1666 frames: Arc<Mutex<HashMap<String, FrameData>>>,
1667 main_id: Arc<Mutex<Option<String>>>,
1668) {
1669 loop {
1670 match rx.recv().await {
1671 Ok(ev) => match ev.method.as_str() {
1672 "Page.frameNavigated" => {
1673 if let Some(frame) = ev.params.get("frame") {
1674 let id = match frame.get("id").and_then(|v| v.as_str()) {
1675 Some(s) => s.to_string(),
1676 None => continue,
1677 };
1678 let parent = frame
1679 .get("parentId")
1680 .and_then(|v| v.as_str())
1681 .map(String::from);
1682 let data = FrameData {
1683 url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
1684 name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
1685 parent_id: parent.clone(),
1686 detached: false,
1687 };
1688 if parent.is_none() {
1689 *main_id.lock() = Some(id.clone());
1690 }
1691 frames.lock().insert(id, data);
1692 }
1693 }
1694 "Page.frameDetached" => {
1695 if let Some(id) = ev.params.get("frameId").and_then(|v| v.as_str()) {
1696 if let Some(d) = frames.lock().get_mut(id) {
1697 d.detached = true;
1698 }
1699 }
1700 }
1701 _ => {}
1702 },
1703 Err(broadcast::error::RecvError::Closed) => break,
1704 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1705 }
1706 }
1707}
1708
1709async fn download_listener(
1711 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
1712 _session: &Arc<CdpSession>,
1713 states: &Arc<Mutex<HashMap<String, DownloadStateCell>>>,
1714 download_path: &std::path::Path,
1715 page: Page,
1716 handler: &Arc<
1717 dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
1718 >,
1719) {
1720 while let Ok(ev) = rx.recv().await {
1721 match ev.method.as_str() {
1722 "Page.downloadWillBegin" => {
1723 let guid = ev
1724 .params
1725 .get("guid")
1726 .and_then(|v| v.as_str())
1727 .unwrap_or("")
1728 .to_string();
1729 let url = ev
1730 .params
1731 .get("url")
1732 .and_then(|v| v.as_str())
1733 .unwrap_or("")
1734 .to_string();
1735 let suggested = ev
1736 .params
1737 .get("suggestedFilename")
1738 .and_then(|v| v.as_str())
1739 .unwrap_or("download")
1740 .to_string();
1741 let cell = DownloadStateCell::new();
1742 states.lock().insert(guid.clone(), cell.clone());
1743 let dl = Download::new(
1744 url,
1745 suggested,
1746 guid,
1747 cell,
1748 download_path.to_path_buf(),
1749 page.clone(),
1750 );
1751 let h = Arc::clone(handler);
1752 tokio::spawn(async move {
1753 (h)(dl).await;
1754 });
1755 }
1756 "Page.downloadProgress" => {
1757 let guid = match ev.params.get("guid").and_then(|v| v.as_str()) {
1758 Some(g) => g.to_string(),
1759 None => continue,
1760 };
1761 let state = ev
1762 .params
1763 .get("state")
1764 .and_then(|v| v.as_str())
1765 .unwrap_or("InProgress");
1766 let mapped = match state {
1767 "Completed" => DownloadState::Completed,
1768 "Canceled" => DownloadState::Canceled,
1769 _ => DownloadState::InProgress,
1770 };
1771 if let Some(cell) = states.lock().get(&guid) {
1772 *cell.state.lock() = mapped;
1773 }
1774 }
1775 _ => {}
1776 }
1777 }
1778}
1779
1780fn internal_binding_name(name: &str) -> String {
1784 format!("__pwcdpInvoke_{name}")
1785}
1786
1787fn binding_wrapper_source(name: &str) -> String {
1792 let name_json = serde_json::to_string(name).unwrap_or_else(|_| "\"\"".to_string());
1795 format!(
1796 r#"(function(){{
1797 var publicName = {name_json};
1798 var invokeName = "__pwcdpInvoke_" + publicName;
1799 var pending = (self.__pwcdpBindings = self.__pwcdpBindings || {{}});
1800 var map = pending[publicName] = pending[publicName] || {{ id: 0, cbs: {{}} }};
1801 self[publicName] = function(){{
1802 var args = Array.prototype.slice.call(arguments);
1803 return new Promise(function(resolve){{
1804 var id = ++map.id;
1805 map.cbs[id] = resolve;
1806 // The CDP binding (self[invokeName]) is installed by Runtime.addBinding;
1807 // if it is not present yet, surface an error so callers see a clear cause.
1808 if (typeof self[invokeName] !== "function") {{
1809 delete map.cbs[id];
1810 resolve({{ "__error": "binding not installed: " + invokeName }});
1811 return;
1812 }}
1813 try {{
1814 self[invokeName](JSON.stringify({{ id: id, args: args }}));
1815 }} catch (e) {{
1816 delete map.cbs[id];
1817 resolve({{ "__error": String((e && e.message) || e) }});
1818 }}
1819 }});
1820 }};
1821}})();
1822"#
1823 )
1824}
1825
1826async fn binding_listener(
1830 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
1831 session: &Arc<CdpSession>,
1832 name: &str,
1833 handler: &ExposedBindingHandler,
1834) {
1835 let binding_name = internal_binding_name(name);
1836 loop {
1837 match rx.recv().await {
1838 Ok(ev) if ev.method == "Runtime.bindingCalled" => {
1839 let fired_name = ev
1840 .params
1841 .get("name")
1842 .and_then(|v| v.as_str())
1843 .unwrap_or("");
1844 if fired_name != binding_name {
1845 continue;
1846 }
1847 let payload_str = match ev.params.get("payload").and_then(|v| v.as_str()) {
1848 Some(s) => s,
1849 None => continue,
1850 };
1851 let parsed: Value = match serde_json::from_str(payload_str) {
1852 Ok(v) => v,
1853 Err(_) => continue,
1854 };
1855 let id = match parsed.get("id").and_then(|v| v.as_i64()) {
1856 Some(i) => i,
1857 None => continue,
1858 };
1859 let args = parsed
1860 .get("args")
1861 .and_then(|v| v.as_array())
1862 .cloned()
1863 .unwrap_or_default();
1864
1865 let h = Arc::clone(handler);
1866 let session = Arc::clone(session);
1867 let name_owned = name.to_string();
1868 tokio::spawn(async move {
1871 let result = (h)(args).await;
1872 let result_json = serde_json::to_string(&result)
1873 .unwrap_or_else(|_| r#"{"__error":"serialize failed"}"#.to_string());
1874 let result_str_literal = serde_json::to_string(&result_json)
1877 .unwrap_or_else(|_| r#""{\"__error\":\"serialize failed\"}""#.to_string());
1878 let name_j = serde_json::to_string(&name_owned).unwrap_or_else(|_| "\"\"".into());
1881 let expr = format!(
1882 "(function(){{
1883 var m = (self.__pwcdpBindings && self.__pwcdpBindings[{name_j}]) || null;
1884 if (!m || !m.cbs || !m.cbs[{id}]) return;
1885 var fn_ = m.cbs[{id}]; delete m.cbs[{id}];
1886 fn_(JSON.parse({result_str_literal}));
1887}})();"
1888 );
1889 let _ = session
1890 .send(
1891 "Runtime.evaluate",
1892 json!({ "expression": expr, "awaitPromise": false }),
1893 )
1894 .await;
1895 });
1896 }
1897 Ok(_) => {}
1898 Err(broadcast::error::RecvError::Closed) => break,
1899 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1900 }
1901 }
1902}
1903
1904fn parse_console(params: &Value) -> ConsoleMessage { let text = params
1905 .get("args")
1906 .and_then(|a| a.as_array())
1907 .map(|args| {
1908 args.iter()
1909 .filter_map(|a| {
1910 a.get("value")
1911 .and_then(|v| v.as_str())
1912 .or_else(|| a.get("description").and_then(|v| v.as_str()))
1913 .map(String::from)
1914 })
1915 .collect::<Vec<_>>()
1916 .join(" ")
1917 })
1918 .unwrap_or_default();
1919 let kind = params
1920 .get("type")
1921 .and_then(|v| v.as_str())
1922 .unwrap_or("log")
1923 .to_string();
1924 ConsoleMessage { text, r#type: kind }
1925}
1926
1927async fn context_tracker(
1930 mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
1931 session: Arc<CdpSession>,
1932 ctx_cell: Arc<Mutex<Option<i64>>>,
1933) {
1934 loop {
1935 match rx.recv().await {
1936 Ok(ev) => match ev.method.as_str() {
1937 "Runtime.executionContextCreated" => {
1938 let is_default = ev
1939 .params
1940 .get("context")
1941 .and_then(|c| c.get("auxData"))
1942 .and_then(|a| a.get("type"))
1943 .and_then(|t| t.as_str())
1944 == Some("default");
1945 if is_default {
1946 let id = ev
1947 .params
1948 .get("context")
1949 .and_then(|c| c.get("id"))
1950 .and_then(|i| i.as_i64());
1951 if let Some(id) = id {
1952 *ctx_cell.lock() = Some(id);
1953 let s = Arc::clone(&session);
1955 tokio::spawn(async move {
1956 let _ = s
1957 .send(
1958 "Runtime.evaluate",
1959 json!({
1960 "expression": selectors::INJECTED_SCRIPT,
1961 "contextId": id,
1962 }),
1963 )
1964 .await;
1965 });
1966 }
1967 }
1968 }
1969 "Runtime.executionContextsCleared" => {
1970 *ctx_cell.lock() = None;
1971 }
1972 "Runtime.executionContextDestroyed" => {
1973 let id = ev
1974 .params
1975 .get("executionContextId")
1976 .and_then(|v| v.as_i64());
1977 let mut cell = ctx_cell.lock();
1978 if id.is_some() && *cell == id {
1979 *cell = None;
1980 }
1981 }
1982 _ => {}
1983 },
1984 Err(broadcast::error::RecvError::Closed) => break,
1985 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1986 }
1987 }
1988}
1989
1990pub struct Dialog {
1992 message: String,
1993 kind: String,
1994 session: Arc<CdpSession>,
1995}
1996
1997impl Dialog {
1998 pub(crate) fn from_event(params: &Value, session: &Arc<CdpSession>) -> Self {
1999 Self {
2000 message: params
2001 .get("message")
2002 .and_then(|v| v.as_str())
2003 .unwrap_or("")
2004 .to_string(),
2005 kind: params
2006 .get("type")
2007 .and_then(|v| v.as_str())
2008 .unwrap_or("alert")
2009 .to_string(),
2010 session: Arc::clone(session),
2011 }
2012 }
2013
2014 pub fn message(&self) -> &str {
2015 &self.message
2016 }
2017
2018 pub fn kind(&self) -> &str {
2019 &self.kind
2020 }
2021
2022 pub async fn accept(&self, prompt_text: Option<&str>) -> Result<()> {
2023 let mut p = json!({ "accept": true });
2024 if let Some(t) = prompt_text {
2025 p["promptText"] = json!(t);
2026 }
2027 self.session
2028 .send("Page.handleJavaScriptDialog", p)
2029 .await
2030 .map(|_| ())
2031 }
2032
2033 pub async fn dismiss(&self) -> Result<()> {
2034 self.session
2035 .send("Page.handleJavaScriptDialog", json!({ "accept": false }))
2036 .await
2037 .map(|_| ())
2038 }
2039}