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;
45
46#[derive(Clone)]
48pub struct Page {
49 inner: Arc<PageInner>,
50}
51
52type CloseHandler =
53 Arc<dyn Fn() -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
54
55type ExposedBindingHandler =
57 Arc<dyn Fn(Vec<Value>) -> std::pin::Pin<Box<dyn Future<Output = Value> + Send>> + Send + Sync>;
58
59struct PageInner {
60 browser: Browser,
61 session: Arc<CdpSession>,
62 target_id: String,
63 main_world_ctx: Arc<Mutex<Option<i64>>>,
64 frame_contexts: Arc<Mutex<HashMap<String, i64>>>,
69 default_timeout_ms: AtomicU64,
70 default_navigation_timeout_ms: AtomicU64,
71 viewport: Mutex<Option<Viewport>>,
72 mouse_pos: Arc<Mutex<(f64, f64)>>,
73 network_store: Arc<NetworkStore>,
74 on_request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
75 on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
76 on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
77 frames: Arc<Mutex<HashMap<String, FrameData>>>,
78 main_frame_id: Arc<Mutex<Option<String>>>,
79 route_handlers: Arc<Mutex<Vec<RouteEntry>>>,
80 route_started: AtomicBool,
81 on_close_handlers: Mutex<Vec<CloseHandler>>,
82 closed: Mutex<bool>,
83 download_dir: Mutex<Option<Arc<tempfile::TempDir>>>,
86 download_path: Mutex<Option<PathBuf>>,
87 download_states: Arc<Mutex<HashMap<String, DownloadStateCell>>>,
88 download_started: AtomicBool,
89 filechooser_started: AtomicBool,
91 worker_started: AtomicBool,
93 touch_emulation_started: Arc<AtomicBool>,
96 web_socket_registry: Arc<WebSocketRegistry>,
100 video_path: Mutex<Option<PathBuf>>,
103}
104
105#[derive(Clone)]
107pub(crate) struct FrameData {
108 pub url: String,
109 pub name: String,
110 pub parent_id: Option<String>,
111 pub detached: bool,
112}
113
114impl Page {
115 #[allow(clippy::too_many_arguments)]
118 pub(crate) async fn attach(
119 browser: Browser,
120 session_id: String,
121 target_id: String,
122 init_scripts: &[String],
123 default_timeout_ms: u64,
124 extra_headers: Option<&Headers>,
125 user_agent: Option<&str>,
126 viewport: Option<Viewport>,
127 ) -> Result<Page> {
128 let session = Arc::new(CdpSession::target(
129 browser.connection().clone(),
130 session_id,
131 ));
132 session.set_default_timeout_ms(default_timeout_ms);
133
134 let main_world_ctx: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
135 let frame_contexts: Arc<Mutex<HashMap<String, i64>>> =
136 Arc::new(Mutex::new(HashMap::new()));
137 let frames_store: Arc<Mutex<HashMap<String, FrameData>>> =
141 Arc::new(Mutex::new(HashMap::new()));
142 let main_frame_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
143 let ctx_rx = session.subscribe();
144 {
147 let session_for_task = Arc::clone(&session);
148 let ctx_cell = Arc::clone(&main_world_ctx);
149 let frame_ctx_cell = Arc::clone(&frame_contexts);
150 let main_id_cell = Arc::clone(&main_frame_id);
151 tokio::spawn(async move {
152 context_tracker(
153 ctx_rx,
154 session_for_task,
155 ctx_cell,
156 frame_ctx_cell,
157 main_id_cell,
158 )
159 .await;
160 });
161 }
162
163 let network_store = NetworkStore::new();
164 let on_request_handlers: Arc<Mutex<Vec<RequestHandler>>> = Arc::new(Mutex::new(Vec::new()));
166 let on_response_handlers: Arc<Mutex<Vec<ResponseHandler>>> =
167 Arc::new(Mutex::new(Vec::new()));
168 let on_requestfailed_handlers: Arc<Mutex<Vec<RequestHandler>>> =
169 Arc::new(Mutex::new(Vec::new()));
170
171 {
173 let net_rx = session.subscribe();
174 let session_for_task = Arc::clone(&session);
175 let store_for_task = Arc::clone(&network_store);
176 let on_request = Arc::clone(&on_request_handlers);
177 let on_response = Arc::clone(&on_response_handlers);
178 let on_failed = Arc::clone(&on_requestfailed_handlers);
179 tokio::spawn(async move {
180 network_tracker(
181 net_rx,
182 session_for_task,
183 store_for_task,
184 on_request,
185 on_response,
186 on_failed,
187 )
188 .await;
189 });
190 }
191
192 let page = Page {
193 inner: Arc::new(PageInner {
194 browser,
195 session: Arc::clone(&session),
196 target_id,
197 main_world_ctx: Arc::clone(&main_world_ctx),
198 frame_contexts: Arc::clone(&frame_contexts),
199 default_timeout_ms: AtomicU64::new(default_timeout_ms),
200 default_navigation_timeout_ms: AtomicU64::new(default_timeout_ms),
201 viewport: Mutex::new(viewport),
202 mouse_pos: Arc::new(Mutex::new((0.0, 0.0))),
203 network_store,
204 on_request_handlers,
205 on_response_handlers,
206 on_requestfailed_handlers,
207 frames: Arc::clone(&frames_store),
208 main_frame_id: Arc::clone(&main_frame_id),
209 route_handlers: Arc::new(Mutex::new(Vec::new())),
210 route_started: AtomicBool::new(false),
211 on_close_handlers: Mutex::new(Vec::new()),
212 closed: Mutex::new(false),
213 download_dir: Mutex::new(None),
214 download_path: Mutex::new(None),
215 download_states: Arc::new(Mutex::new(HashMap::new())),
216 download_started: AtomicBool::new(false),
217 filechooser_started: AtomicBool::new(false),
218 worker_started: AtomicBool::new(false),
219 touch_emulation_started: Arc::new(AtomicBool::new(false)),
220 web_socket_registry: Arc::new(WebSocketRegistry::new()),
221 video_path: Mutex::new(None),
222 }),
223 };
224
225 page.inner.web_socket_registry.attach(&page.inner.session);
228
229 {
231 let rx = session.subscribe();
232 let frames_store = Arc::clone(&page.inner.frames);
233 let main_id = Arc::clone(&page.inner.main_frame_id);
234 tokio::spawn(async move {
235 frame_tracker(rx, frames_store, main_id).await;
236 });
237 }
238 let _ = page.refresh_frame_tree().await;
240
241 let _ = session.send("Page.enable", json!({})).await;
244 let _ = session.send("Runtime.enable", json!({})).await;
245 let _ = session.send("Network.enable", json!({})).await;
246 let _ = session.send("Log.enable", json!({})).await;
247
248 let mut source = String::from(selectors::INJECTED_SCRIPT);
250 for s in init_scripts {
251 source.push_str("\n");
252 source.push_str(s);
253 }
254 let _ = session
255 .send(
256 "Page.addScriptToEvaluateOnNewDocument",
257 json!({ "source": source }),
258 )
259 .await;
260
261 page.ensure_engine_in_current_context().await;
263
264 if let Some(headers) = extra_headers {
265 let _ = page.set_extra_http_headers(headers.clone()).await;
266 }
267 if let Some(ua) = user_agent {
268 let _ = session
269 .send("Emulation.setUserAgentOverride", json!({ "userAgent": ua }))
270 .await;
271 }
272 if let Some(vp) = page.inner.viewport.lock().as_ref().copied() {
273 let _ = page.set_viewport_size(vp).await;
274 }
275
276 Ok(page)
277 }
278
279 pub(crate) fn session(&self) -> &CdpSession {
282 &self.inner.session
283 }
284
285 pub(crate) fn session_arc(&self) -> Arc<CdpSession> {
288 Arc::clone(&self.inner.session)
289 }
290
291 pub(crate) fn context_id(&self) -> Option<i64> {
292 self.inner.main_world_ctx.lock().as_ref().copied()
293 }
294
295 pub(crate) async fn ctx(&self) -> Option<i64> {
307 None
308 }
309
310 pub(crate) fn context_for_frame(&self, frame_id: &str) -> Option<i64> {
317 if self
319 .inner
320 .main_frame_id
321 .lock()
322 .as_deref()
323 .map(|m| m == frame_id)
324 .unwrap_or(false)
325 {
326 return None;
327 }
328 self.inner
329 .frame_contexts
330 .lock()
331 .get(frame_id)
332 .copied()
333 .or(None)
335 }
336
337 pub(crate) async fn ctx_for_frame(&self, frame_id: &str) -> Option<i64> {
342 if self
344 .inner
345 .main_frame_id
346 .lock()
347 .as_deref()
348 .map(|m| m == frame_id)
349 .unwrap_or(false)
350 {
351 return None;
352 }
353 let deadline = tokio::time::Instant::now() + Duration::from_millis(2000);
354 loop {
355 if let Some(id) = self.inner.frame_contexts.lock().get(frame_id).copied() {
356 return Some(id);
357 }
358 if tokio::time::Instant::now() >= deadline {
359 return None;
362 }
363 tokio::time::sleep(Duration::from_millis(20)).await;
364 }
365 }
366
367 pub fn browser(&self) -> Browser {
368 self.inner.browser.clone()
369 }
370
371 pub fn target_id(&self) -> &str {
372 &self.inner.target_id
373 }
374
375 pub fn is_closed(&self) -> bool {
376 *self.inner.closed.lock()
377 }
378
379 pub fn set_default_timeout(&self, ms: u64) {
384 self.inner.default_timeout_ms.store(ms, Ordering::Relaxed);
385 self.inner.session.set_default_timeout_ms(ms);
386 }
387
388 pub(crate) fn default_timeout(&self) -> Duration {
389 Duration::from_millis(self.inner.default_timeout_ms.load(Ordering::Relaxed))
390 }
391
392 pub(crate) fn default_navigation_timeout(&self) -> Duration {
393 Duration::from_millis(self.inner.default_navigation_timeout_ms.load(Ordering::Relaxed))
394 }
395
396 async fn ensure_engine_in_current_context(&self) {
397 let _ = selectors::eval_context(
399 &self.inner.session,
400 self.ctx().await,
401 "(arg) => { void arg; }",
402 Value::Null,
403 )
404 .await;
405 }
406
407 pub async fn goto(
411 &self,
412 url: &str,
413 opts: Option<GotoOptions>,
414 ) -> Result<Option<Response>> {
415 let opts = opts.unwrap_or_default();
416 let wait = opts.wait_until_or_default();
417
418 let mut rx = self.inner.session.subscribe();
420 let mut params = json!({ "url": url });
421 if let Some(referer) = &opts.referer {
422 params["referer"] = json!(referer);
423 }
424 let nav = self.inner.session.send("Page.navigate", params).await?;
425 if let Some(err) = nav.get("errorText").and_then(|v| v.as_str()) {
426 return Err(Error::ProtocolError(format!("navigation to {url} failed: {err}")));
427 }
428 let loader_id = nav.get("loaderId").and_then(|v| v.as_str()).map(String::from);
429
430 let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
431 self.wait_lifecycle(&mut rx, loader_id.as_deref(), wait, timeout, url)
432 .await?;
433
434 let response = match &loader_id {
436 Some(lid) => self.wait_for_nav_response(lid).await,
437 None => None,
438 };
439 Ok(response)
440 }
441
442 async fn wait_for_nav_response(&self, loader_id: &str) -> Option<Response> {
444 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
445 loop {
446 if let Some(r) = self.inner.network_store.response_for_loader(loader_id) {
447 return Some(r);
448 }
449 if tokio::time::Instant::now() >= deadline {
450 return None;
451 }
452 tokio::time::sleep(Duration::from_millis(50)).await;
453 }
454 }
455
456 async fn wait_network_idle(&self, deadline: tokio::time::Instant) {
458 let window = Duration::from_millis(500);
459 loop {
460 if self.inner.network_store.inflight() == 0 {
461 let settled_at = tokio::time::Instant::now();
462 loop {
463 if self.inner.network_store.inflight() != 0 {
464 break; }
466 if tokio::time::Instant::now().duration_since(settled_at) >= window {
467 return; }
469 if tokio::time::Instant::now() >= deadline {
470 return; }
472 tokio::time::sleep(Duration::from_millis(50)).await;
473 }
474 }
475 if tokio::time::Instant::now() >= deadline {
476 return;
477 }
478 tokio::time::sleep(Duration::from_millis(50)).await;
479 }
480 }
481
482 async fn wait_lifecycle(
483 &self,
484 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
485 loader_id: Option<&str>,
486 wait: WaitUntil,
487 timeout: Duration,
488 url: &str,
489 ) -> Result<()> {
490 if matches!(wait, WaitUntil::Commit) {
491 return Ok(()); }
493 let target_name = match wait {
494 WaitUntil::Load => "load",
495 WaitUntil::DomContentLoaded => "DOMContentLoaded",
496 WaitUntil::NetworkIdle => "load", WaitUntil::Commit => "load",
498 };
499
500 let deadline = tokio::time::Instant::now() + timeout;
501 loop {
502 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
503 match tokio::time::timeout(remaining, rx.recv()).await {
504 Err(_) => {
505 return Err(Error::NavigationTimeout {
506 url: url.to_string(),
507 duration_ms: timeout.as_millis() as u64,
508 })
509 }
510 Ok(Err(broadcast::error::RecvError::Closed)) => {
511 return Err(Error::ChannelClosed);
512 }
513 Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
514 Ok(Ok(ev)) => {
515 let matched = match ev.method.as_str() {
520 "Page.lifecycleEvent" => {
521 let same_loader = loader_id
522 .map(|l| {
523 ev.params.get("loaderId").and_then(|v| v.as_str()) == Some(l)
524 })
525 .unwrap_or(true);
526 let name = ev.params.get("name").and_then(|v| v.as_str()).unwrap_or("");
527 same_loader && name == target_name
528 }
529 "Page.loadEventFired" => target_name == "load",
530 "Page.domContentEventFired" => target_name == "DOMContentLoaded",
531 _ => false,
532 };
533 if matched {
534 if matches!(wait, WaitUntil::NetworkIdle) {
535 self.wait_network_idle(deadline).await;
538 }
539 return Ok(());
540 }
541 }
542 }
543 }
544 }
545
546 pub async fn reload(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
548 let opts = opts.unwrap_or_default();
549 let wait = opts.wait_until_or_default();
550 let mut rx = self.inner.session.subscribe();
551 let _ = self.inner.session.send("Page.reload", json!({})).await?;
552 let timeout = opts.timeout.unwrap_or_else(|| self.default_timeout());
553 self.wait_lifecycle(&mut rx, None, wait, timeout, "reload").await?;
554 Ok(None)
555 }
556
557 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
559 let state = state.unwrap_or_default();
560 let mut rx = self.inner.session.subscribe();
561 self.wait_lifecycle(&mut rx, None, state, self.default_timeout(), "wait_for_load_state")
562 .await
563 }
564
565 pub async fn go_back(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
567 self.history_nav("history.back()", opts).await
568 }
569
570 pub async fn go_forward(&self, opts: Option<GotoOptions>) -> Result<Option<Response>> {
572 self.history_nav("history.forward()", opts).await
573 }
574
575 async fn history_nav(
576 &self,
577 expr: &str,
578 opts: Option<GotoOptions>,
579 ) -> Result<Option<Response>> {
580 let opts = opts.unwrap_or_default();
581 let navigated: bool = self.evaluate(expr).await.unwrap_or(false);
583 if navigated {
584 let wait = opts.wait_until_or_default();
585 let mut rx = self.inner.session.subscribe();
586 let timeout = opts
587 .timeout
588 .unwrap_or_else(|| self.default_navigation_timeout());
589 let _ = self
590 .wait_lifecycle(&mut rx, None, wait, timeout, "history navigation")
591 .await;
592 }
593 Ok(None)
594 }
595
596 pub async fn wait_for_url(&self, url: &str, opts: Option<GotoOptions>) -> Result<()> {
599 let opts = opts.unwrap_or_default();
600 let timeout = opts
601 .timeout
602 .unwrap_or_else(|| self.default_navigation_timeout());
603 let deadline = tokio::time::Instant::now() + timeout;
604 loop {
605 let cur = self.url().await.unwrap_or_default();
606 if glob_matches(url, &cur) {
607 break;
608 }
609 if tokio::time::Instant::now() >= deadline {
610 return Err(Error::Timeout(format!(
611 "wait_for_url '{url}' timed out after {}ms (last: '{cur}')",
612 timeout.as_millis()
613 )));
614 }
615 tokio::time::sleep(Duration::from_millis(100)).await;
616 }
617 if let Some(state) = opts.wait_until {
618 self.wait_for_load_state(Some(state)).await?;
619 }
620 Ok(())
621 }
622
623 pub fn set_default_navigation_timeout(&self, ms: u64) {
625 self.inner
626 .default_navigation_timeout_ms
627 .store(ms, Ordering::Relaxed);
628 }
629
630 pub async fn emulate_media(&self, opts: Option<EmulateMediaOptions>) -> Result<()> {
632 use crate::options::{ColorScheme, Media, ReducedMotion};
633 let opts = opts.unwrap_or_default();
634 let mut params = json!({});
635 if let Some(m) = opts.media {
636 params["media"] = json!(match m {
637 Media::Screen => "screen",
638 Media::Print => "print",
639 });
640 }
641 let mut features = Vec::new();
642 if let Some(cs) = opts.color_scheme {
643 features.push(json!({
644 "name": "prefers-color-scheme",
645 "value": match cs {
646 ColorScheme::Light => "light",
647 ColorScheme::Dark => "dark",
648 ColorScheme::NoPreference => "no-preference",
649 }
650 }));
651 }
652 if let Some(rm) = opts.reduced_motion {
653 features.push(json!({
654 "name": "prefers-reduced-motion",
655 "value": match rm {
656 ReducedMotion::Reduce => "reduce",
657 ReducedMotion::NoPreference => "no-preference",
658 }
659 }));
660 }
661 if !features.is_empty() {
662 params["features"] = json!(features);
663 }
664 self.inner
665 .session
666 .send("Emulation.setEmulatedMedia", params)
667 .await
668 .map(|_: Value| ())
669 }
670
671 pub async fn set_offline(&self, offline: bool) -> Result<()> {
673 let params = if offline {
674 json!({ "offline": true, "latency": 0, "downloadThroughput": 0, "uploadThroughput": 0 })
675 } else {
676 json!({ "offline": false, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1 })
677 };
678 self.inner
679 .session
680 .send("Network.emulateNetworkConditions", params)
681 .await
682 .map(|_: Value| ())
683 }
684
685 pub async fn drag_and_drop(
687 &self,
688 source: &str,
689 target: &str,
690 options: Option<DragToOptions>,
691 ) -> Result<()> {
692 let src = self.locator(source);
693 let tgt = self.locator(target);
694 src.drag_to(&tgt, options).await
695 }
696
697 pub async fn url(&self) -> Result<String> {
700 self.evaluate::<String>("location.href").await
701 }
702
703 pub async fn title(&self) -> Result<String> {
704 self.evaluate::<String>("document.title").await
705 }
706
707 pub async fn content(&self) -> Result<String> {
708 self.evaluate::<String>("document.documentElement.outerHTML").await
709 }
710
711 pub async fn set_content(&self, html: &str) -> Result<()> {
712 let _ = selectors::eval_context(
713 &self.inner.session,
714 self.ctx().await,
715 "(html) => { document.open(); document.write(html); document.close(); }",
716 json!(html),
717 )
718 .await?;
719 Ok(())
720 }
721
722 pub async fn evaluate<R: DeserializeOwned>(&self, expression: &str) -> Result<R> {
726 let function = format!("(arg) => {{ return ({expression}); }}");
727 let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, Value::Null)
728 .await?;
729 serde_json::from_value::<R>(v).map_err(Error::from)
730 }
731
732 pub async fn evaluate_handle(&self, expression: &str) -> Result<JSHandle> {
742 let function = format!("(arg) => {{ return ({expression}); }}");
743 let object_id = selectors::eval_context_handle(
744 &self.inner.session,
745 self.ctx().await,
746 &function,
747 Value::Null,
748 )
749 .await?
750 .ok_or_else(|| {
751 Error::ProtocolError(
752 "evaluate_handle returned no remote object (null/undefined result)".into(),
753 )
754 })?;
755 Ok(JSHandle::with_context(
756 Arc::clone(&self.inner.session),
757 object_id,
758 self.context_id(),
759 ))
760 }
761
762 pub async fn evaluate_with_arg<R: DeserializeOwned, T: serde::Serialize>(
764 &self,
765 expression: &str,
766 arg: &T,
767 ) -> Result<R> {
768 let function = format!("(arg) => {{ return ({expression}); }}");
769 let arg_val = serde_json::to_value(arg)?;
770 let v = selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val)
771 .await?;
772 serde_json::from_value::<R>(v).map_err(Error::from)
773 }
774
775 pub async fn wait_for_function<R: DeserializeOwned>(
783 &self,
784 expression: &str,
785 arg: Option<Value>,
786 options: Option<WaitForFunctionOptions>,
787 ) -> Result<R> {
788 let opts = options.unwrap_or_default();
789 let timeout = Duration::from_millis(opts.timeout.unwrap_or_else(|| self.default_timeout().as_millis() as f64) as u64);
790 let poll = Duration::from_millis(opts.polling_interval.unwrap_or(100.0) as u64);
791 let function = format!("(arg) => {{ return ({expression}); }}");
792 let arg_val = arg.unwrap_or(Value::Null);
793
794 let deadline = tokio::time::Instant::now() + timeout;
795 loop {
796 let v =
797 selectors::eval_context(&self.inner.session, self.ctx().await, &function, arg_val.clone())
798 .await?;
799 if is_truthy(&v) {
800 return serde_json::from_value::<R>(v).map_err(Error::from);
801 }
802 if tokio::time::Instant::now() >= deadline {
803 return Err(Error::Timeout(format!(
804 "wait_for_function timed out after {}ms (expression: {expression})",
805 timeout.as_millis()
806 )));
807 }
808 tokio::time::sleep(poll).await;
809 }
810 }
811
812 pub async fn eval_on_selector<R: DeserializeOwned>(
814 &self,
815 selector: &str,
816 expression: &str,
817 ) -> Result<R> {
818 let object_id = self
819 .resolve_strict(selector, Some(0))
820 .await?
821 .ok_or_else(|| Error::ElementNotFound(selector.to_string()))?;
822 let function = format!("(el) => {{ return ({expression}); }}");
823 let v = selectors::eval_object(&self.inner.session, &object_id, &function, Value::Null)
824 .await?;
825 self.release_object(&object_id).await;
826 serde_json::from_value::<R>(v).map_err(Error::from)
827 }
828
829 pub async fn eval_on_selector_all<R: DeserializeOwned>(
831 &self,
832 selector: &str,
833 expression: &str,
834 ) -> Result<Vec<R>> {
835 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
836 let mut out = Vec::with_capacity(n);
837 for i in 0..n {
838 if let Some(oid) = selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await? {
839 let function = format!("(el) => {{ return ({expression}); }}");
840 let v = selectors::eval_object(&self.inner.session, &oid, &function, Value::Null).await?;
841 self.release_object(&oid).await;
842 out.push(serde_json::from_value::<R>(v).map_err(Error::from)?);
843 }
844 }
845 Ok(out)
846 }
847
848 async fn release_object(&self, object_id: &str) {
849 let _ = self
850 .inner
851 .session
852 .send("Runtime.releaseObject", json!({ "objectId": object_id }))
853 .await;
854 }
855
856 pub async fn set_viewport_size(&self, viewport: Viewport) -> Result<()> {
859 *self.inner.viewport.lock() = Some(viewport);
860 self.inner
861 .session
862 .send(
863 "Emulation.setDeviceMetricsOverride",
864 json!({
865 "width": viewport.width,
866 "height": viewport.height,
867 "deviceScaleFactor": 1,
868 "mobile": false,
869 }),
870 )
871 .await?;
872 Ok(())
873 }
874
875 pub async fn screenshot(&self, opts: Option<ScreenshotOptions>) -> Result<Vec<u8>> {
876 let opts = opts.unwrap_or_default();
877 let format = match opts.r#type.unwrap_or_default() {
878 crate::types::ScreenshotType::Png => "png",
879 crate::types::ScreenshotType::Jpeg => "jpeg",
880 crate::types::ScreenshotType::Webp => "webp",
881 };
882 let mut params = json!({ "format": format });
883 if opts.full_page.unwrap_or(false) {
884 params["captureBeyondViewport"] = json!(true);
885 }
886 if opts.omit_background.unwrap_or(false) && format == "png" {
887 params["omitBackground"] = json!(true);
888 }
889 let resp = self
890 .inner
891 .session
892 .send("Page.captureScreenshot", params)
893 .await?;
894 let data = resp
895 .get("data")
896 .and_then(|v| v.as_str())
897 .ok_or_else(|| Error::ProtocolError("screenshot missing data".into()))?;
898 let bytes = base64::engine::general_purpose::STANDARD
899 .decode(data)
900 .map_err(|e| Error::ProtocolError(format!("base64 decode: {e}")))?;
901 if let Some(path) = &opts.path {
902 tokio::fs::write(path, &bytes).await?;
903 }
904 Ok(bytes)
905 }
906
907 pub async fn add_script_tag(
911 &self,
912 content: Option<&str>,
913 url: Option<&str>,
914 ) -> Result<()> {
915 let arg = json!({ "content": content, "url": url });
916 let _ = selectors::eval_context(
917 &self.inner.session,
918 self.ctx().await,
919 "(a) => { const s = document.createElement('script'); if (a.url) s.src = a.url; if (a.content) s.textContent = a.content; document.head.appendChild(s); }",
920 arg,
921 )
922 .await?;
923 Ok(())
924 }
925
926 pub async fn add_style_tag(&self, content: &str) -> Result<()> {
928 let _ = selectors::eval_context(
929 &self.inner.session,
930 self.ctx().await,
931 "(a) => { const s = document.createElement('style'); s.textContent = a; document.head.appendChild(s); }",
932 json!(content),
933 )
934 .await?;
935 Ok(())
936 }
937
938 pub async fn set_cache_disabled(&self, disabled: bool) -> Result<()> {
940 self.inner
941 .session
942 .send("Network.setCacheDisabled", json!({ "cacheDisabled": disabled }))
943 .await
944 .map(|_: Value| ())
945 }
946
947 pub async fn bring_to_front(&self) -> Result<()> {
949 self.inner
950 .session
951 .send("Page.bringToFront", json!({}))
952 .await
953 .map(|_: Value| ())
954 }
955
956 pub async fn set_geolocation(&self, geolocation: Option<(f64, f64)>) -> Result<()> {
959 let params = match geolocation {
960 Some((lat, lon)) => json!({ "latitude": lat, "longitude": lon }),
961 None => json!({}),
962 };
963 self.inner
964 .session
965 .send("Emulation.setGeolocationOverride", params)
966 .await
967 .map(|_: Value| ())
968 }
969
970 pub fn request(&self) -> APIRequestContext {
979 APIRequestContext::new(Headers::new())
980 }
981
982 pub async fn set_extra_http_headers(&self, headers: Headers) -> Result<()> {
983 let list: Vec<Value> = headers
985 .iter()
986 .map(|(k, v)| json!({ "name": k, "value": v }))
987 .collect();
988 self.inner
989 .session
990 .send("Network.setExtraHTTPHeaders", json!({ "headers": list }))
991 .await?;
992 Ok(())
993 }
994
995 pub async fn add_init_script(&self, script: &str) -> Result<()> {
996 let _ = self
997 .inner
998 .session
999 .send(
1000 "Page.addScriptToEvaluateOnNewDocument",
1001 json!({ "source": script }),
1002 )
1003 .await;
1004 let _ = selectors::eval_context(
1006 &self.inner.session,
1007 self.ctx().await,
1008 "(s) => { (0, eval)(s); }",
1009 json!(script),
1010 )
1011 .await;
1012 Ok(())
1013 }
1014
1015 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1042 where
1043 F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
1044 Fut: Future<Output = Value> + Send + 'static,
1045 {
1046 let handler: ExposedBindingHandler =
1049 Arc::new(move |args| Box::pin(callback(args)));
1050
1051 let binding_name = internal_binding_name(name);
1054 let _ = self
1055 .inner
1056 .session
1057 .send("Runtime.addBinding", json!({ "name": binding_name }))
1058 .await;
1059
1060 let wrapper = binding_wrapper_source(name);
1062 let _ = self
1063 .inner
1064 .session
1065 .send(
1066 "Page.addScriptToEvaluateOnNewDocument",
1067 json!({ "source": wrapper }),
1068 )
1069 .await;
1070 let _ = selectors::eval_context(
1072 &self.inner.session,
1073 self.ctx().await,
1074 "(s) => { (0, eval)(s); }",
1075 json!(wrapper),
1076 )
1077 .await;
1078
1079 let mut rx = self.inner.session.subscribe();
1081 let session = Arc::clone(&self.inner.session);
1082 let name_owned = name.to_string();
1083 tokio::spawn(async move {
1084 binding_listener(&mut rx, &session, &name_owned, &handler).await;
1085 });
1086
1087 Ok(())
1088 }
1089
1090 pub fn locator(&self, selector: impl Into<String>) -> Locator {
1093 Locator::new(self.clone(), selector.into(), true, None)
1094 }
1095
1096 pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator {
1105 FrameLocator::new(self.clone(), selector.into())
1106 }
1107
1108 pub fn get_by_text(&self, text: &str, _exact: bool) -> Locator {
1109 self.locator(format!("text={text}"))
1111 }
1112
1113 pub fn get_by_label(&self, text: &str) -> Locator {
1114 self.locator(format!("[aria-label=\"{}\"]", attr_escape(text)))
1115 }
1116
1117 pub fn get_by_placeholder(&self, text: &str) -> Locator {
1118 self.locator(format!("[placeholder=\"{}\"]", attr_escape(text)))
1119 }
1120
1121 pub fn get_by_alt_text(&self, text: &str) -> Locator {
1122 self.locator(format!("[alt=\"{}\"]", attr_escape(text)))
1123 }
1124
1125 pub fn get_by_title(&self, text: &str) -> Locator {
1126 self.locator(format!("[title=\"{}\"]", attr_escape(text)))
1127 }
1128
1129 pub fn get_by_test_id(&self, text: &str) -> Locator {
1130 self.locator(format!("[data-testid=\"{}\"]", attr_escape(text)))
1131 }
1132
1133 pub fn get_by_role(&self, role: AriaRole, opts: Option<crate::options::GetByRoleOptions>) -> Locator {
1134 let opts = opts.unwrap_or_default();
1135 let mut sel = format!("role={}", role.as_str());
1136 if let Some(name) = &opts.name {
1137 sel.push_str(&format!("[name=\"{name}\"]"));
1138 }
1139 if opts.exact == Some(true) {
1140 sel.push_str("[exact=\"true\"]");
1141 }
1142 Locator::new(self.clone(), sel, true, None)
1143 }
1144
1145 pub(crate) async fn resolve_strict(
1148 &self,
1149 selector: &str,
1150 forced_index: Option<usize>,
1151 ) -> Result<Option<String>> {
1152 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1153 if n == 0 {
1154 return Ok(None);
1155 }
1156 let index = forced_index.unwrap_or_else(|| {
1157 0
1160 });
1161 selectors::element_at(&self.inner.session, self.ctx().await, selector, index).await
1162 }
1163
1164 pub async fn query_selector(&self, selector: &str) -> Result<Option<ElementHandle>> {
1166 let oid = self.resolve_strict(selector, Some(0)).await?;
1167 Ok(oid.map(|oid| ElementHandle::new(self.clone(), oid)))
1168 }
1169
1170 pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementHandle>> {
1172 let n = selectors::count(&self.inner.session, self.ctx().await, selector).await?;
1173 let mut out = Vec::with_capacity(n);
1174 for i in 0..n {
1175 if let Some(oid) =
1176 selectors::element_at(&self.inner.session, self.ctx().await, selector, i).await?
1177 {
1178 out.push(ElementHandle::new(self.clone(), oid));
1179 }
1180 }
1181 Ok(out)
1182 }
1183
1184 pub fn keyboard(&self) -> Keyboard {
1187 Keyboard::new(self.clone())
1188 }
1189
1190 pub fn mouse(&self) -> Mouse {
1191 Mouse::with_pos(self.clone(), Arc::clone(&self.inner.mouse_pos))
1192 }
1193
1194 pub fn touchscreen(&self) -> Touchscreen {
1195 Touchscreen::with_flag(self.clone(), Arc::clone(&self.inner.touch_emulation_started))
1196 }
1197
1198 pub fn video(&self) -> Video {
1206 let session = Arc::clone(&self.inner.session);
1207 match self.inner.video_path.lock().clone() {
1208 Some(path) => Video::with_path(session, path),
1209 None => Video::new(session),
1210 }
1211 }
1212
1213 pub fn accessibility(&self) -> Accessibility {
1216 Accessibility::new(self.target_session())
1217 }
1218
1219 pub fn coverage(&self) -> Coverage {
1222 Coverage::new(self.target_session())
1223 }
1224
1225 pub fn clock(&self) -> Clock {
1227 Clock::new(self.target_session())
1228 }
1229
1230 pub async fn web_storage(&self) -> WebStorage {
1238 let origin = self.security_origin().await;
1239 WebStorage::local_storage(self.target_session(), origin)
1240 }
1241
1242 pub async fn local_storage(&self) -> WebStorage {
1244 let origin = self.security_origin().await;
1245 WebStorage::local_storage(self.target_session(), origin)
1246 }
1247
1248 pub async fn session_storage(&self) -> WebStorage {
1250 let origin = self.security_origin().await;
1251 WebStorage::session_storage(self.target_session(), origin)
1252 }
1253
1254 fn target_session(&self) -> CdpSession {
1262 let conn = self.inner.session.connection().clone();
1263 let session_id = self
1264 .inner
1265 .session
1266 .session_id()
1267 .map(|s| s.to_string())
1268 .unwrap_or_default();
1269 let session = CdpSession::target(conn, session_id);
1270 session.set_default_timeout_ms(self.inner.default_timeout_ms.load(Ordering::Relaxed));
1271 session
1272 }
1273
1274 async fn security_origin(&self) -> String {
1277 let url = self.url().await.unwrap_or_default();
1278 derive_security_origin(&url)
1279 }
1280
1281 pub fn main_frame(&self) -> Frame {
1282 Frame::main(self.clone())
1283 }
1284
1285 pub async fn frames(&self) -> Result<Vec<Frame>> {
1287 self.refresh_frame_tree().await?;
1288 let ids: Vec<String> = self.inner.frames.lock().keys().cloned().collect();
1289 Ok(ids.into_iter().map(|id| Frame::new(self.clone(), id)).collect())
1290 }
1291
1292 pub(crate) async fn refresh_frame_tree(&self) -> Result<()> {
1294 let resp = self
1295 .inner
1296 .session
1297 .send("Page.getFrameTree", json!({}))
1298 .await?;
1299 if let Some(tree) = resp.get("frameTree") {
1300 let mut frames = self.inner.frames.lock();
1301 frames.clear();
1302 walk_frame_tree(tree, &mut frames);
1303 if let Some(id) = tree
1304 .get("frame")
1305 .and_then(|f| f.get("id"))
1306 .and_then(|v| v.as_str())
1307 {
1308 *self.inner.main_frame_id.lock() = Some(id.to_string());
1309 }
1310 }
1311 Ok(())
1312 }
1313
1314 pub(crate) fn main_frame_id(&self) -> Option<String> {
1315 self.inner.main_frame_id.lock().clone()
1316 }
1317
1318 pub(crate) fn frame_data(&self, frame_id: &str) -> Option<FrameData> {
1319 self.inner.frames.lock().get(frame_id).cloned()
1320 }
1321
1322 pub(crate) fn frame_ids_with_parent(&self, parent: &str) -> Vec<String> {
1323 self.inner
1324 .frames
1325 .lock()
1326 .iter()
1327 .filter(|(_, d)| d.parent_id.as_deref() == Some(parent))
1328 .map(|(k, _)| k.clone())
1329 .collect()
1330 }
1331
1332 pub fn on_console<F, Fut>(&self, handler: F)
1335 where
1336 F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
1337 Fut: std::future::Future<Output = ()> + Send + 'static,
1338 {
1339 let mut rx = self.inner.session.subscribe();
1340 tokio::spawn(async move {
1341 while let Ok(ev) = rx.recv().await {
1342 if ev.method == "Runtime.consoleAPICalled" {
1343 let msg = parse_console(&ev.params);
1344 handler(msg).await;
1345 }
1346 }
1347 });
1348 }
1349
1350 pub fn on_load<F, Fut>(&self, handler: F)
1353 where
1354 F: Fn(()) -> Fut + Send + Sync + 'static,
1355 Fut: std::future::Future<Output = ()> + Send + 'static,
1356 {
1357 let mut rx = self.inner.session.subscribe();
1358 tokio::spawn(async move {
1359 while let Ok(ev) = rx.recv().await {
1360 if ev.method == "Page.loadEventFired" {
1361 handler(()).await;
1362 }
1363 }
1364 });
1365 }
1366
1367 pub fn on_crash<F, Fut>(&self, handler: F)
1370 where
1371 F: Fn(()) -> Fut + Send + Sync + 'static,
1372 Fut: std::future::Future<Output = ()> + Send + 'static,
1373 {
1374 let mut rx = self.inner.session.subscribe();
1375 tokio::spawn(async move {
1376 while let Ok(ev) = rx.recv().await {
1377 if ev.method == "Inspector.targetCrashed" {
1378 handler(()).await;
1379 }
1380 }
1381 });
1382 }
1383
1384 pub fn on_frameattached<F, Fut>(&self, handler: F)
1387 where
1388 F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1389 Fut: std::future::Future<Output = ()> + Send + 'static,
1390 {
1391 let mut rx = self.inner.session.subscribe();
1392 tokio::spawn(async move {
1393 while let Ok(ev) = rx.recv().await {
1394 if ev.method == "Page.frameAttached" {
1395 if let Some(fe) = FrameEvent::from_attached(&ev.params) {
1396 handler(fe).await;
1397 }
1398 }
1399 }
1400 });
1401 }
1402
1403 pub fn on_framedetached<F, Fut>(&self, handler: F)
1406 where
1407 F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1408 Fut: std::future::Future<Output = ()> + Send + 'static,
1409 {
1410 let mut rx = self.inner.session.subscribe();
1411 tokio::spawn(async move {
1412 while let Ok(ev) = rx.recv().await {
1413 if ev.method == "Page.frameDetached" {
1414 if let Some(fe) = FrameEvent::from_detached(&ev.params) {
1415 handler(fe).await;
1416 }
1417 }
1418 }
1419 });
1420 }
1421
1422 pub fn on_framenavigated<F, Fut>(&self, handler: F)
1425 where
1426 F: Fn(FrameEvent) -> Fut + Send + Sync + 'static,
1427 Fut: std::future::Future<Output = ()> + Send + 'static,
1428 {
1429 let mut rx = self.inner.session.subscribe();
1430 tokio::spawn(async move {
1431 while let Ok(ev) = rx.recv().await {
1432 if ev.method == "Page.frameNavigated" {
1433 if let Some(fe) = FrameEvent::from_navigated(&ev.params) {
1434 handler(fe).await;
1435 }
1436 }
1437 }
1438 });
1439 }
1440
1441 pub fn on_dialog<F, Fut>(&self, handler: F)
1442 where
1443 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1444 Fut: std::future::Future<Output = ()> + Send + 'static,
1445 {
1446 let mut rx = self.inner.session.subscribe();
1447 let session = Arc::clone(&self.inner.session);
1448 tokio::spawn(async move {
1449 while let Ok(ev) = rx.recv().await {
1450 if ev.method == "Page.javascriptDialogOpening" {
1451 let dialog = Dialog::from_event(&ev.params, &session);
1452 handler(dialog).await;
1453 }
1454 }
1455 });
1456 }
1457
1458 pub fn on_request<F, Fut>(&self, handler: F)
1460 where
1461 F: Fn(Request) -> Fut + Send + Sync + 'static,
1462 Fut: Future<Output = ()> + Send + 'static,
1463 {
1464 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1465 self.inner.on_request_handlers.lock().push(h);
1466 }
1467
1468 pub fn on_response<F, Fut>(&self, handler: F)
1470 where
1471 F: Fn(Response) -> Fut + Send + Sync + 'static,
1472 Fut: Future<Output = ()> + Send + 'static,
1473 {
1474 let h: ResponseHandler = Arc::new(move |r| Box::pin(handler(r)));
1475 self.inner.on_response_handlers.lock().push(h);
1476 }
1477
1478 pub fn on_requestfailed<F, Fut>(&self, handler: F)
1480 where
1481 F: Fn(Request) -> Fut + Send + Sync + 'static,
1482 Fut: Future<Output = ()> + Send + 'static,
1483 {
1484 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1485 self.inner.on_requestfailed_handlers.lock().push(h);
1486 }
1487
1488 pub fn on_pageerror<F, Fut>(&self, handler: F)
1490 where
1491 F: Fn(String) -> Fut + Send + Sync + 'static,
1492 Fut: Future<Output = ()> + Send + 'static,
1493 {
1494 let mut rx = self.inner.session.subscribe();
1495 tokio::spawn(async move {
1496 while let Ok(ev) = rx.recv().await {
1497 if ev.method == "Runtime.exceptionThrown" {
1498 let msg = ev
1499 .params
1500 .get("exceptionDetails")
1501 .and_then(|d| d.get("exception"))
1502 .and_then(|e| e.get("description"))
1503 .and_then(|v| v.as_str())
1504 .unwrap_or("unknown error")
1505 .to_string();
1506 handler(msg).await;
1507 }
1508 }
1509 });
1510 }
1511
1512 pub fn on_websocket<F, Fut>(&self, handler: F)
1524 where
1525 F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1526 Fut: Future<Output = ()> + Send + 'static,
1527 {
1528 let mut rx = self.inner.web_socket_registry.on_created();
1529 let handler: Arc<dyn Fn(WebSocket) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync> =
1532 Arc::new(move |ws| Box::pin(handler(ws)));
1533 tokio::spawn(async move {
1534 while let Ok(ws) = rx.recv().await {
1535 let h = Arc::clone(&handler);
1536 tokio::spawn(async move {
1539 h(ws).await;
1540 });
1541 }
1542 });
1543 }
1544
1545 pub fn on_requestfinished<F, Fut>(&self, handler: F)
1547 where
1548 F: Fn(Request) -> Fut + Send + Sync + 'static,
1549 Fut: Future<Output = ()> + Send + 'static,
1550 {
1551 let mut rx = self.inner.session.subscribe();
1552 let store = Arc::clone(&self.inner.network_store);
1553 tokio::spawn(async move {
1554 while let Ok(ev) = rx.recv().await {
1555 if ev.method == "Network.loadingFinished" {
1556 if let Some(rid) = ev.params.get("requestId").and_then(|v| v.as_str()) {
1557 if let Some(req) = store.get_request(rid) {
1558 handler(req).await;
1559 }
1560 }
1561 }
1562 }
1563 });
1564 }
1565
1566 pub fn on_close<F, Fut>(&self, handler: F)
1568 where
1569 F: Fn() -> Fut + Send + Sync + 'static,
1570 Fut: Future<Output = ()> + Send + 'static,
1571 {
1572 let h: CloseHandler = Arc::new(move || Box::pin(handler()));
1573 self.inner.on_close_handlers.lock().push(h);
1574 }
1575
1576 pub fn on_download<F, Fut>(&self, handler: F)
1583 where
1584 F: Fn(Download) -> Fut + Send + Sync + 'static,
1585 Fut: Future<Output = ()> + Send + 'static,
1586 {
1587 if self
1589 .inner
1590 .download_started
1591 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1592 .is_ok()
1593 {
1594 let dir = match tempfile::TempDir::new() {
1595 Ok(d) => Arc::new(d),
1596 Err(e) => {
1597 tracing::error!("failed to create download temp dir: {e}");
1598 return;
1599 }
1600 };
1601 let dir_path = dir.path().to_path_buf();
1602 *self.inner.download_dir.lock() = Some(Arc::clone(&dir));
1603 *self.inner.download_path.lock() = Some(dir_path.clone());
1604
1605 let session_for_setup = Arc::clone(&self.inner.session);
1607 let setup_path = dir_path.clone();
1608 tokio::spawn(async move {
1609 let _ = session_for_setup
1610 .send(
1611 "Page.setDownloadBehavior",
1612 json!({ "behavior": "allow", "downloadPath": setup_path }),
1613 )
1614 .await;
1615 });
1616
1617 let mut rx = self.inner.session.subscribe();
1619 let session = Arc::clone(&self.inner.session);
1620 let states = Arc::clone(&self.inner.download_states);
1621 let download_path = dir_path.clone();
1622 let page = self.clone();
1625 let handler: Arc<dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync> =
1626 Arc::new(move |d| Box::pin(handler(d)));
1627 tokio::spawn(async move {
1628 download_listener(
1629 &mut rx,
1630 &session,
1631 &states,
1632 &download_path,
1633 page.clone(),
1634 &handler,
1635 )
1636 .await;
1637 });
1638 }
1639 }
1640
1641 pub async fn on_filechooser<F, Fut>(&self, handler: F)
1649 where
1650 F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
1651 Fut: Future<Output = ()> + Send + 'static,
1652 {
1653 if self
1655 .inner
1656 .filechooser_started
1657 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1658 .is_ok()
1659 {
1660 let mut rx = self.inner.session.subscribe();
1663 let _ = self
1666 .inner
1667 .session
1668 .send(
1669 "Page.setInterceptFileChooserDialog",
1670 json!({ "enabled": true }),
1671 )
1672 .await;
1673
1674 let page = self.clone();
1676 let handler: Arc<
1677 dyn Fn(FileChooser) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>>
1678 + Send
1679 + Sync,
1680 > = Arc::new(move |fc| Box::pin(handler(fc)));
1681 tokio::spawn(async move {
1682 while let Ok(ev) = rx.recv().await {
1683 if ev.method == "Page.fileChooserOpened" {
1684 let multiple = ev
1685 .params
1686 .get("mode")
1687 .and_then(|v| v.as_str())
1688 == Some("selectMultiple");
1689 let backend_node_id = ev
1690 .params
1691 .get("backendNodeId")
1692 .and_then(|v| v.as_i64());
1693 let fc = FileChooser::new(page.clone(), backend_node_id, multiple);
1694 let h = Arc::clone(&handler);
1695 tokio::spawn(async move {
1696 (h)(fc).await;
1697 });
1698 }
1699 }
1700 });
1701 }
1702 }
1703
1704 pub async fn on_worker<F, Fut>(&self, handler: F)
1715 where
1716 F: Fn(Worker) -> Fut + Send + Sync + 'static,
1717 Fut: Future<Output = ()> + Send + 'static,
1718 {
1719 if self
1721 .inner
1722 .worker_started
1723 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1724 .is_ok()
1725 {
1726 let mut rx = self.inner.session.subscribe();
1729 let _ = self
1730 .inner
1731 .session
1732 .send(
1733 "Target.setAutoAttach",
1734 json!({ "autoAttach": true, "waitForDebuggerOnStart": false, "flatten": true }),
1735 )
1736 .await;
1737
1738 let connection = self.inner.session.connection().clone();
1739 let handler: Arc<
1740 dyn Fn(Worker) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
1741 > = Arc::new(move |w| Box::pin(handler(w)));
1742 tokio::spawn(async move {
1743 while let Ok(ev) = rx.recv().await {
1744 if ev.method != "Target.attachedToTarget" {
1745 continue;
1746 }
1747 let session_id = ev
1749 .params
1750 .get("sessionId")
1751 .or_else(|| ev.params.get("session_id"))
1752 .and_then(|v| v.as_str());
1753 let target_info = match ev.params.get("targetInfo") {
1754 Some(t) => t,
1755 None => continue,
1756 };
1757 let target_type = target_info
1758 .get("type")
1759 .and_then(|v| v.as_str())
1760 .unwrap_or("");
1761 if !matches!(target_type, "worker" | "service_worker" | "shared_worker") {
1763 continue;
1764 }
1765 let (sid, url) = match (session_id, target_info.get("url").and_then(|v| v.as_str())) {
1766 (Some(sid), Some(url)) => (sid.to_string(), url.to_string()),
1767 _ => continue,
1768 };
1769 let worker = Worker::new(connection.clone(), sid, url);
1770 worker.enable_runtime().await;
1772 let h = Arc::clone(&handler);
1773 tokio::spawn(async move {
1774 (h)(worker).await;
1775 });
1776 }
1777 });
1778 }
1779 }
1780
1781 pub async fn close(&self) -> Result<()> {
1784 if *self.inner.closed.lock() {
1785 return Ok(());
1786 }
1787 *self.inner.closed.lock() = true;
1788 let handlers = std::mem::take(&mut *self.inner.on_close_handlers.lock());
1789 for h in handlers {
1790 tokio::spawn(async move { (h)().await; });
1791 }
1792 let _ = self
1793 .inner
1794 .browser
1795 .browser_session()
1796 .send("Target.closeTarget", json!({ "targetId": self.inner.target_id }))
1797 .await;
1798 Ok(())
1799 }
1800
1801 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1807 where
1808 F: Fn(Route) -> Fut + Send + Sync + 'static,
1809 Fut: Future<Output = ()> + Send + 'static,
1810 {
1811 let entry = RouteEntry {
1812 pattern: pattern.to_string(),
1813 handler: Arc::new(move |r| Box::pin(handler(r))),
1814 };
1815 self.inner.route_handlers.lock().push(entry);
1816 self.ensure_route_listener().await;
1817 self.refresh_fetch_patterns().await
1818 }
1819
1820 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1822 let mut handlers = self.inner.route_handlers.lock();
1823 if let Some(pos) = handlers.iter().position(|e| e.pattern == pattern) {
1824 handlers.remove(pos);
1825 }
1826 drop(handlers);
1827 self.refresh_fetch_patterns().await
1828 }
1829
1830 pub async fn unroute_all(&self) -> Result<()> {
1832 self.inner.route_handlers.lock().clear();
1833 let _ = self.inner.session.send("Fetch.disable", json!({})).await;
1834 Ok(())
1835 }
1836
1837 async fn ensure_route_listener(&self) {
1838 if self
1839 .inner
1840 .route_started
1841 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1842 .is_ok()
1843 {
1844 let rx = self.inner.session.subscribe();
1845 let session = Arc::clone(&self.inner.session);
1846 let store = Arc::clone(&self.inner.network_store);
1847 let handlers = Arc::clone(&self.inner.route_handlers);
1848 tokio::spawn(async move {
1849 route_listener(rx, session, store, handlers).await;
1850 });
1851 }
1852 }
1853
1854 async fn refresh_fetch_patterns(&self) -> Result<()> {
1855 let patterns: Vec<Value> = self
1856 .inner
1857 .route_handlers
1858 .lock()
1859 .iter()
1860 .map(|e| json!({ "urlPattern": e.pattern }))
1861 .collect();
1862 if patterns.is_empty() {
1863 let _ = self.inner.session.send("Fetch.disable", json!({})).await;
1864 } else {
1865 self.inner
1866 .session
1867 .send("Fetch.enable", json!({ "patterns": patterns }))
1868 .await?;
1869 }
1870 Ok(())
1871 }
1872
1873 pub async fn pdf(&self, opts: Option<crate::options::PdfOptions>) -> Result<Vec<u8>> {
1876 let opts = opts.unwrap_or_default();
1877 let mut params = json!({ "printBackground": opts.print_background.unwrap_or(true) });
1878 if let Some(fmt) = opts.format {
1879 let (w, h) = fmt.inches();
1880 params["paperWidth"] = json!(w);
1881 params["paperHeight"] = json!(h);
1882 }
1883 if let Some(l) = opts.landscape {
1884 params["landscape"] = json!(l);
1885 }
1886 if let Some(s) = opts.scale {
1887 params["scale"] = json!(s);
1888 }
1889 if let Some(p) = opts.prefer_css_page_size {
1890 params["preferCSSPageSize"] = json!(p);
1891 }
1892 if let Some(m) = opts.margin {
1893 if let Some(v) = m.top {
1894 params["marginTop"] = json!(v);
1895 }
1896 if let Some(v) = m.bottom {
1897 params["marginBottom"] = json!(v);
1898 }
1899 if let Some(v) = m.left {
1900 params["marginLeft"] = json!(v);
1901 }
1902 if let Some(v) = m.right {
1903 params["marginRight"] = json!(v);
1904 }
1905 }
1906 let resp = self.inner.session.send("Page.printToPDF", params).await?;
1907 let data = resp
1908 .get("data")
1909 .and_then(|v| v.as_str())
1910 .ok_or_else(|| Error::ProtocolError("pdf missing data".into()))?;
1911 Ok(base64::engine::general_purpose::STANDARD
1912 .decode(data)
1913 .map_err(|e| Error::ProtocolError(format!("pdf base64 decode: {e}")))?)
1914 }
1915
1916 pub async fn aria_snapshot(&self) -> Result<String> {
1923 let _ = self
1924 .inner
1925 .session
1926 .send("Accessibility.enable", json!({}))
1927 .await;
1928 let resp = self
1929 .inner
1930 .session
1931 .send("Accessibility.getFullAXTree", json!({}))
1932 .await?;
1933 let nodes = resp
1934 .get("nodes")
1935 .and_then(|v| v.as_array())
1936 .cloned()
1937 .unwrap_or_default();
1938 Ok(crate::aria_snapshot::serialize(&nodes, None))
1939 }
1940}
1941
1942fn attr_escape(s: &str) -> String {
1943 s.replace('\\', "\\\\").replace('"', "\\\"")
1944}
1945
1946fn derive_security_origin(url: &str) -> String {
1952 let after_scheme = match url.find("://") {
1954 Some(i) => &url[i + 3..],
1955 None => return String::new(),
1956 };
1957 let scheme = &url[..url.find("://").unwrap()];
1958 if matches!(scheme, "about" | "data" | "blob" | "javascript") {
1960 return String::new();
1961 }
1962 let end = after_scheme
1964 .find(|c: char| c == '/' || c == '?' || c == '#')
1965 .unwrap_or(after_scheme.len());
1966 let authority = &after_scheme[..end];
1967 if authority.is_empty() {
1968 return String::new();
1969 }
1970 format!("{scheme}://{authority}")
1971}
1972
1973fn is_truthy(v: &Value) -> bool {
1975 match v {
1976 Value::Null => false,
1977 Value::Bool(b) => *b,
1978 Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
1979 Value::String(s) => !s.is_empty(),
1980 _ => true,
1981 }
1982}
1983
1984fn glob_matches(pattern: &str, value: &str) -> bool {
1987 if pattern == value {
1988 return true;
1989 }
1990 if pattern.len() >= 2 && pattern.starts_with('*') && pattern.ends_with('*') {
1992 return value.contains(&pattern[1..pattern.len() - 1]);
1993 }
1994 if let Some(rest) = pattern.strip_prefix('*') {
1995 if value.ends_with(rest) {
1996 return true;
1997 }
1998 }
1999 if let Some(rest) = pattern.strip_suffix('*') {
2000 if value.starts_with(rest) {
2001 return true;
2002 }
2003 }
2004 false
2005}
2006
2007fn walk_frame_tree(node: &Value, frames: &mut HashMap<String, FrameData>) {
2009 let frame = match node.get("frame") {
2010 Some(f) => f,
2011 None => return,
2012 };
2013 let id = match frame.get("id").and_then(|v| v.as_str()) {
2014 Some(s) => s.to_string(),
2015 None => return,
2016 };
2017 let data = FrameData {
2018 url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2019 name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2020 parent_id: frame
2021 .get("parentId")
2022 .and_then(|v| v.as_str())
2023 .map(String::from),
2024 detached: false,
2025 };
2026 frames.insert(id, data);
2027 if let Some(children) = node.get("childFrames").and_then(|v| v.as_array()) {
2028 for child in children {
2029 walk_frame_tree(child, frames);
2030 }
2031 }
2032}
2033
2034async fn frame_tracker(
2036 mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
2037 frames: Arc<Mutex<HashMap<String, FrameData>>>,
2038 main_id: Arc<Mutex<Option<String>>>,
2039) {
2040 loop {
2041 match rx.recv().await {
2042 Ok(ev) => match ev.method.as_str() {
2043 "Page.frameNavigated" => {
2044 if let Some(frame) = ev.params.get("frame") {
2045 let id = match frame.get("id").and_then(|v| v.as_str()) {
2046 Some(s) => s.to_string(),
2047 None => continue,
2048 };
2049 let parent = frame
2050 .get("parentId")
2051 .and_then(|v| v.as_str())
2052 .map(String::from);
2053 let data = FrameData {
2054 url: frame.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2055 name: frame.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
2056 parent_id: parent.clone(),
2057 detached: false,
2058 };
2059 if parent.is_none() {
2060 *main_id.lock() = Some(id.clone());
2061 }
2062 frames.lock().insert(id, data);
2063 }
2064 }
2065 "Page.frameDetached" => {
2066 if let Some(id) = ev.params.get("frameId").and_then(|v| v.as_str()) {
2067 if let Some(d) = frames.lock().get_mut(id) {
2068 d.detached = true;
2069 }
2070 }
2071 }
2072 _ => {}
2073 },
2074 Err(broadcast::error::RecvError::Closed) => break,
2075 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2076 }
2077 }
2078}
2079
2080async fn download_listener(
2082 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
2083 _session: &Arc<CdpSession>,
2084 states: &Arc<Mutex<HashMap<String, DownloadStateCell>>>,
2085 download_path: &std::path::Path,
2086 page: Page,
2087 handler: &Arc<
2088 dyn Fn(Download) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
2089 >,
2090) {
2091 while let Ok(ev) = rx.recv().await {
2092 match ev.method.as_str() {
2093 "Page.downloadWillBegin" => {
2094 let guid = ev
2095 .params
2096 .get("guid")
2097 .and_then(|v| v.as_str())
2098 .unwrap_or("")
2099 .to_string();
2100 let url = ev
2101 .params
2102 .get("url")
2103 .and_then(|v| v.as_str())
2104 .unwrap_or("")
2105 .to_string();
2106 let suggested = ev
2107 .params
2108 .get("suggestedFilename")
2109 .and_then(|v| v.as_str())
2110 .unwrap_or("download")
2111 .to_string();
2112 let cell = DownloadStateCell::new();
2113 states.lock().insert(guid.clone(), cell.clone());
2114 let dl = Download::new(
2115 url,
2116 suggested,
2117 guid,
2118 cell,
2119 download_path.to_path_buf(),
2120 page.clone(),
2121 );
2122 let h = Arc::clone(handler);
2123 tokio::spawn(async move {
2124 (h)(dl).await;
2125 });
2126 }
2127 "Page.downloadProgress" => {
2128 let guid = match ev.params.get("guid").and_then(|v| v.as_str()) {
2129 Some(g) => g.to_string(),
2130 None => continue,
2131 };
2132 let state = ev
2133 .params
2134 .get("state")
2135 .and_then(|v| v.as_str())
2136 .unwrap_or("InProgress");
2137 let mapped = match state {
2138 "Completed" => DownloadState::Completed,
2139 "Canceled" => DownloadState::Canceled,
2140 _ => DownloadState::InProgress,
2141 };
2142 if let Some(cell) = states.lock().get(&guid) {
2143 *cell.state.lock() = mapped;
2144 }
2145 }
2146 _ => {}
2147 }
2148 }
2149}
2150
2151fn internal_binding_name(name: &str) -> String {
2155 format!("__pwcdpInvoke_{name}")
2156}
2157
2158fn binding_wrapper_source(name: &str) -> String {
2163 let name_json = serde_json::to_string(name).unwrap_or_else(|_| "\"\"".to_string());
2166 format!(
2167 r#"(function(){{
2168 var publicName = {name_json};
2169 var invokeName = "__pwcdpInvoke_" + publicName;
2170 var pending = (self.__pwcdpBindings = self.__pwcdpBindings || {{}});
2171 var map = pending[publicName] = pending[publicName] || {{ id: 0, cbs: {{}} }};
2172 self[publicName] = function(){{
2173 var args = Array.prototype.slice.call(arguments);
2174 return new Promise(function(resolve){{
2175 var id = ++map.id;
2176 map.cbs[id] = resolve;
2177 // The CDP binding (self[invokeName]) is installed by Runtime.addBinding;
2178 // if it is not present yet, surface an error so callers see a clear cause.
2179 if (typeof self[invokeName] !== "function") {{
2180 delete map.cbs[id];
2181 resolve({{ "__error": "binding not installed: " + invokeName }});
2182 return;
2183 }}
2184 try {{
2185 self[invokeName](JSON.stringify({{ id: id, args: args }}));
2186 }} catch (e) {{
2187 delete map.cbs[id];
2188 resolve({{ "__error": String((e && e.message) || e) }});
2189 }}
2190 }});
2191 }};
2192}})();
2193"#
2194 )
2195}
2196
2197async fn binding_listener(
2201 rx: &mut broadcast::Receiver<crate::cdp::CdpEvent>,
2202 session: &Arc<CdpSession>,
2203 name: &str,
2204 handler: &ExposedBindingHandler,
2205) {
2206 let binding_name = internal_binding_name(name);
2207 loop {
2208 match rx.recv().await {
2209 Ok(ev) if ev.method == "Runtime.bindingCalled" => {
2210 let fired_name = ev
2211 .params
2212 .get("name")
2213 .and_then(|v| v.as_str())
2214 .unwrap_or("");
2215 if fired_name != binding_name {
2216 continue;
2217 }
2218 let payload_str = match ev.params.get("payload").and_then(|v| v.as_str()) {
2219 Some(s) => s,
2220 None => continue,
2221 };
2222 let parsed: Value = match serde_json::from_str(payload_str) {
2223 Ok(v) => v,
2224 Err(_) => continue,
2225 };
2226 let id = match parsed.get("id").and_then(|v| v.as_i64()) {
2227 Some(i) => i,
2228 None => continue,
2229 };
2230 let args = parsed
2231 .get("args")
2232 .and_then(|v| v.as_array())
2233 .cloned()
2234 .unwrap_or_default();
2235
2236 let h = Arc::clone(handler);
2237 let session = Arc::clone(session);
2238 let name_owned = name.to_string();
2239 tokio::spawn(async move {
2242 let result = (h)(args).await;
2243 let result_json = serde_json::to_string(&result)
2244 .unwrap_or_else(|_| r#"{"__error":"serialize failed"}"#.to_string());
2245 let result_str_literal = serde_json::to_string(&result_json)
2248 .unwrap_or_else(|_| r#""{\"__error\":\"serialize failed\"}""#.to_string());
2249 let name_j = serde_json::to_string(&name_owned).unwrap_or_else(|_| "\"\"".into());
2252 let expr = format!(
2253 "(function(){{
2254 var m = (self.__pwcdpBindings && self.__pwcdpBindings[{name_j}]) || null;
2255 if (!m || !m.cbs || !m.cbs[{id}]) return;
2256 var fn_ = m.cbs[{id}]; delete m.cbs[{id}];
2257 fn_(JSON.parse({result_str_literal}));
2258}})();"
2259 );
2260 let _ = session
2261 .send(
2262 "Runtime.evaluate",
2263 json!({ "expression": expr, "awaitPromise": false }),
2264 )
2265 .await;
2266 });
2267 }
2268 Ok(_) => {}
2269 Err(broadcast::error::RecvError::Closed) => break,
2270 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2271 }
2272 }
2273}
2274
2275fn parse_console(params: &Value) -> ConsoleMessage { let text = params
2276 .get("args")
2277 .and_then(|a| a.as_array())
2278 .map(|args| {
2279 args.iter()
2280 .filter_map(|a| {
2281 a.get("value")
2282 .and_then(|v| v.as_str())
2283 .or_else(|| a.get("description").and_then(|v| v.as_str()))
2284 .map(String::from)
2285 })
2286 .collect::<Vec<_>>()
2287 .join(" ")
2288 })
2289 .unwrap_or_default();
2290 let kind = params
2291 .get("type")
2292 .and_then(|v| v.as_str())
2293 .unwrap_or("log")
2294 .to_string();
2295 let location = params
2297 .get("stackTrace")
2298 .and_then(|s| s.get("callFrames"))
2299 .and_then(|f| f.as_array())
2300 .and_then(|frames| frames.first())
2301 .map(|frame| {
2302 use crate::types::ConsoleMessageLocation;
2303 ConsoleMessageLocation {
2304 url: frame
2305 .get("url")
2306 .and_then(|v| v.as_str())
2307 .map(String::from),
2308 line_number: frame.get("lineNumber").and_then(|v| v.as_i64()),
2309 column_number: frame.get("columnNumber").and_then(|v| v.as_i64()),
2310 }
2311 });
2312 ConsoleMessage {
2313 text,
2314 r#type: kind,
2315 location,
2316 }
2317}
2318
2319async fn context_tracker(
2328 mut rx: broadcast::Receiver<crate::cdp::CdpEvent>,
2329 session: Arc<CdpSession>,
2330 ctx_cell: Arc<Mutex<Option<i64>>>,
2331 frame_ctx_cell: Arc<Mutex<HashMap<String, i64>>>,
2332 main_frame_id: Arc<Mutex<Option<String>>>,
2333) {
2334 loop {
2335 match rx.recv().await {
2336 Ok(ev) => match ev.method.as_str() {
2337 "Runtime.executionContextCreated" => {
2338 let context = match ev.params.get("context") {
2339 Some(c) => c,
2340 None => continue,
2341 };
2342 let aux = context.get("auxData");
2343 let is_default = aux
2344 .and_then(|a| a.get("type"))
2345 .and_then(|t| t.as_str())
2346 == Some("default");
2347 let id = context.get("id").and_then(|i| i.as_i64());
2348 let Some(id) = id else { continue };
2349
2350 let frame_id = aux
2351 .and_then(|a| a.get("frameId"))
2352 .and_then(|v| v.as_str())
2353 .map(String::from);
2354
2355 if is_default {
2356 if let Some(fid) = &frame_id {
2361 frame_ctx_cell.lock().insert(fid.clone(), id);
2362 }
2363
2364 let main_id = main_frame_id.lock().clone();
2373 let is_main_frame = match (&main_id, &frame_id) {
2374 (Some(m), Some(f)) => m == f,
2375 (None, _) => ctx_cell.lock().is_none(),
2376 (Some(_), None) => ctx_cell.lock().is_none(),
2379 };
2380 if is_main_frame {
2381 *ctx_cell.lock() = Some(id);
2382 }
2383 }
2384
2385 let s = Arc::clone(&session);
2387 tokio::spawn(async move {
2388 let _ = s
2389 .send(
2390 "Runtime.evaluate",
2391 json!({
2392 "expression": selectors::INJECTED_SCRIPT,
2393 "contextId": id,
2394 }),
2395 )
2396 .await;
2397 });
2398 }
2399 "Runtime.executionContextsCleared" => {
2400 *ctx_cell.lock() = None;
2401 frame_ctx_cell.lock().clear();
2402 }
2403 "Runtime.executionContextDestroyed" => {
2404 let id = ev
2405 .params
2406 .get("executionContextId")
2407 .and_then(|v| v.as_i64());
2408 {
2409 let mut cell = ctx_cell.lock();
2410 if id.is_some() && *cell == id {
2411 *cell = None;
2412 }
2413 }
2414 if let Some(id) = id {
2415 let mut frames = frame_ctx_cell.lock();
2416 frames.retain(|_, v| *v != id);
2419 }
2420 }
2421 _ => {}
2422 },
2423 Err(broadcast::error::RecvError::Closed) => break,
2424 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2425 }
2426 }
2427}
2428
2429#[derive(Debug, Clone)]
2433pub struct FrameEvent {
2434 pub id: String,
2436 pub parent_id: Option<String>,
2438 pub url: String,
2440}
2441
2442impl FrameEvent {
2443 pub(crate) fn from_attached(params: &Value) -> Option<Self> {
2445 let frame = params.get("frame").unwrap_or(params);
2446 let id = frame.get("id").and_then(|v| v.as_str())?.to_string();
2447 let parent_id = frame
2448 .get("parentFrameId")
2449 .and_then(|v| v.as_str())
2450 .map(String::from);
2451 Some(Self {
2452 id,
2453 parent_id,
2454 url: String::new(),
2455 })
2456 }
2457
2458 pub(crate) fn from_detached(params: &Value) -> Option<Self> {
2460 let id = params
2462 .get("frameId")
2463 .or_else(|| params.get("frame"))
2464 .and_then(|v| {
2465 v.as_str()
2466 .map(String::from)
2467 .or_else(|| v.get("id").and_then(|i| i.as_str()).map(String::from))
2468 })?;
2469 let parent_id = params
2470 .get("frame")
2471 .and_then(|f| f.get("parentFrameId"))
2472 .and_then(|v| v.as_str())
2473 .map(String::from);
2474 Some(Self {
2475 id,
2476 parent_id,
2477 url: String::new(),
2478 })
2479 }
2480
2481 pub(crate) fn from_navigated(params: &Value) -> Option<Self> {
2483 let frame = params.get("frame")?;
2484 let id = frame.get("id").and_then(|v| v.as_str())?.to_string();
2485 let url = frame
2486 .get("url")
2487 .and_then(|v| v.as_str())
2488 .unwrap_or("")
2489 .to_string();
2490 let parent_id = frame
2491 .get("parentId")
2492 .and_then(|v| v.as_str())
2493 .map(String::from);
2494 Some(Self {
2495 id,
2496 parent_id,
2497 url,
2498 })
2499 }
2500}
2501
2502pub struct Dialog {
2504 message: String,
2505 kind: String,
2506 session: Arc<CdpSession>,
2507}
2508
2509impl Dialog {
2510 pub(crate) fn from_event(params: &Value, session: &Arc<CdpSession>) -> Self {
2511 Self {
2512 message: params
2513 .get("message")
2514 .and_then(|v| v.as_str())
2515 .unwrap_or("")
2516 .to_string(),
2517 kind: params
2518 .get("type")
2519 .and_then(|v| v.as_str())
2520 .unwrap_or("alert")
2521 .to_string(),
2522 session: Arc::clone(session),
2523 }
2524 }
2525
2526 pub fn message(&self) -> &str {
2527 &self.message
2528 }
2529
2530 pub fn kind(&self) -> &str {
2531 &self.kind
2532 }
2533
2534 pub async fn accept(&self, prompt_text: Option<&str>) -> Result<()> {
2535 let mut p = json!({ "accept": true });
2536 if let Some(t) = prompt_text {
2537 p["promptText"] = json!(t);
2538 }
2539 self.session
2540 .send("Page.handleJavaScriptDialog", p)
2541 .await
2542 .map(|_| ())
2543 }
2544
2545 pub async fn dismiss(&self) -> Result<()> {
2546 self.session
2547 .send("Page.handleJavaScriptDialog", json!({ "accept": false }))
2548 .await
2549 .map(|_| ())
2550 }
2551}