1use crate::accessibility::Accessibility;
5use crate::browser::Browser;
6use crate::api_request::APIRequestContext;
7use crate::cdp::session::CdpSession;
8use crate::clock::Clock;
9use crate::coverage::Coverage;
10use crate::js_handle::JSHandle;
11use crate::video::Video;
12use crate::web_socket::{WebSocket, WebSocketRegistry};
13use crate::web_storage::WebStorage;
14use crate::download::{Download, DownloadState, DownloadStateCell};
15use crate::element_handle::ElementHandle;
16use crate::error::{Error, Result};
17use crate::file_chooser::FileChooser;
18use crate::frame::Frame;
19use crate::frame_locator::FrameLocator;
20use crate::keyboard::Keyboard;
21use crate::locator::Locator;
22use crate::mouse::Mouse;
23use crate::network::{network_tracker, NetworkStore, RequestHandler, ResponseHandler};
24use crate::options::{
25 DragToOptions, EmulateMediaOptions, GotoOptions, ScreenshotOptions, WaitForFunctionOptions, WaitUntil,
26};
27use crate::request::Request;
28use crate::response::Response;
29use crate::route::{route_listener, Route, RouteEntry};
30use crate::selectors;
31use crate::touchscreen::Touchscreen;
32use crate::types::{AriaRole, ConsoleMessage, Headers, Viewport};
33use crate::worker::Worker;
34use base64::Engine;
35use parking_lot::Mutex;
36use serde::de::DeserializeOwned;
37use serde_json::{json, Value};
38use std::collections::HashMap;
39use std::future::Future;
40use std::path::PathBuf;
41use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
42use std::sync::Arc;
43use std::time::Duration;
44use tokio::sync::broadcast;
45use tokio::sync::oneshot;
46
47#[derive(Clone)]
49pub struct Page {
50 inner: Arc<PageInner>,
51}
52
53type CloseHandler =
54 Arc<dyn Fn() -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
55
56type PopupHandler =
58 Arc<dyn Fn(Page) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
59
60type ExposedBindingHandler =
62 Arc<dyn Fn(Vec<Value>) -> std::pin::Pin<Box<dyn Future<Output = Value> + Send>> + Send + Sync>;
63
64struct PageInner {
65 browser: Browser,
66 session: Arc<CdpSession>,
67 target_id: String,
68 main_world_ctx: Arc<Mutex<Option<i64>>>,
69 frame_contexts: Arc<Mutex<HashMap<String, i64>>>,
74 default_timeout_ms: AtomicU64,
75 default_navigation_timeout_ms: AtomicU64,
76 viewport: Mutex<Option<Viewport>>,
77 mouse_pos: Arc<Mutex<(f64, f64)>>,
78 network_store: Arc<NetworkStore>,
79 on_request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
80 on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
81 on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
82 frames: Arc<Mutex<HashMap<String, FrameData>>>,
83 main_frame_id: Arc<Mutex<Option<String>>>,
84 route_handlers: Arc<Mutex<Vec<RouteEntry>>>,
85 route_started: AtomicBool,
86 on_close_handlers: Mutex<Vec<CloseHandler>>,
87 on_popup_handlers: Arc<Mutex<Vec<PopupHandler>>>,
90 popup_started: AtomicBool,
91 closed: Mutex<bool>,
92 download_dir: Mutex<Option<Arc<tempfile::TempDir>>>,
95 download_path: Mutex<Option<PathBuf>>,
96 download_states: Arc<Mutex<HashMap<String, DownloadStateCell>>>,
97 download_started: AtomicBool,
98 filechooser_started: AtomicBool,
100 worker_started: AtomicBool,
102 touch_emulation_started: Arc<AtomicBool>,
105 web_socket_registry: Arc<WebSocketRegistry>,
109 video_path: Mutex<Option<PathBuf>>,
112}
113
114#[derive(Clone)]
116pub(crate) struct FrameData {
117 pub url: String,
118 pub name: String,
119 pub parent_id: Option<String>,
120 pub detached: bool,
121}
122
123impl Page {
124 #[allow(clippy::too_many_arguments)]
127 pub(crate) async fn attach(
128 browser: Browser,
129 session_id: String,
130 target_id: String,
131 init_scripts: &[String],
132 default_timeout_ms: u64,
133 extra_headers: Option<&Headers>,
134 user_agent: Option<&str>,
135 viewport: Option<Viewport>,
136 ) -> Result<Page> {
137 let session = Arc::new(CdpSession::target(
138 browser.connection().clone(),
139 session_id,
140 ));
141 session.set_default_timeout_ms(default_timeout_ms);
142
143 let main_world_ctx: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
144 let frame_contexts: Arc<Mutex<HashMap<String, i64>>> =
145 Arc::new(Mutex::new(HashMap::new()));
146 let frames_store: Arc<Mutex<HashMap<String, FrameData>>> =
150 Arc::new(Mutex::new(HashMap::new()));
151 let main_frame_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
152 let ctx_rx = session.subscribe();
153 {
156 let session_for_task = Arc::clone(&session);
157 let ctx_cell = Arc::clone(&main_world_ctx);
158 let frame_ctx_cell = Arc::clone(&frame_contexts);
159 let main_id_cell = Arc::clone(&main_frame_id);
160 tokio::spawn(async move {
161 context_tracker(
162 ctx_rx,
163 session_for_task,
164 ctx_cell,
165 frame_ctx_cell,
166 main_id_cell,
167 )
168 .await;
169 });
170 }
171
172 let network_store = NetworkStore::new();
173 let on_request_handlers: Arc<Mutex<Vec<RequestHandler>>> = Arc::new(Mutex::new(Vec::new()));
175 let on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>> =
176 Arc::new(Mutex::new(Vec::new()));
177 let on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>> =
178 Arc::new(Mutex::new(Vec::new()));
179
180 {
182 let net_rx = session.subscribe();
183 let session_for_task = Arc::clone(&session);
184 let store_for_task = Arc::clone(&network_store);
185 let on_request = Arc::clone(&on_request_handlers);
186 let on_response = Arc::clone(&on_response_handlers);
187 let on_failed = Arc::clone(&on_requestfailed_handlers);
188 tokio::spawn(async move {
189 network_tracker(
190 net_rx,
191 session_for_task,
192 store_for_task,
193 on_request,
194 on_response,
195 on_failed,
196 )
197 .await;
198 });
199 }
200
201 let page = Page {
202 inner: Arc::new(PageInner {
203 browser,
204 session: Arc::clone(&session),
205 target_id,
206 main_world_ctx: Arc::clone(&main_world_ctx),
207 frame_contexts: Arc::clone(&frame_contexts),
208 default_timeout_ms: AtomicU64::new(default_timeout_ms),
209 default_navigation_timeout_ms: AtomicU64::new(default_timeout_ms),
210 viewport: Mutex::new(viewport),
211 mouse_pos: Arc::new(Mutex::new((0.0, 0.0))),
212 network_store,
213 on_request_handlers,
214 on_response_handlers,
215 on_requestfailed_handlers,
216 frames: Arc::clone(&frames_store),
217 main_frame_id: Arc::clone(&main_frame_id),
218 route_handlers: Arc::new(Mutex::new(Vec::new())),
219 route_started: AtomicBool::new(false),
220 on_close_handlers: Mutex::new(Vec::new()),
221 on_popup_handlers: Arc::new(Mutex::new(Vec::new())),
222 popup_started: AtomicBool::new(false),
223 closed: Mutex::new(false),
224 download_dir: Mutex::new(None),
225 download_path: Mutex::new(None),
226 download_states: Arc::new(Mutex::new(HashMap::new())),
227 download_started: AtomicBool::new(false),
228 filechooser_started: AtomicBool::new(false),
229 worker_started: AtomicBool::new(false),
230 touch_emulation_started: Arc::new(AtomicBool::new(false)),
231 web_socket_registry: Arc::new(WebSocketRegistry::new()),
232 video_path: Mutex::new(None),
233 }),
234 };
235
236 page.inner.web_socket_registry.attach(&page.inner.session);
239
240 {
242 let rx = session.subscribe();
243 let frames_store = Arc::clone(&page.inner.frames);
244 let main_id = Arc::clone(&page.inner.main_frame_id);
245 tokio::spawn(async move {
246 frame_tracker(rx, frames_store, main_id).await;
247 });
248 }
249 let _ = page.refresh_frame_tree().await;
251
252 let _ = session.send("Page.enable", json!({})).await;
255 let _ = session.send("Runtime.enable", json!({})).await;
256 let _ = session.send("Network.enable", json!({})).await;
257 let _ = session.send("Log.enable", json!({})).await;
258
259 let mut source = String::from(selectors::INJECTED_SCRIPT);
261 for s in init_scripts {
262 source.push_str("\n");
263 source.push_str(s);
264 }
265 let _ = session
266 .send(
267 "Page.addScriptToEvaluateOnNewDocument",
268 json!({ "source": source }),
269 )
270 .await;
271
272 page.ensure_engine_in_current_context().await;
274
275 if let Some(headers) = extra_headers {
276 let _ = page.set_extra_http_headers(headers.clone()).await;
277 }
278 if let Some(ua) = user_agent {
279 let _ = session
280 .send("Emulation.setUserAgentOverride", json!({ "userAgent": ua }))
281 .await;
282 }
283 let vp = page.inner.viewport.lock().as_ref().copied();
288 if let Some(vp) = vp {
289 let _ = page.set_viewport_size(vp).await;
290 }
291
292 Ok(page)
293 }
294
295 pub(crate) fn session(&self) -> &CdpSession {
298 &self.inner.session
299 }
300
301 pub(crate) fn session_arc(&self) -> Arc<CdpSession> {
304 Arc::clone(&self.inner.session)
305 }
306
307 pub(crate) fn context_id(&self) -> Option<i64> {
308 self.inner.main_world_ctx.lock().as_ref().copied()
309 }
310
311 pub(crate) async fn ctx(&self) -> Option<i64> {
323 None
324 }
325
326 pub(crate) fn context_for_frame(&self, frame_id: &str) -> Option<i64> {
333 if self
335 .inner
336 .main_frame_id
337 .lock()
338 .as_deref()
339 .map(|m| m == frame_id)
340 .unwrap_or(false)
341 {
342 return None;
343 }
344 self.inner
345 .frame_contexts
346 .lock()
347 .get(frame_id)
348 .copied()
349 .or(None)
351 }
352
353 pub(crate) async fn ctx_for_frame(&self, frame_id: &str) -> Option<i64> {
358 if self
360 .inner
361 .main_frame_id
362 .lock()
363 .as_deref()
364 .map(|m| m == frame_id)
365 .unwrap_or(false)
366 {
367 return None;
368 }
369 let deadline = tokio::time::Instant::now() + Duration::from_millis(2000);
370 loop {
371 if let Some(id) = self.inner.frame_contexts.lock().get(frame_id).copied() {
372 return Some(id);
373 }
374 if tokio::time::Instant::now() >= deadline {
375 return None;
378 }
379 tokio::time::sleep(Duration::from_millis(20)).await;
380 }
381 }
382
383 pub fn browser(&self) -> Browser {
384 self.inner.browser.clone()
385 }
386
387 pub fn target_id(&self) -> &str {
388 &self.inner.target_id
389 }
390
391 pub fn is_closed(&self) -> bool {
392 *self.inner.closed.lock()
393 }
394
395 pub fn set_default_timeout(&self, ms: u64) {
400 self.inner.default_timeout_ms.store(ms, Ordering::Relaxed);
401 self.inner.session.set_default_timeout_ms(ms);
402 }
403
404 pub(crate) fn default_timeout(&self) -> Duration {
405 Duration::from_millis(self.inner.default_timeout_ms.load(Ordering::Relaxed))
406 }
407
408 pub(crate) fn default_navigation_timeout(&self) -> Duration {
409 Duration::from_millis(self.inner.default_navigation_timeout_ms.load(Ordering::Relaxed))
410 }
411
412 async fn ensure_engine_in_current_context(&self) {
413 let _ = selectors::eval_context(
415 &self.inner.session,
416 self.ctx().await,
417 "(arg) => { void arg; }",
418 Value::Null,
419 )
420 .await;
421 }
422
423 pub async fn goto(
427 &self,
428 url: &str,
429 opts: Option<GotoOptions>,
430 ) -> Result<Option<Response>> {
431 let opts = opts.unwrap_or_default();
432 let wait = opts.wait_until_or_default();
433
434 let mut rx = self.inner.session.subscribe();
436 let mut params = json!({ "url": url });
437 if let Some(referer) = &opts.referer {
438 params["referer"] = json!(referer);
439 }
440 let nav = self.inner.session.send("Page.navigate", params).await?;
441 if let Some(err) = nav.get("errorText").and_then(|v| v.as_str()) {
442 return Err(Error::ProtocolError(format!("navigation to {url} failed: {err}")));
443 }
444 let loader_id = nav.get("loaderId").and_then(|v| v.as_str()).map(String::from);
445
446 let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
447 self.wait_lifecycle(&mut rx, loader_id.as_deref(), wait, timeout, url)
448 .await?;
449
450 let response = match &loader_id {
452 Some(lid) => self.wait_for_nav_response(lid).await,
453 None => None,
454 };
455 Ok(response)
456 }
457
458 async fn wait_for_nav_response(&self, loader_id: &str) -> Option<Response> {
460 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
461 loop {
462 if let Some(r) = self.inner.network_store.response_for_loader(loader_id) {
463 return Some(r);
464 }
465 if tokio::time::Instant::now() >= deadline {
466 return None;
467 }
468 tokio::time::sleep(Duration::from_millis(50)).await;
469 }
470 }
471
472 async fn wait_network_idle(&self, deadline: tokio::time::Instant) {
474 let window = Duration::from_millis(500);
475 loop {
476 if self.inner.network_store.inflight() == 0 {
477 let settled_at = tokio::time::Instant::now();
478 loop {
479 if self.inner.network_store.inflight() != 0 {
480 break; }
482 if tokio::time::Instant::now().duration_since(settled_at) >= window {
483 return; }
485 if tokio::time::Instant::now() >= deadline {
486 return; }
488 tokio::time::sleep(Duration::from_millis(50)).await;
489 }
490 }
491 if tokio::time::Instant::now() >= deadline {
492 return;
493 }
494 tokio::time::sleep(Duration::from_millis(50)).await;
495 }
496 }
497
498 async fn wait_lifecycle(
499 &self,
500 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
501 loader_id: Option<&str>,
502 wait: WaitUntil,
503 timeout: Duration,
504 url: &str,
505 ) -> Result<()> {
506 if matches!(wait, WaitUntil::Commit) {
507 return Ok(()); }
509 let target_name = match wait {
510 WaitUntil::Load => "load",
511 WaitUntil::DomContentLoaded => "DOMContentLoaded",
512 WaitUntil::NetworkIdle => "load", WaitUntil::Commit => "load",
514 };
515
516 let deadline = tokio::time::Instant::now() + timeout;
517 loop {
518 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
519 match tokio::time::timeout(remaining, rx.recv()).await {
520 Err(_) => {
521 return Err(Error::NavigationTimeout {
522 url: url.to_string(),
523 duration_ms: timeout.as_millis() as u64,
524 })
525 }
526 Ok(Err(broadcast::error::RecvError::Closed)) => {
527 return Err(Error::ChannelClosed);
528 }
529 Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
530 Ok(Ok(ev)) => {
531 let matched = match ev.method.as_str() {
536 "Page.lifecycleEvent" => {
537 let same_loader = loader_id
538 .map(|l| {
539 ev.params.get("loaderId").and_then(|v| v.as_str()) == Some(l)
540 })
541 .unwrap_or(true);
542 let name = ev.params.get("name").and_then(|v| v.as_str()).unwrap_or("");
543 same_loader && name == target_name
544 }
545 "Page.loadEventFired" => target_name == "load",
546 "Page.domContentEventFired" => target_name == "DOMContentLoaded",
547 _ => false,
548 };
549 if matched {
550 if matches!(wait, WaitUntil::NetworkIdle) {
551 self.wait_network_idle(deadline).await;
554 }
555 return Ok(());
556 }
557 }
558 }
559 }
560 }
561
562 pub async fn reload(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
564 let opts = opts.unwrap_or_default();
565 let wait = opts.wait_until_or_default();
566 let mut rx = self.inner.session.subscribe();
567 let _ = self.inner.session.send("Page.reload", json!({})).await?;
568 let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
569 self.wait_lifecycle(&mut rx, None, wait, timeout, "reload").await?;
570 Ok(None)
571 }
572
573 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
575 let state = state.unwrap_or_default();
576 let mut rx = self.inner.session.subscribe();
577 self.wait_lifecycle(&mut rx, None, state, self.default_timeout(), "wait_for_load_state")
578 .await
579 }
580
581 pub async fn go_back(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
583 self.history_nav("history.back()", opts).await
584 }
585
586 pub async fn go_forward(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
588 self.history_nav("history.forward()", opts).await
589 }
590
591 async fn history_nav(
592 &self,
593 expr: &str,
594 opts: Option<GotoOptions>,
595 ) -> Result<Option<Response>> {
596 let opts = opts.unwrap_or_default();
597 let navigated: bool = self.evaluate(expr).await.unwrap_or(false);
599 if navigated {
600 let wait = opts.wait_until_or_default();
601 let mut rx = self.inner.session.subscribe();
602 let timeout = opts
603 .timeout
604 .unwrap_or_else(|| self.default_navigation_timeout());
605 let _ = self
606 .wait_lifecycle(&mut rx, None, wait, timeout, "history navigation")
607 .await;
608 }
609 Ok(None)
610 }
611
612 pub async fn wait_for_url(&self, url: &str, opts: Option<GotoOptions>) -> Result<()> {
615 let opts = opts.unwrap_or_default();
616 let timeout = opts
617 .timeout
618 .unwrap_or_else(|| self.default_navigation_timeout());
619 let deadline = tokio::time::Instant::now() + timeout;
620 loop {
621 let cur = self.url().await.unwrap_or_default();
622 if glob_matches(url, &cur) {
623 break;
624 }
625 if tokio::time::Instant::now() >= deadline {
626 return Err(Error::Timeout(format!(
627 "wait_for_url '{url}' timed out after {}ms (last: '{cur}')",
628 timeout.as_millis()
629 )));
630 }
631 tokio::time::sleep(Duration::from_millis(100)).await;
632 }
633 if let Some(state) = opts.wait_until {
634 self.wait_for_load_state(Some(state)).await?;
635 }
636 Ok(())
637 }
638
639 pub fn set_default_navigation_timeout(&self, ms: u64) {
641 self.inner
642 .default_navigation_timeout_ms
643 .store(ms, Ordering::Relaxed);
644 }
645
646 pub async fn emulate_media(&self, opts: Option<EmulateMediaOptions>) -> Result<()> {
648 use crate::options::{ColorScheme, Media, ReducedMotion};
649 let opts = opts.unwrap_or_default();
650 let mut params = json!({});
651 if let Some(m) = opts.media {
652 params["media"] = json!(match m {
653 Media::Screen => "screen",
654 Media::Print => "print",
655 });
656 }
657 let mut features = Vec::new();
658 if let Some(cs) = opts.color_scheme {
659 features.push(json!({
660 "name": "prefers-color-scheme",
661 "value": match cs {
662 ColorScheme::Light => "light",
663 ColorScheme::Dark => "dark",
664 ColorScheme::NoPreference => "no-preference",
665 }
666 }));
667 }
668 if let Some(rm) = opts.reduced_motion {
669 features.push(json!({
670 "name": "prefers-reduced-motion",
671 "value": match rm {
672 ReducedMotion::Reduce => "reduce",
673 ReducedMotion::NoPreference => "no-preference",
674 }
675 }));
676 }
677 if !features.is_empty() {
678 params["features"] = json!(features);
679 }
680 self.inner
681 .session
682 .send("Emulation.setEmulatedMedia", params)
683 .await
684 .map(|_: Value| ())
685 }
686
687 pub async fn set_offline(&self, offline: bool) -> Result<()> {
689 let params = if offline {
690 json!({ "offline": true, "latency": 0, "downloadThroughput": 0, "uploadThroughput": 0 })
691 } else {
692 json!({ "offline": false, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1 })
693 };
694 self.inner
695 .session
696 .send("Network.emulateNetworkConditions", params)
697 .await
698 .map(|_: Value| ())
699 }
700
701 pub async fn drag_and_drop(
703 &self,
704 source: &str,
705 target: &str,
706 options: Option<DragToOptions>,
707 ) -> Result<()> {
708 let src = self.locator(source);
709 let tgt = self.locator(target);
710 src.drag_to(&tgt, options).await
711 }
712
713 pub async fn url(&self) -> Result<String> {
716 self.evaluate::<String>("location.href").await
717 }
718
719 pub async fn title(&self) -> Result<String> {
720 self.evaluate::<String>("document.title").await
721 }
722
723 pub async fn content(&self) -> Result<String> {
724 self.evaluate::<String>("document.documentElement.outerHTML").await
725 }
726
727 pub async fn set_content(&self, html: &str) -> Result<()> {
728 let _ = selectors::eval_context(
729 &self.inner.session,
730 self.ctx().await,
731 "(html) => { document.open(); document.write(html); document.close(); }",
732 json!(html),
733 )
734 .await?;
735 Ok(())
736 }
737
738 pub async fn evaluate<R: DeserializeOwned>(&self, expression: &str) -> Result<R> {
742 let function = format!("(arg) => {{ return ({expression}); }}");
743 let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, Value::Null)
744 .await?;
745 serde_json::from_value::<R>(v).map_err(Error::from)
746 }
747
748 pub async fn evaluate_handle(&self, expression: &str) -> Result<JSHandle> {
758 let function = format!("(arg) => {{ return ({expression}); }}");
759 let object_id = selectors::eval_context_handle(
760 &self.inner.session,
761 self.ctx().await,
762 &function,
763 Value::Null,
764 )
765 .await?
766 .ok_or_else(|| {
767 Error::ProtocolError(
768 "evaluate_handle returned no remote object (null/undefined result)".into(),
769 )
770 })?;
771 Ok(JSHandle::with_context(
772 Arc::clone(&self.inner.session),
773 object_id,
774 self.context_id(),
775 ))
776 }
777
778 pub async fn evaluate_with_arg<R: DeserializeOwned, T: serde::Serialize>(
780 &self,
781 expression: &str,
782 arg: &T,
783 ) -> Result<R> {
784 let function = format!("(arg) => {{ return ({expression}); }}");
785 let arg_val = serde_json::to_value(arg)?;
786 let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val)
787 .await?;
788 serde_json::from_value::<R>(v).map_err(Error::from)
789 }
790
791 pub async fn wait_for_function<R: DeserializeOwned>(
799 &self,
800 expression: &str,
801 arg: Option<Value>,
802 options: Option<WaitForFunctionOptions>,
803 ) -> Result<R> {
804 let opts = options.unwrap_or_default();
805 let timeout = Duration::from_millis(opts.timeout.unwrap_or_else(|| self.default_timeout().as_millis() as f64) as u64);
806 let poll = Duration::from_millis(opts.polling_interval.unwrap_or(100.0) as u64);
807 let function = format!("(arg) => {{ return ({expression}); }}");
808 let arg_val = arg.unwrap_or(Value::Null);
809
810 let deadline = tokio::time::Instant::now() + timeout;
811 loop {
812 let v =
813 selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val.clone())
814 .await?;
815 if is_truthy(&v) {
816 return serde_json::from_value::<R>(v).map_err(Error::from);
817 }
818 if tokio::time::Instant::now() >= deadline {
819 return Err(Error::Timeout(format!(
820 "wait_for_function timed out after {}ms (expression: {expression})",
821 timeout.as_millis()
822 )));
823 }
824 tokio::time::sleep(poll).await;
825 }
826 }
827
828 pub async fn eval_on_selector<R: DeserializeOwned>(
830 &self,
831 selector: &str,
832 expression: &str,
833 ) -> Result<R> {
834 let object_id = self
835 .resolve_strict(selector, Some(0))
836 .await?
837 .ok_or_else(|| Error::ElementNotFound(selector.to_string()))?;
838 let function = format!("(el) => {{ return ({expression}); }}");
839 let v = selectors::eval_object(&self.inner.session, &object_id, &function, Value::Null)
840 .await?;
841 self.release_object(&object_id).await;
842 serde_json::from_value::<R>(v).map_err(Error::from)
843 }
844
845 pub async fn eval_on_selector_all<R: DeserializeOwned>(
847 &self,
848 selector: &str,
849 expression: &str,
850 ) -> Result<Vec<R>> {
851 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
852 let mut out = Vec::with_capacity(n);
853 for i in 0..n {
854 if let Some(oid) = selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await? {
855 let function = format!("(el) => {{ return ({expression}); }}");
856 let v = selectors::eval_object(&self.inner.session, &oid, &function, Value::Null).await?;
857 self.release_object(&oid).await;
858 out.push(serde_json::from_value::<R>(v).map_err(Error::from)?);
859 }
860 }
861 Ok(out)
862 }
863
864 async fn release_object(&self, object_id: &str) {
865 let _ = self
866 .inner
867 .session
868 .send("Runtime.releaseObject", json!({ "objectId": object_id }))
869 .await;
870 }
871
872 pub async fn set_viewport_size(&self, viewport: Viewport) -> Result<()> {
875 *self.inner.viewport.lock() = Some(viewport);
876 self.inner
877 .session
878 .send(
879 "Emulation.setDeviceMetricsOverride",
880 json!({
881 "width": viewport.width,
882 "height": viewport.height,
883 "deviceScaleFactor": 1,
884 "mobile": false,
885 }),
886 )
887 .await?;
888 Ok(())
889 }
890
891 pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
892 let opts = opts.unwrap_or_default();
893 let format = match opts.r#type.unwrap_or_default() {
894 crate::types::ScreenshotType::Png => "png",
895 crate::types::ScreenshotType::Jpeg => "jpeg",
896 crate::types::ScreenshotType::Webp => "webp",
897 };
898 let mut params = json!({ "format": format });
899 if opts.full_page.unwrap_or(false) {
900 params["captureBeyondViewport"] = json!(true);
901 }
902 if opts.omit_background.unwrap_or(false) && format == "png" {
903 params["omitBackground"] = json!(true);
904 }
905 let resp = self
906 .inner
907 .session
908 .send("Page.captureScreenshot", params)
909 .await?;
910 let data = resp
911 .get("data")
912 .and_then(|v| v.as_str())
913 .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
914 let bytes = base64::engine::general_purpose::STANDARD
915 .decode(data)
916 .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
917 if let Some(path) = &opts.path {
918 tokio::fs::write(path, &bytes).await?;
919 }
920 Ok(bytes)
921 }
922
923 pub async fn add_script_tag(
927 &self,
928 content: Option<&str>,
929 url: Option<&str>,
930 ) -> Result<()> {
931 let arg = json!({ "content": content, "url": url });
932 let _ = selectors::eval_context(
933 &self.inner.session,
934 self.ctx().await,
935 "(a) => { const s = document.createElement('script'); if (a.url) s.src = a.url; if (a.content) s.textContent = a.content; document.head.appendChild(s); }",
936 arg,
937 )
938 .await?;
939 Ok(())
940 }
941
942 pub async fn add_style_tag(&self, content: &str) -> Result<()> {
944 let _ = selectors::eval_context(
945 &self.inner.session,
946 self.ctx().await,
947 "(a) => { const s = document.createElement('style'); s.textContent = a; document.head.appendChild(s); }",
948 json!(content),
949 )
950 .await?;
951 Ok(())
952 }
953
954 pub async fn set_cache_disabled(&self, disabled: bool) -> Result<()> {
956 self.inner
957 .session
958 .send("Network.setCacheDisabled", json!({ "cacheDisabled": disabled }))
959 .await
960 .map(|_: Value| ())
961 }
962
963 pub async fn bring_to_front(&self) -> Result<()> {
965 self.inner
966 .session
967 .send("Page.bringToFront", json!({}))
968 .await
969 .map(|_: Value| ())
970 }
971
972 pub async fn set_geolocation(&self, geolocation: Option<(f64, f64)>) -> Result<()> {
975 let params = match geolocation {
976 Some((lat, lon)) => json!({ "latitude": lat, "longitude": lon }),
977 None => json!({}),
978 };
979 self.inner
980 .session
981 .send("Emulation.setGeolocationOverride", params)
982 .await
983 .map(|_: Value| ())
984 }
985
986 pub fn request(&self) -> APIRequestContext {
995 APIRequestContext::new(Headers::new())
996 }
997
998 pub async fn set_extra_http_headers(&self, headers: Headers) -> Result<()> {
999 let list: Vec<Value> = headers
1001 .iter()
1002 .map(|(k, v)| json!({ "name": k, "value": v }))
1003 .collect();
1004 self.inner
1005 .session
1006 .send("Network.setExtraHTTPHeaders", json!({ "headers": list }))
1007 .await?;
1008 Ok(())
1009 }
1010
1011 pub async fn add_init_script(&self, script: &str) -> Result<()> {
1012 let _ = self
1013 .inner
1014 .session
1015 .send(
1016 "Page.addScriptToEvaluateOnNewDocument",
1017 json!({ "source": script }),
1018 )
1019 .await;
1020 let _ = selectors::eval_context(
1022 &self.inner.session,
1023 self.ctx().await,
1024 "(s) => { (0, eval)(s); }",
1025 json!(script),
1026 )
1027 .await;
1028 Ok(())
1029 }
1030
1031 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1058 where
1059 F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
1060 Fut: Future<Output = Value> + Send + 'static,
1061 {
1062 let handler: ExposedBindingHandler =
1065 Arc::new(move |args| Box::pin(callback(args)));
1066
1067 let binding_name = internal_binding_name(name);
1070 let _ = self
1071 .inner
1072 .session
1073 .send("Runtime.addBinding", json!({ "name": binding_name }))
1074 .await;
1075
1076 let wrapper = binding_wrapper_source(name);
1078 let _ = self
1079 .inner
1080 .session
1081 .send(
1082 "Page.addScriptToEvaluateOnNewDocument",
1083 json!({ "source": wrapper }),
1084 )
1085 .await;
1086 let _ = selectors::eval_context(
1088 &self.inner.session,
1089 self.ctx().await,
1090 "(s) => { (0, eval)(s); }",
1091 json!(wrapper),
1092 )
1093 .await;
1094
1095 let mut rx = self.inner.session.subscribe();
1097 let session = Arc::clone(&self.inner.session);
1098 let name_owned = name.to_string();
1099 tokio::spawn(async move {
1100 binding_listener(&mut rx, &session, &name_owned, &handler).await;
1101 });
1102
1103 Ok(())
1104 }
1105
1106 pub fn locator(&self, selector: impl Into<String>) -> Locator {
1109 Locator::new(self.clone(), selector.into(), true, None)
1110 }
1111
1112 pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator {
1121 FrameLocator::new(self.clone(), selector.into())
1122 }
1123
1124 pub fn get_by_text(&self, text: &str, _exact: bool) -> Locator {
1125 self.locator(format!("text={text}"))
1127 }
1128
1129 pub fn get_by_label(&self, text: &str) -> Locator {
1130 self.locator(format!("[aria-label=\"{}\"]", attr_escape(text)))
1131 }
1132
1133 pub fn get_by_placeholder(&self, text: &str) -> Locator {
1134 self.locator(format!("[placeholder=\"{}\"]", attr_escape(text)))
1135 }
1136
1137 pub fn get_by_alt_text(&self, text: &str) -> Locator {
1138 self.locator(format!("[alt=\"{}\"]", attr_escape(text)))
1139 }
1140
1141 pub fn get_by_title(&self, text: &str) -> Locator {
1142 self.locator(format!("[title=\"{}\"]", attr_escape(text)))
1143 }
1144
1145 pub fn get_by_test_id(&self, text: &str) -> Locator {
1146 self.locator(format!("[data-testid=\"{}\"]", attr_escape(text)))
1147 }
1148
1149 pub fn get_by_role(&self, role: AriaRole, opts: Option<crate::options::GetByRoleOptions>) -> Locator {
1150 let opts = opts.unwrap_or_default();
1151 let mut sel = format!("role={}", role.as_str());
1152 if let Some(name) = &opts.name {
1153 sel.push_str(&format!("[name=\"{name}\"]"));
1154 }
1155 if opts.exact == Some(true) {
1156 sel.push_str("[exact=\"true\"]");
1157 }
1158 Locator::new(self.clone(), sel, true, None)
1159 }
1160
1161 pub(crate) async fn resolve_strict(
1164 &self,
1165 selector: &str,
1166 forced_index: Option<usize>,
1167 ) -> Result<Option<String>> {
1168 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1169 if n == 0 {
1170 return Ok(None);
1171 }
1172 let index = forced_index.unwrap_or_else(|| {
1173 0
1176 });
1177 selectors::element_at(&self.inner.session, self.ctx().await, selector, index).await
1178 }
1179
1180 pub async fn query_selector(&self, selector: &str) -> Result<Option<ElementHandle>> {
1182 let oid = self.resolve_strict(selector, Some(0)).await?;
1183 Ok(oid.map(|oid| ElementHandle::new(self.clone(), oid)))
1184 }
1185
1186 pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementHandle>> {
1188 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1189 let mut out = Vec::with_capacity(n);
1190 for i in 0..n {
1191 if let Some(oid) =
1192 selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await?
1193 {
1194 out.push(ElementHandle::new(self.clone(), oid));
1195 }
1196 }
1197 Ok(out)
1198 }
1199
1200 pub fn keyboard(&self) -> Keyboard {
1203 Keyboard::new(self.clone())
1204 }
1205
1206 pub fn mouse(&self) -> Mouse {
1207 Mouse::with_pos(self.clone(), Arc::clone(&self.inner.mouse_pos))
1208 }
1209
1210 pub fn touchscreen(&self) -> Touchscreen {
1211 Touchscreen::with_flag(self.clone(), Arc::clone(&self.inner.touch_emulation_started))
1212 }
1213
1214 pub fn video(&self) -> Video {
1222 let session = Arc::clone(&self.inner.session);
1223 match self.inner.video_path.lock().clone() {
1224 Some(path) => Video::with_path(session, path),
1225 None => Video::new(session),
1226 }
1227 }
1228
1229 pub fn accessibility(&self) -> Accessibility {
1232 Accessibility::new(self.target_session())
1233 }
1234
1235 pub fn coverage(&self) -> Coverage {
1238 Coverage::new(self.target_session())
1239 }
1240
1241 pub fn clock(&self) -> Clock {
1243 Clock::new(self.target_session())
1244 }
1245
1246 pub async fn web_storage(&self) -> WebStorage {
1254 let origin = self.security_origin().await;
1255 WebStorage::local_storage(self.target_session(), origin)
1256 }
1257
1258 pub async fn local_storage(&self) -> WebStorage {
1260 let origin = self.security_origin().await;
1261 WebStorage::local_storage(self.target_session(), origin)
1262 }
1263
1264 pub async fn session_storage(&self) -> WebStorage {
1266 let origin = self.security_origin().await;
1267 WebStorage::session_storage(self.target_session(), origin)
1268 }
1269
1270 fn target_session(&self) -> CdpSession {
1278 let conn = self.inner.session.connection().clone();
1279 let session_id = self
1280 .inner
1281 .session
1282 .session_id()
1283 .map(|s| s.to_string())
1284 .unwrap_or_default();
1285 let session = CdpSession::target(conn, session_id);
1286 session.set_default_timeout_ms(self.inner.default_timeout_ms.load(Ordering::Relaxed));
1287 session
1288 }
1289
1290 async fn security_origin(&self) -> String {
1293 let url = self.url().await.unwrap_or_default();
1294 derive_security_origin(&url)
1295 }
1296
1297 pub fn main_frame(&self) -> Frame {
1298 Frame::main(self.clone())
1299 }
1300
1301 pub async fn frames(&self) -> Result<Vec<Frame>> {
1303 self.refresh_frame_tree().await?;
1304 let ids: Vec<String> = self.inner.frames.lock().keys().cloned().collect();
1305 Ok(ids.into_iter().map(|id| Frame::new(self.clone(), id)).collect())
1306 }
1307
1308 pub(crate) async fn refresh_frame_tree(&self) -> Result<()> {
1310 let resp = self
1311 .inner
1312 .session
1313 .send("Page.getFrameTree", json!({}))
1314 .await?;
1315 if let Some(tree) = resp.get("frameTree") {
1316 let mut frames = self.inner.frames.lock();
1317 frames.clear();
1318 walk_frame_tree(tree, &mut frames);
1319 if let Some(id) = tree
1320 .get("frame")
1321 .and_then(|f| f.get("id"))
1322 .and_then(|v| v.as_str())
1323 {
1324 *self.inner.main_frame_id.lock() = Some(id.to_string());
1325 }
1326 }
1327 Ok(())
1328 }
1329
1330 pub(crate) fn main_frame_id(&self) -> Option<String> {
1331 self.inner.main_frame_id.lock().clone()
1332 }
1333
1334 pub(crate) fn frame_data(&self, frame_id: &str) -> Option<FrameData> {
1335 self.inner.frames.lock().get(frame_id).cloned()
1336 }
1337
1338 pub(crate) fn frame_ids_with_parent(&self, parent: &str) -> Vec<String> {
1339 self.inner
1340 .frames
1341 .lock()
1342 .iter()
1343 .filter(|(_, d)| d.parent_id.as_deref() == Some(parent))
1344 .map(|(k, _)| k.clone())
1345 .collect()
1346 }
1347
1348 pub fn on_console<F, Fut>(&self, handler: F)
1351 where
1352 F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
1353 Fut: std::future::Future<Output = ()> + Send + 'static,
1354 {
1355 let mut rx = self.inner.session.subscribe();
1356 tokio::spawn(async move {
1357 while let Ok(ev) = rx.recv().await {
1358 if ev.method == "Runtime.consoleAPICalled" {
1359 let msg = parse_console(&ev.params);
1360 handler(msg).await;
1361 }
1362 }
1363 });
1364 }
1365
1366 pub fn on_load<F, Fut>(&self, handler: F)
1369 where
1370 F: Fn(()) -> Fut + Send + Sync + 'static,
1371 Fut: std::future::Future<Output = ()> + Send + 'static,
1372 {
1373 let mut rx = self.inner.session.subscribe();
1374 tokio::spawn(async move {
1375 while let Ok(ev) = rx.recv().await {
1376 if ev.method == "Page.loadEventFired" {
1377 handler(()).await;
1378 }
1379 }
1380 });
1381 }
1382
1383 pub fn on_crash<F, Fut>(&self, handler: F)
1386 where
1387 F: Fn(()) -> Fut + Send + Sync + 'static,
1388 Fut: std::future::Future<Output = ()> + Send + 'static,
1389 {
1390 let mut rx = self.inner.session.subscribe();
1391 tokio::spawn(async move {
1392 while let Ok(ev) = rx.recv().await {
1393 if ev.method == "Inspector.targetCrashed" {
1394 handler(()).await;
1395 }
1396 }
1397 });
1398 }
1399
1400 pub fn on_frameattached<F, Fut>(&self, handler: F)
1403 where
1404 F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1405 Fut: std::future::Future<Output = ()> + Send + 'static,
1406 {
1407 let mut rx = self.inner.session.subscribe();
1408 tokio::spawn(async move {
1409 while let Ok(ev) = rx.recv().await {
1410 if ev.method == "Page.frameAttached" {
1411 if let Some(fe) = FrameEvent::from_attached(&ev.params) {
1412 handler(fe).await;
1413 }
1414 }
1415 }
1416 });
1417 }
1418
1419 pub fn on_framedetached<F, Fut>(&self, handler: F)
1422 where
1423 F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1424 Fut: std::future::Future<Output = ()> + Send + 'static,
1425 {
1426 let mut rx = self.inner.session.subscribe();
1427 tokio::spawn(async move {
1428 while let Ok(ev) = rx.recv().await {
1429 if ev.method == "Page.frameDetached" {
1430 if let Some(fe) = FrameEvent::from_detached(&ev.params) {
1431 handler(fe).await;
1432 }
1433 }
1434 }
1435 });
1436 }
1437
1438 pub fn on_framenavigated<F, Fut>(&self, handler: F)
1441 where
1442 F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1443 Fut: std::future::Future<Output = ()> + Send + 'static,
1444 {
1445 let mut rx = self.inner.session.subscribe();
1446 tokio::spawn(async move {
1447 while let Ok(ev) = rx.recv().await {
1448 if ev.method == "Page.frameNavigated" {
1449 if let Some(fe) = FrameEvent::from_navigated(&ev.params) {
1450 handler(fe).await;
1451 }
1452 }
1453 }
1454 });
1455 }
1456
1457 pub fn on_dialog<F, Fut>(&self, handler: F)
1458 where
1459 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1460 Fut: std::future::Future<Output = ()> + Send + 'static,
1461 {
1462 let mut rx = self.inner.session.subscribe();
1463 let session = Arc::clone(&self.inner.session);
1464 tokio::spawn(async move {
1465 while let Ok(ev) = rx.recv().await {
1466 if ev.method == "Page.javascriptDialogOpening" {
1467 let dialog = Dialog::from_event(&ev.params, &session);
1468 handler(dialog).await;
1469 }
1470 }
1471 });
1472 }
1473
1474 pub fn on_request<F, Fut>(&self, handler: F)
1476 where
1477 F: Fn(Request) -> Fut + Send + Sync + 'static,
1478 Fut: Future<Output = ()> + Send + 'static,
1479 {
1480 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1481 self.inner.on_request_handlers.lock().push(h);
1482 }
1483
1484 pub async fn expect_request(
1512 &self,
1513 url_predicate: &str,
1514 timeout: Option<Duration>,
1515 ) -> Result<Request> {
1516 let (tx, rx) = oneshot::channel::<Request>();
1517 let tx = Arc::new(Mutex::new(Some(tx)));
1518 let url_predicate = url_predicate.to_string();
1520 self.on_request({
1521 let url_predicate = url_predicate.clone();
1524 move |req| {
1525 let tx = Arc::clone(&tx);
1526 let url_predicate = url_predicate.clone();
1529 Box::pin(async move {
1530 if req.url().contains(&url_predicate) {
1531 if let Some(sender) = tx.lock().take() {
1532 let _ = sender.send(req);
1533 }
1534 }
1535 })
1536 }
1537 });
1538 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1539 Ok(Ok(req)) => Ok(req),
1540 Ok(Err(_)) => Err(Error::Timeout(
1541 "expect_request: oneshot sender dropped before a match arrived".into(),
1542 )),
1543 Err(_) => Err(Error::Timeout(format!(
1544 "expect_request: no request matched {url_predicate:?} within the timeout"
1545 ))),
1546 }
1547 }
1548
1549 pub fn on_response<F, Fut>(&self, handler: F)
1551 where
1552 F: Fn(Response) -> Fut + Send + Sync + 'static,
1553 Fut: Future<Output = ()> + Send + 'static,
1554 {
1555 let h: ResponseHandler = Arc::new(move |r| Box::pin(handler(r)));
1556 self.inner.on_response_handlers.lock().push(h);
1557 }
1558
1559 pub async fn expect_response(
1585 &self,
1586 url_predicate: &str,
1587 timeout: Option<Duration>,
1588 ) -> Result<Response> {
1589 let (tx, rx) = oneshot::channel::<Response>();
1590 let tx = Arc::new(Mutex::new(Some(tx)));
1591 let url_predicate = url_predicate.to_string();
1593 self.on_response({
1594 let url_predicate = url_predicate.clone();
1597 move |resp| {
1598 let tx = Arc::clone(&tx);
1599 let url_predicate = url_predicate.clone();
1602 Box::pin(async move {
1603 if resp.url().contains(&url_predicate) {
1604 if let Some(sender) = tx.lock().take() {
1605 let _ = sender.send(resp);
1606 }
1607 }
1608 })
1609 }
1610 });
1611 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1612 Ok(Ok(resp)) => Ok(resp),
1613 Ok(Err(_)) => Err(Error::Timeout(
1614 "expect_response: oneshot sender dropped before a match arrived".into(),
1615 )),
1616 Err(_) => Err(Error::Timeout(format!(
1617 "expect_response: no response matched {url_predicate:?} within the timeout"
1618 ))),
1619 }
1620 }
1621
1622 pub async fn expect_event(
1643 &self,
1644 event_name: &str,
1645 timeout: Option<Duration>,
1646 ) -> Result<Value> {
1647 let mut rx = self.inner.session.subscribe();
1650 let effective = timeout.unwrap_or(Duration::from_secs(30));
1651 let deadline = tokio::time::Instant::now() + effective;
1652
1653 loop {
1654 match tokio::time::timeout_at(deadline, rx.recv()).await {
1655 Ok(Ok(ev)) if ev.method == event_name => return Ok(ev.params),
1656 Ok(Ok(_)) => continue,
1657 Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
1658 Ok(Err(broadcast::error::RecvError::Closed)) => {
1659 return Err(Error::Timeout(format!(
1660 "expect_event('{event_name}'): event stream closed before a match arrived"
1661 )));
1662 }
1663 Err(_) => {
1664 return Err(Error::Timeout(format!(
1665 "expect_event('{event_name}') timed out after {}ms",
1666 effective.as_millis()
1667 )));
1668 }
1669 }
1670 }
1671 }
1672
1673 pub fn on_requestfailed<F, Fut>(&self, handler: F)
1675 where
1676 F: Fn(Request) -> Fut + Send + Sync + 'static,
1677 Fut: Future<Output = ()> + Send + 'static,
1678 {
1679 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1680 self.inner.on_requestfailed_handlers.lock().push(h);
1681 }
1682
1683 pub fn on_pageerror<F, Fut>(&self, handler: F)
1685 where
1686 F: Fn(String) -> Fut + Send + Sync + 'static,
1687 Fut: Future<Output = ()> + Send + 'static,
1688 {
1689 let mut rx = self.inner.session.subscribe();
1690 tokio::spawn(async move {
1691 while let Ok(ev) = rx.recv().await {
1692 if ev.method == "Runtime.exceptionThrown" {
1693 let msg = ev
1694 .params
1695 .get("exceptionDetails")
1696 .and_then(|d| d.get("exception"))
1697 .and_then(|e| e.get("description"))
1698 .and_then(|v| v.as_str())
1699 .unwrap_or("unknown error")
1700 .to_string();
1701 handler(msg).await;
1702 }
1703 }
1704 });
1705 }
1706
1707 pub fn on_websocket<F, Fut>(&self, handler: F)
1719 where
1720 F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1721 Fut: Future<Output = ()> + Send + 'static,
1722 {
1723 let mut rx = self.inner.web_socket_registry.on_created();
1724 let handler: Arc<dyn Fn(WebSocket) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync> =
1727 Arc::new(move |ws| Box::pin(handler(ws)));
1728 tokio::spawn(async move {
1729 while let Ok(ws) = rx.recv().await {
1730 let h = Arc::clone(&handler);
1731 tokio::spawn(async move {
1734 h(ws).await;
1735 });
1736 }
1737 });
1738 }
1739
1740 pub fn on_requestfinished<F, Fut>(&self, handler: F)
1742 where
1743 F: Fn(Request) -> Fut + Send + Sync + 'static,
1744 Fut: Future<Output = ()> + Send + 'static,
1745 {
1746 let mut rx = self.inner.session.subscribe();
1747 let store = Arc::clone(&self.inner.network_store);
1748 tokio::spawn(async move {
1749 while let Ok(ev) = rx.recv().await {
1750 if ev.method == "Network.loadingFinished" {
1751 if let Some(rid) = ev.params.get("requestId").and_then(|v| v.as_str()) {
1752 if let Some(req) = store.get_request(rid) {
1753 handler(req).await;
1754 }
1755 }
1756 }
1757 }
1758 });
1759 }
1760
1761 pub fn on_close<F, Fut>(&self, handler: F)
1763 where
1764 F: Fn() -> Fut + Send + Sync + 'static,
1765 Fut: Future<Output = ()> + Send + 'static,
1766 {
1767 let h: CloseHandler = Arc::new(move || Box::pin(handler()));
1768 self.inner.on_close_handlers.lock().push(h);
1769 }
1770
1771 pub fn on_download<F, Fut>(&self, handler: F)
1778 where
1779 F: Fn(Download) -> Fut + Send + Sync + 'static,
1780 Fut: Future<Output = ()> + Send + 'static,
1781 {
1782 if self
1784 .inner
1785 .download_started
1786 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1787 .is_ok()
1788 {
1789 let dir = match tempfile::TempDir::new() {
1790 Ok(d) => Arc::new(d),
1791 Err(e) => {
1792 tracing::error!("failed to create download temp dir: {e}");
1793 return;
1794 }
1795 };
1796 let dir_path = dir.path().to_path_buf();
1797 *self.inner.download_dir.lock() = Some(Arc::clone(&dir));
1798 *self.inner.download_path.lock() = Some(dir_path.clone());
1799
1800 let session_for_setup = Arc::clone(&self.inner.session);
1802 let setup_path = dir_path.clone();
1803 tokio::spawn(async move {
1804 let _ = session_for_setup
1805 .send(
1806 "Page.setDownloadBehavior",
1807 json!({ "behavior": "allow", "downloadPath": setup_path }),
1808 )
1809 .await;
1810 });
1811
1812 let mut rx = self.inner.session.subscribe();
1814 let session = Arc::clone(&self.inner.session);
1815 let states = Arc::clone(&self.inner.download_states);
1816 let download_path = dir_path.clone();
1817 let page = self.clone();
1820 let handler: Arc<dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync> =
1821 Arc::new(move |d| Box::pin(handler(d)));
1822 tokio::spawn(async move {
1823 download_listener(
1824 &mut rx,
1825 &session,
1826 &states,
1827 &download_path,
1828 page.clone(),
1829 &handler,
1830 )
1831 .await;
1832 });
1833 }
1834 }
1835
1836 pub async fn expect_download(
1868 &self,
1869 url_predicate: &str,
1870 timeout: Option<Duration>,
1871 ) -> Result<Download> {
1872 let (tx, rx) = oneshot::channel::<Download>();
1873 let tx = Arc::new(Mutex::new(Some(tx)));
1874 let url_predicate = url_predicate.to_string();
1876 self.on_download({
1877 let url_predicate = url_predicate.clone();
1880 move |dl| {
1881 let tx = Arc::clone(&tx);
1882 let url_predicate = url_predicate.clone();
1885 Box::pin(async move {
1886 if dl.url().contains(&url_predicate) {
1887 if let Some(sender) = tx.lock().take() {
1888 let _ = sender.send(dl);
1889 }
1890 }
1891 })
1892 }
1893 });
1894 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1895 Ok(Ok(dl)) => Ok(dl),
1896 Ok(Err(_)) => Err(Error::Timeout(
1897 "expect_download: oneshot sender dropped before a match arrived".into(),
1898 )),
1899 Err(_) => Err(Error::Timeout(format!(
1900 "expect_download: no download matched {url_predicate:?} within the timeout"
1901 ))),
1902 }
1903 }
1904
1905 pub async fn expect_console_message(
1933 &self,
1934 text_predicate: &str,
1935 timeout: Option<Duration>,
1936 ) -> Result<ConsoleMessage> {
1937 let (tx, rx) = oneshot::channel::<ConsoleMessage>();
1938 let tx = Arc::new(Mutex::new(Some(tx)));
1939 let text_predicate = text_predicate.to_string();
1941 self.on_console({
1942 let text_predicate = text_predicate.clone();
1945 move |msg| {
1946 let tx = Arc::clone(&tx);
1947 let text_predicate = text_predicate.clone();
1950 Box::pin(async move {
1951 if msg.text().contains(&text_predicate) {
1952 if let Some(sender) = tx.lock().take() {
1953 let _ = sender.send(msg);
1954 }
1955 }
1956 })
1957 }
1958 });
1959 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1960 Ok(Ok(msg)) => Ok(msg),
1961 Ok(Err(_)) => Err(Error::Timeout(
1962 "expect_console_message: oneshot sender dropped before a match arrived".into(),
1963 )),
1964 Err(_) => Err(Error::Timeout(format!(
1965 "expect_console_message: no message matched {text_predicate:?} within the timeout"
1966 ))),
1967 }
1968 }
1969
1970 pub async fn expect_popup(&self, timeout: Option<Duration>) -> Result<Page> {
2004 let (tx, rx) = oneshot::channel::<Page>();
2005 let tx = Arc::new(Mutex::new(Some(tx)));
2006 self.on_popup(move |popup| {
2007 let tx = Arc::clone(&tx);
2008 Box::pin(async move {
2009 if let Some(sender) = tx.lock().take() {
2010 let _ = sender.send(popup);
2011 }
2012 })
2013 })
2014 .await;
2015 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
2016 Ok(Ok(popup)) => Ok(popup),
2017 Ok(Err(_)) => Err(Error::Timeout(
2018 "expect_popup: oneshot sender dropped before a popup arrived".into(),
2019 )),
2020 Err(_) => Err(Error::Timeout(
2021 "expect_popup: no popup opened within the timeout".into(),
2022 )),
2023 }
2024 }
2025
2026 pub async fn on_popup<F, Fut>(&self, handler: F)
2057 where
2058 F: Fn(Page) -> Fut + Send + Sync + 'static,
2059 Fut: Future<Output = ()> + Send + 'static,
2060 {
2061 let handler: PopupHandler = Arc::new(move |p| Box::pin(handler(p)));
2063 self.inner.on_popup_handlers.lock().push(handler);
2064
2065 if self
2067 .inner
2068 .popup_started
2069 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2070 .is_ok()
2071 {
2072 let browser = self.inner.browser.clone();
2073 let default_timeout_ms = self.inner.default_timeout_ms.load(Ordering::Relaxed);
2074 let opener_target_id = self.inner.target_id.clone();
2075 let handlers = Arc::clone(&self.inner.on_popup_handlers);
2076 let browser_session = browser.new_browser_cdp_session();
2077
2078 tokio::spawn(async move {
2079 let resp = match browser_session
2083 .send("Target.getTargets", json!({}))
2084 .await
2085 {
2086 Ok(r) => r,
2087 Err(_) => return,
2088 };
2089 let targets = resp
2090 .get("targetInfos")
2091 .and_then(|v| v.as_array())
2092 .cloned()
2093 .unwrap_or_default();
2094 let my_context = targets
2096 .iter()
2097 .find(|t| {
2098 t.get("targetId").and_then(|v| v.as_str()) == Some(&opener_target_id)
2099 })
2100 .and_then(|t| t.get("browserContextId"))
2101 .and_then(|v| v.as_str())
2102 .map(|s| s.to_string());
2103 let mut delivered: std::collections::HashSet<String> =
2113 std::collections::HashSet::new();
2114
2115 loop {
2116 tokio::time::sleep(Duration::from_millis(100)).await;
2117 let resp = match browser_session.send("Target.getTargets", json!({})).await {
2118 Ok(r) => r,
2119 Err(_) => continue,
2120 };
2121 let targets = match resp.get("targetInfos").and_then(|v| v.as_array()) {
2122 Some(a) => a,
2123 None => continue,
2124 };
2125 let new_popups: Vec<&Value> = targets
2128 .iter()
2129 .filter(|t| t.get("type").and_then(|v| v.as_str()) == Some("page"))
2130 .filter(|t| {
2131 my_context
2132 .as_deref()
2133 .zip(t.get("browserContextId").and_then(|v| v.as_str()))
2134 .map(|(mc, c)| mc == c)
2135 .unwrap_or(true)
2136 })
2137 .filter(|t| {
2138 t.get("targetId")
2139 .and_then(|v| v.as_str())
2140 .map(|id| {
2141 !delivered.contains(id) && id != opener_target_id
2142 })
2143 .unwrap_or(false)
2144 })
2145 .collect();
2146 if new_popups.is_empty() {
2147 continue;
2148 }
2149 for np in new_popups {
2150 let target_id = match np.get("targetId").and_then(|v| v.as_str()) {
2151 Some(id) => id.to_string(),
2152 None => continue,
2153 };
2154 delivered.insert(target_id.clone());
2155 let attach = match browser_session
2157 .send(
2158 "Target.attachToTarget",
2159 json!({ "targetId": target_id, "flatten": true }),
2160 )
2161 .await
2162 {
2163 Ok(r) => r,
2164 Err(_) => continue,
2165 };
2166 let sid = match attach.get("sessionId").and_then(|v| v.as_str()) {
2167 Some(s) => s.to_string(),
2168 None => continue,
2169 };
2170 let popup = match Page::attach(
2174 browser.clone(),
2175 sid,
2176 target_id,
2177 &[],
2178 default_timeout_ms,
2179 None,
2180 None,
2181 None,
2182 )
2183 .await
2184 {
2185 Ok(p) => p,
2186 Err(_) => continue,
2187 };
2188 let snapshot = handlers.lock().clone();
2190 for h in snapshot {
2191 let p = popup.clone();
2192 tokio::spawn(async move {
2193 (h)(p).await;
2194 });
2195 }
2196 }
2197 }
2198 });
2199 }
2200 }
2201
2202 pub async fn on_filechooser<F, Fut>(&self, handler: F)
2210 where
2211 F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
2212 Fut: Future<Output = ()> + Send + 'static,
2213 {
2214 if self
2216 .inner
2217 .filechooser_started
2218 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2219 .is_ok()
2220 {
2221 let mut rx = self.inner.session.subscribe();
2224 let _ = self
2227 .inner
2228 .session
2229 .send(
2230 "Page.setInterceptFileChooserDialog",
2231 json!({ "enabled": true }),
2232 )
2233 .await;
2234
2235 let page = self.clone();
2237 let handler: Arc<
2238 dyn Fn(FileChooser) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>>
2239 + Send
2240 + Sync,
2241 > = Arc::new(move |fc| Box::pin(handler(fc)));
2242 tokio::spawn(async move {
2243 while let Ok(ev) = rx.recv().await {
2244 if ev.method == "Page.fileChooserOpened" {
2245 let multiple = ev
2246 .params
2247 .get("mode")
2248 .and_then(|v| v.as_str())
2249 == Some("selectMultiple");
2250 let backend_node_id = ev
2251 .params
2252 .get("backendNodeId")
2253 .and_then(|v| v.as_i64());
2254 let fc = FileChooser::new(page.clone(), backend_node_id, multiple);
2255 let h = Arc::clone(&handler);
2256 tokio::spawn(async move {
2257 (h)(fc).await;
2258 });
2259 }
2260 }
2261 });
2262 }
2263 }
2264
2265 pub async fn on_worker<F, Fut>(&self, handler: F)
2276 where
2277 F: Fn(Worker) -> Fut + Send + Sync + 'static,
2278 Fut: Future<Output = ()> + Send + 'static,
2279 {
2280 if self
2282 .inner
2283 .worker_started
2284 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2285 .is_ok()
2286 {
2287 let mut rx = self.inner.session.subscribe();
2290 let _ = self
2291 .inner
2292 .session
2293 .send(
2294 "Target.setAutoAttach",
2295 json!({ "autoAttach": true, "waitForDebuggerOnStart": false, "flatten": true }),
2296 )
2297 .await;
2298
2299 let connection = self.inner.session.connection().clone();
2300 let handler: Arc<
2301 dyn Fn(Worker) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
2302 > = Arc::new(move |w| Box::pin(handler(w)));
2303 tokio::spawn(async move {
2304 while let Ok(ev) = rx.recv().await {
2305 if ev.method != "Target.attachedToTarget" {
2306 continue;
2307 }
2308 let session_id = ev
2310 .params
2311 .get("sessionId")
2312 .or_else(|| ev.params.get("session_id"))
2313 .and_then(|v| v.as_str());
2314 let target_info = match ev.params.get("targetInfo") {
2315 Some(t) => t,
2316 None => continue,
2317 };
2318 let target_type = target_info
2319 .get("type")
2320 .and_then(|v| v.as_str())
2321 .unwrap_or("");
2322 if !matches!(target_type, "worker" | "service_worker" | "shared_worker") {
2324 continue;
2325 }
2326 let (sid, url) = match (session_id, target_info.get("url").and_then(|v| v.as_str())) {
2327 (Some(sid), Some(url)) => (sid.to_string(), url.to_string()),
2328 _ => continue,
2329 };
2330 let worker = Worker::new(connection.clone(), sid, url);
2331 worker.enable_runtime().await;
2333 let h = Arc::clone(&handler);
2334 tokio::spawn(async move {
2335 (h)(worker).await;
2336 });
2337 }
2338 });
2339 }
2340 }
2341
2342 pub async fn close(&self) -> Result<()> {
2345 if *self.inner.closed.lock() {
2346 return Ok(());
2347 }
2348 *self.inner.closed.lock() = true;
2349 let handlers = std::mem::take(&mut *self.inner.on_close_handlers.lock());
2350 for h in handlers {
2351 tokio::spawn(async move { (h)().await; });
2352 }
2353 let _ = self
2354 .inner
2355 .browser
2356 .browser_session()
2357 .send("Target.closeTarget", json!({ "targetId": self.inner.target_id }))
2358 .await;
2359 Ok(())
2360 }
2361
2362 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
2368 where
2369 F: Fn(Route) -> Fut + Send + Sync + 'static,
2370 Fut: Future<Output = ()> + Send + 'static,
2371 {
2372 let entry = RouteEntry {
2373 pattern: pattern.to_string(),
2374 handler: Arc::new(move |r| Box::pin(handler(r))),
2375 };
2376 self.inner.route_handlers.lock().push(entry);
2377 self.ensure_route_listener().await;
2378 self.refresh_fetch_patterns().await
2379 }
2380
2381 pub async fn unroute(&self, pattern: &str) -> Result<()> {
2383 let mut handlers = self.inner.route_handlers.lock();
2384 if let Some(pos) = handlers.iter().position(|e| e.pattern == pattern) {
2385 handlers.remove(pos);
2386 }
2387 drop(handlers);
2388 self.refresh_fetch_patterns().await
2389 }
2390
2391 pub async fn unroute_all(&self) -> Result<()> {
2393 self.inner.route_handlers.lock().clear();
2394 let _ = self.inner.session.send("Fetch.disable", json!({})).await;
2395 Ok(())
2396 }
2397
2398 async fn ensure_route_listener(&self) {
2399 if self
2400 .inner
2401 .route_started
2402 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
2403 .is_ok()
2404 {
2405 let rx = self.inner.session.subscribe();
2406 let session = Arc::clone(&self.inner.session);
2407 let store = Arc::clone(&self.inner.network_store);
2408 let handlers = Arc::clone(&self.inner.route_handlers);
2409 tokio::spawn(async move {
2410 route_listener(rx, session, store, handlers).await;
2411 });
2412 }
2413 }
2414
2415 async fn refresh_fetch_patterns(&self) -> Result<()> {
2416 let patterns: Vec<Value> = self
2417 .inner
2418 .route_handlers
2419 .lock()
2420 .iter()
2421 .map(|e| json!({ "urlPattern": e.pattern }))
2422 .collect();
2423 if patterns.is_empty() {
2424 let _ = self.inner.session.send("Fetch.disable", json!({})).await;
2425 } else {
2426 self.inner
2427 .session
2428 .send("Fetch.enable", json!({ "patterns": patterns }))
2429 .await?;
2430 }
2431 Ok(())
2432 }
2433
2434 pub async fn pdf(&self, opts: Option<crate::options::PdfOptions>) -> Result<Vec<u8>> {
2437 let opts = opts.unwrap_or_default();
2438 let mut params = json!({ "printBackground": opts.print_background.unwrap_or(true) });
2439 if let Some(fmt) = opts.format {
2440 let (w, h) = fmt.inches();
2441 params["paperWidth"] = json!(w);
2442 params["paperHeight"] = json!(h);
2443 }
2444 if let Some(l) = opts.landscape {
2445 params["landscape"] = json!(l);
2446 }
2447 if let Some(s) = opts.scale {
2448 params["scale"] = json!(s);
2449 }
2450 if let Some(p) = opts.prefer_css_page_size {
2451 params["preferCSSPageSize"] = json!(p);
2452 }
2453 if let Some(m) = opts.margin {
2454 if let Some(v) = m.top {
2455 params["marginTop"] = json!(v);
2456 }
2457 if let Some(v) = m.bottom {
2458 params["marginBottom"] = json!(v);
2459 }
2460 if let Some(v) = m.left {
2461 params["marginLeft"] = json!(v);
2462 }
2463 if let Some(v) = m.right {
2464 params["marginRight"] = json!(v);
2465 }
2466 }
2467 let resp = self.inner.session.send("Page.printToPDF", params).await?;
2468 let data = resp
2469 .get("data")
2470 .and_then(|v| v.as_str())
2471 .ok_or_else(|| Error::ProtocolError("pdf missing data".into()))?;
2472 Ok(base64::engine::general_purpose::STANDARD
2473 .decode(data)
2474 .map_err(|e| Error::ProtocolError(format!("pdf base64 decode: {e}")))?)
2475 }
2476
2477 pub async fn aria_snapshot(&self) -> Result<String> {
2484 let _ = self
2485 .inner
2486 .session
2487 .send("Accessibility.enable", json!({}))
2488 .await;
2489 let resp = self
2490 .inner
2491 .session
2492 .send("Accessibility.getFullAXTree", json!({}))
2493 .await?;
2494 let nodes = resp
2495 .get("nodes")
2496 .and_then(|v| v.as_array())
2497 .cloned()
2498 .unwrap_or_default();
2499 Ok(crate::aria_snapshot::serialize(&nodes, None))
2500 }
2501}
2502
2503fn attr_escape(s: &str) -> String {
2504 s.replace('\\', "\\\\").replace('"', "\\\"")
2505}
2506
2507fn derive_security_origin(url: &str) -> String {
2513 let after_scheme = match url.find("://") {
2515 Some(i) => &url[i + 3..],
2516 None => return String::new(),
2517 };
2518 let scheme = &url[..url.find("://").unwrap()];
2519 if matches!(scheme, "about" | "data" | "blob" | "javascript") {
2521 return String::new();
2522 }
2523 let end = after_scheme
2525 .find(|c: char| c == '/' || c == '?' || c == '#')
2526 .unwrap_or(after_scheme.len());
2527 let authority = &after_scheme[..end];
2528 if authority.is_empty() {
2529 return String::new();
2530 }
2531 format!("{scheme}://{authority}")
2532}
2533
2534fn is_truthy(v: &Value) -> bool {
2536 match v {
2537 Value::Null => false,
2538 Value::Bool(b) => *b,
2539 Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
2540 Value::String(s) => !s.is_empty(),
2541 _ => true,
2542 }
2543}
2544
2545fn glob_matches(pattern: &str, value: &str) -> bool {
2548 if pattern == value {
2549 return true;
2550 }
2551 if pattern.len() >= 2 && pattern.starts_with('*') && pattern.ends_with('*') {
2553 return value.contains(&pattern[1..pattern.len() - 1]);
2554 }
2555 if let Some(rest) = pattern.strip_prefix('*') {
2556 if value.ends_with(rest) {
2557 return true;
2558 }
2559 }
2560 if let Some(rest) = pattern.strip_suffix('*') {
2561 if value.starts_with(rest) {
2562 return true;
2563 }
2564 }
2565 false
2566}
2567
2568fn walk_frame_tree(node: &Value, frames: &mut HashMap<String, FrameData>) {
2570 let frame = match node.get("frame") {
2571 Some(f) => f,
2572 None => return,
2573 };
2574 let id = match frame.get("id").and_then(|v| v.as_str()) {
2575 Some(s) => s.to_string(),
2576 None => return,
2577 };
2578 let data = FrameData {
2579 url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2580 name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2581 parent_id: frame
2582 .get("parentId")
2583 .and_then(|v| v.as_str())
2584 .map(String::from),
2585 detached: false,
2586 };
2587 frames.insert(id, data);
2588 if let Some(children) = node.get("childFrames").and_then(|v| v.as_array()) {
2589 for child in children {
2590 walk_frame_tree(child, frames);
2591 }
2592 }
2593}
2594
2595async fn frame_tracker(
2597 mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
2598 frames: Arc<Mutex<HashMap<String, FrameData>>>,
2599 main_id: Arc<Mutex<Option<String>>>,
2600) {
2601 loop {
2602 match rx.recv().await {
2603 Ok(ev) => match ev.method.as_str() {
2604 "Page.frameNavigated" => {
2605 if let Some(frame) = ev.params.get("frame") {
2606 let id = match frame.get("id").and_then(|v| v.as_str()) {
2607 Some(s) => s.to_string(),
2608 None => continue,
2609 };
2610 let parent = frame
2611 .get("parentId")
2612 .and_then(|v| v.as_str())
2613 .map(String::from);
2614 let data = FrameData {
2615 url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2616 name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2617 parent_id: parent.clone(),
2618 detached: false,
2619 };
2620 if parent.is_none() {
2621 *main_id.lock() = Some(id.clone());
2622 }
2623 frames.lock().insert(id, data);
2624 }
2625 }
2626 "Page.frameDetached" => {
2627 if let Some(id) = ev.params.get("frameId").and_then(|v| v.as_str()) {
2628 if let Some(d) = frames.lock().get_mut(id) {
2629 d.detached = true;
2630 }
2631 }
2632 }
2633 _ => {}
2634 },
2635 Err(broadcast::error::RecvError::Closed) => break,
2636 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2637 }
2638 }
2639}
2640
2641async fn download_listener(
2643 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
2644 _session: &Arc<CdpSession>,
2645 states: &Arc<Mutex<HashMap<String, DownloadStateCell>>>,
2646 download_path: &std::path::Path,
2647 page: Page,
2648 handler: &Arc<
2649 dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
2650 >,
2651) {
2652 while let Ok(ev) = rx.recv().await {
2653 match ev.method.as_str() {
2654 "Page.downloadWillBegin" => {
2655 let guid = ev
2656 .params
2657 .get("guid")
2658 .and_then(|v| v.as_str())
2659 .unwrap_or("")
2660 .to_string();
2661 let url = ev
2662 .params
2663 .get("url")
2664 .and_then(|v| v.as_str())
2665 .unwrap_or("")
2666 .to_string();
2667 let suggested = ev
2668 .params
2669 .get("suggestedFilename")
2670 .and_then(|v| v.as_str())
2671 .unwrap_or("download")
2672 .to_string();
2673 let cell = DownloadStateCell::new();
2674 states.lock().insert(guid.clone(), cell.clone());
2675 let dl = Download::new(
2676 url,
2677 suggested,
2678 guid,
2679 cell,
2680 download_path.to_path_buf(),
2681 page.clone(),
2682 );
2683 let h = Arc::clone(handler);
2684 tokio::spawn(async move {
2685 (h)(dl).await;
2686 });
2687 }
2688 "Page.downloadProgress" => {
2689 let guid = match ev.params.get("guid").and_then(|v| v.as_str()) {
2690 Some(g) => g.to_string(),
2691 None => continue,
2692 };
2693 let state = ev
2694 .params
2695 .get("state")
2696 .and_then(|v| v.as_str())
2697 .unwrap_or("InProgress");
2698 let mapped = match state {
2699 "Completed" => DownloadState::Completed,
2700 "Canceled" => DownloadState::Canceled,
2701 _ => DownloadState::InProgress,
2702 };
2703 if let Some(cell) = states.lock().get(&guid) {
2704 *cell.state.lock() = mapped;
2705 }
2706 }
2707 _ => {}
2708 }
2709 }
2710}
2711
2712fn internal_binding_name(name: &str) -> String {
2716 format!("__pwcdpInvoke_{name}")
2717}
2718
2719fn binding_wrapper_source(name: &str) -> String {
2724 let name_json = serde_json::to_string(name).unwrap_or_else(|_| "\"\"".to_string());
2727 format!(
2728 r#"(function(){{
2729 var publicName = {name_json};
2730 var invokeName = "__pwcdpInvoke_" + publicName;
2731 var pending = (self.__pwcdpBindings = self.__pwcdpBindings || {{}});
2732 var map = pending[publicName] = pending[publicName] || {{ id: 0, cbs: {{}} }};
2733 self[publicName] = function(){{
2734 var args = Array.prototype.slice.call(arguments);
2735 return new Promise(function(resolve){{
2736 var id = ++map.id;
2737 map.cbs[id] = resolve;
2738 // The CDP binding (self[invokeName]) is installed by Runtime.addBinding;
2739 // if it is not present yet, surface an error so callers see a clear cause.
2740 if (typeof self[invokeName] !== "function") {{
2741 delete map.cbs[id];
2742 resolve({{ "__error": "binding not installed: " + invokeName }});
2743 return;
2744 }}
2745 try {{
2746 self[invokeName](JSON.stringify({{ id: id, args: args }}));
2747 }} catch (e) {{
2748 delete map.cbs[id];
2749 resolve({{ "__error": String((e && e.message) || e) }});
2750 }}
2751 }});
2752 }};
2753}})();
2754"#
2755 )
2756}
2757
2758async fn binding_listener(
2762 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
2763 session: &Arc<CdpSession>,
2764 name: &str,
2765 handler: &ExposedBindingHandler,
2766) {
2767 let binding_name = internal_binding_name(name);
2768 loop {
2769 match rx.recv().await {
2770 Ok(ev) if ev.method == "Runtime.bindingCalled" => {
2771 let fired_name = ev
2772 .params
2773 .get("name")
2774 .and_then(|v| v.as_str())
2775 .unwrap_or("");
2776 if fired_name != binding_name {
2777 continue;
2778 }
2779 let payload_str = match ev.params.get("payload").and_then(|v| v.as_str()) {
2780 Some(s) => s,
2781 None => continue,
2782 };
2783 let parsed: Value = match serde_json::from_str(payload_str) {
2784 Ok(v) => v,
2785 Err(_) => continue,
2786 };
2787 let id = match parsed.get("id").and_then(|v| v.as_i64()) {
2788 Some(i) => i,
2789 None => continue,
2790 };
2791 let args = parsed
2792 .get("args")
2793 .and_then(|v| v.as_array())
2794 .cloned()
2795 .unwrap_or_default();
2796
2797 let h = Arc::clone(handler);
2798 let session = Arc::clone(session);
2799 let name_owned = name.to_string();
2800 tokio::spawn(async move {
2803 let result = (h)(args).await;
2804 let result_json = serde_json::to_string(&result)
2805 .unwrap_or_else(|_| r#"{"__error":"serialize failed"}"#.to_string());
2806 let result_str_literal = serde_json::to_string(&result_json)
2809 .unwrap_or_else(|_| r#""{\"__error\":\"serialize failed\"}""#.to_string());
2810 let name_j = serde_json::to_string(&name_owned).unwrap_or_else(|_| "\"\"".into());
2813 let expr = format!(
2814 "(function(){{
2815 var m = (self.__pwcdpBindings && self.__pwcdpBindings[{name_j}]) || null;
2816 if (!m || !m.cbs || !m.cbs[{id}]) return;
2817 var fn_ = m.cbs[{id}]; delete m.cbs[{id}];
2818 fn_(JSON.parse({result_str_literal}));
2819}})();"
2820 );
2821 let _ = session
2822 .send(
2823 "Runtime.evaluate",
2824 json!({ "expression": expr, "awaitPromise": false }),
2825 )
2826 .await;
2827 });
2828 }
2829 Ok(_) => {}
2830 Err(broadcast::error::RecvError::Closed) => break,
2831 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2832 }
2833 }
2834}
2835
2836fn parse_console(params: &Value) -> ConsoleMessage { let text = params
2837 .get("args")
2838 .and_then(|a| a.as_array())
2839 .map(|args| {
2840 args.iter()
2841 .filter_map(|a| {
2842 a.get("value")
2843 .and_then(|v| v.as_str())
2844 .or_else(|| a.get("description").and_then(|v| v.as_str()))
2845 .map(String::from)
2846 })
2847 .collect::<Vec<_>>()
2848 .join(" ")
2849 })
2850 .unwrap_or_default();
2851 let kind = params
2852 .get("type")
2853 .and_then(|v| v.as_str())
2854 .unwrap_or("log")
2855 .to_string();
2856 let location = params
2858 .get("stackTrace")
2859 .and_then(|s| s.get("callFrames"))
2860 .and_then(|f| f.as_array())
2861 .and_then(|frames| frames.first())
2862 .map(|frame| {
2863 use crate::types::ConsoleMessageLocation;
2864 ConsoleMessageLocation {
2865 url: frame
2866 .get("url")
2867 .and_then(|v| v.as_str())
2868 .map(String::from),
2869 line_number: frame.get("lineNumber").and_then(|v| v.as_i64()),
2870 column_number: frame.get("columnNumber").and_then(|v| v.as_i64()),
2871 }
2872 });
2873 ConsoleMessage {
2874 text,
2875 r#type: kind,
2876 location,
2877 }
2878}
2879
2880async fn context_tracker(
2889 mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
2890 session: Arc<CdpSession>,
2891 ctx_cell: Arc<Mutex<Option<i64>>>,
2892 frame_ctx_cell: Arc<Mutex<HashMap<String, i64>>>,
2893 main_frame_id: Arc<Mutex<Option<String>>>,
2894) {
2895 loop {
2896 match rx.recv().await {
2897 Ok(ev) => match ev.method.as_str() {
2898 "Runtime.executionContextCreated" => {
2899 let context = match ev.params.get("context") {
2900 Some(c) => c,
2901 None => continue,
2902 };
2903 let aux = context.get("auxData");
2904 let is_default = aux
2905 .and_then(|a| a.get("type"))
2906 .and_then(|t| t.as_str())
2907 == Some("default");
2908 let id = context.get("id").and_then(|i| i.as_i64());
2909 let Some(id) = id else { continue };
2910
2911 let frame_id = aux
2912 .and_then(|a| a.get("frameId"))
2913 .and_then(|v| v.as_str())
2914 .map(String::from);
2915
2916 if is_default {
2917 if let Some(fid) = &frame_id {
2922 frame_ctx_cell.lock().insert(fid.clone(), id);
2923 }
2924
2925 let main_id = main_frame_id.lock().clone();
2934 let is_main_frame = match (&main_id, &frame_id) {
2935 (Some(m), Some(f)) => m == f,
2936 (None, _) => ctx_cell.lock().is_none(),
2937 (Some(_), None) => ctx_cell.lock().is_none(),
2940 };
2941 if is_main_frame {
2942 *ctx_cell.lock() = Some(id);
2943 }
2944 }
2945
2946 let s = Arc::clone(&session);
2948 tokio::spawn(async move {
2949 let _ = s
2950 .send(
2951 "Runtime.evaluate",
2952 json!({
2953 "expression": selectors::INJECTED_SCRIPT,
2954 "contextId": id,
2955 }),
2956 )
2957 .await;
2958 });
2959 }
2960 "Runtime.executionContextsCleared" => {
2961 *ctx_cell.lock() = None;
2962 frame_ctx_cell.lock().clear();
2963 }
2964 "Runtime.executionContextDestroyed" => {
2965 let id = ev
2966 .params
2967 .get("executionContextId")
2968 .and_then(|v| v.as_i64());
2969 {
2970 let mut cell = ctx_cell.lock();
2971 if id.is_some() && *cell == id {
2972 *cell = None;
2973 }
2974 }
2975 if let Some(id) = id {
2976 let mut frames = frame_ctx_cell.lock();
2977 frames.retain(|_, v| *v != id);
2980 }
2981 }
2982 _ => {}
2983 },
2984 Err(broadcast::error::RecvError::Closed) => break,
2985 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2986 }
2987 }
2988}
2989
2990#[derive(Debug, Clone)]
2994pub struct FrameEvent {
2995 pub id: String,
2997 pub parent_id: Option<String>,
2999 pub url: String,
3001}
3002
3003impl FrameEvent {
3004 pub(crate) fn from_attached(params: &Value) -> Option<Self> {
3006 let frame = params.get("frame").unwrap_or(params);
3007 let id = frame.get("id").and_then(|v| v.as_str())?.to_string();
3008 let parent_id = frame
3009 .get("parentFrameId")
3010 .and_then(|v| v.as_str())
3011 .map(String::from);
3012 Some(Self {
3013 id,
3014 parent_id,
3015 url: String::new(),
3016 })
3017 }
3018
3019 pub(crate) fn from_detached(params: &Value) -> Option<Self> {
3021 let id = params
3023 .get("frameId")
3024 .or_else(|| params.get("frame"))
3025 .and_then(|v| {
3026 v.as_str()
3027 .map(String::from)
3028 .or_else(|| v.get("id").and_then(|i| i.as_str()).map(String::from))
3029 })?;
3030 let parent_id = params
3031 .get("frame")
3032 .and_then(|f| f.get("parentFrameId"))
3033 .and_then(|v| v.as_str())
3034 .map(String::from);
3035 Some(Self {
3036 id,
3037 parent_id,
3038 url: String::new(),
3039 })
3040 }
3041
3042 pub(crate) fn from_navigated(params: &Value) -> Option<Self> {
3044 let frame = params.get("frame")?;
3045 let id = frame.get("id").and_then(|v| v.as_str())?.to_string();
3046 let url = frame
3047 .get("url")
3048 .and_then(|v| v.as_str())
3049 .unwrap_or("")
3050 .to_string();
3051 let parent_id = frame
3052 .get("parentId")
3053 .and_then(|v| v.as_str())
3054 .map(String::from);
3055 Some(Self {
3056 id,
3057 parent_id,
3058 url,
3059 })
3060 }
3061}
3062
3063pub struct Dialog {
3065 message: String,
3066 kind: String,
3067 session: Arc<CdpSession>,
3068}
3069
3070impl Dialog {
3071 pub(crate) fn from_event(params: &Value, session: &Arc<CdpSession>) -> Self {
3072 Self {
3073 message: params
3074 .get("message")
3075 .and_then(|v| v.as_str())
3076 .unwrap_or("")
3077 .to_string(),
3078 kind: params
3079 .get("type")
3080 .and_then(|v| v.as_str())
3081 .unwrap_or("alert")
3082 .to_string(),
3083 session: Arc::clone(session),
3084 }
3085 }
3086
3087 pub fn message(&self) -> &str {
3088 &self.message
3089 }
3090
3091 pub fn kind(&self) -> &str {
3092 &self.kind
3093 }
3094
3095 pub async fn accept(&self, prompt_text: Option<&str>) -> Result<()> {
3096 let mut p = json!({ "accept": true });
3097 if let Some(t) = prompt_text {
3098 p["promptText"] = json!(t);
3099 }
3100 self.session
3101 .send("Page.handleJavaScriptDialog", p)
3102 .await
3103 .map(|_| ())
3104 }
3105
3106 pub async fn dismiss(&self) -> Result<()> {
3107 self.session
3108 .send("Page.handleJavaScriptDialog", json!({ "accept": false }))
3109 .await
3110 .map(|_| ())
3111 }
3112}