1use super::page_core::Page;
2use crate::error::{CdpError, Result};
3use crate::page::element;
4use cdp_protocol::runtime;
5use serde_json::Value;
6use std::sync::Arc;
7use tokio::time::{Duration, sleep};
8
9#[derive(Clone, Debug)]
11pub struct RetryConfig {
12 pub max_retries: u32,
14 pub initial_delay_ms: u64,
16 pub backoff_multiplier: f64,
18 pub max_delay_ms: u64,
20}
21
22impl Default for RetryConfig {
23 fn default() -> Self {
24 Self {
25 max_retries: 3,
26 initial_delay_ms: 100,
27 backoff_multiplier: 2.0,
28 max_delay_ms: 5000,
29 }
30 }
31}
32
33impl RetryConfig {
34 pub fn conservative() -> Self {
36 Self {
37 max_retries: 5,
38 initial_delay_ms: 200,
39 backoff_multiplier: 2.0,
40 max_delay_ms: 10000,
41 }
42 }
43
44 pub fn aggressive() -> Self {
46 Self {
47 max_retries: 2,
48 initial_delay_ms: 50,
49 backoff_multiplier: 1.5,
50 max_delay_ms: 2000,
51 }
52 }
53
54 pub fn no_retry() -> Self {
56 Self {
57 max_retries: 0,
58 initial_delay_ms: 0,
59 backoff_multiplier: 1.0,
60 max_delay_ms: 0,
61 }
62 }
63}
64
65#[derive(Clone)]
66pub struct Frame {
67 pub id: String, pub page: Arc<Page>,
71}
72
73impl Frame {
74 pub fn new(id: String, page: Arc<Page>) -> Self {
76 Self { id, page }
77 }
78
79 pub fn id(&self) -> &str {
81 &self.id
82 }
83
84 pub async fn execution_context_id(&self) -> Result<u32> {
86 self.page
87 .contexts
88 .lock()
89 .await
90 .get(&self.id)
91 .cloned()
92 .ok_or_else(|| {
93 CdpError::frame(format!("Execution context not found for frame {}", self.id))
94 })
95 }
96
97 pub async fn call_function_on(
99 &self,
100 function_declaration: &str,
101 args: Vec<Value>,
102 ) -> Result<Value> {
103 let context_id = self.execution_context_id().await?;
104 let params = runtime::CallFunctionOn {
105 function_declaration: function_declaration.to_string(),
106 object_id: None,
107 arguments: Some(
108 args.into_iter()
109 .map(|v| runtime::CallArgument {
110 value: Some(v),
111 unserializable_value: None,
112 object_id: None,
113 })
114 .collect(),
115 ),
116 silent: None,
117 return_by_value: Some(true),
118 generate_preview: None,
119 user_gesture: None,
120 await_promise: Some(true),
121 execution_context_id: Some(context_id),
122 object_group: None,
123 throw_on_side_effect: None,
124 unique_context_id: None,
125 serialization_options: None,
126 };
127 let result: runtime::CallFunctionOnReturnObject =
128 self.page.session.send_command(params, None).await?;
129 if let Some(details) = result.exception_details.as_ref() {
130 return Err(CdpError::frame(format!(
131 "JavaScript execution failed: {:?}",
132 details
133 )));
134 }
135 Ok(result.result.value.unwrap_or(Value::Null))
137 }
138
139 pub async fn evaluate(&self, script: &str) -> Result<Value> {
141 let context_id = self.execution_context_id().await?;
142 let params = runtime::Evaluate {
143 expression: script.to_string(),
144 object_group: None,
145 include_command_line_api: None,
146 silent: None,
147 context_id: Some(context_id),
148 return_by_value: Some(true),
149 generate_preview: None,
150 user_gesture: None,
151 await_promise: Some(true),
152 throw_on_side_effect: None,
153 timeout: None,
154 disable_breaks: None,
155 repl_mode: None,
156 allow_unsafe_eval_blocked_by_csp: None,
157 unique_context_id: None,
158 serialization_options: None,
159 };
160 let result: runtime::EvaluateReturnObject =
161 self.page.session.send_command(params, None).await?;
162 if let Some(details) = result.exception_details.as_ref() {
163 return Err(CdpError::frame(format!(
164 "JavaScript execution failed: {:?}",
165 details
166 )));
167 }
168 Ok(result.result.value.unwrap_or(Value::Null))
169 }
170
171 pub async fn name(&self) -> Result<Option<String>> {
173 let script = "window.name";
174 let result = self.evaluate(script).await?;
175 Ok(result.as_str().map(|s| s.to_string()))
176 }
177
178 pub async fn url(&self) -> Result<String> {
180 let script = "window.location.href";
181 let result = self.evaluate(script).await?;
182 result
183 .as_str()
184 .map(|s| s.to_string())
185 .ok_or_else(|| CdpError::frame(format!("Failed to resolve URL for frame {}", self.id)))
186 }
187
188 pub async fn is_detached(&self) -> bool {
190 self.page.contexts.lock().await.get(&self.id).is_none()
192 }
193
194 pub async fn query_selector(&self, selector: &str) -> Result<Option<element::ElementHandle>> {
201 if Self::is_xpath(selector) {
203 return self.query_selector_xpath(selector).await;
204 }
205
206 use cdp_protocol::dom::{
208 DescribeNode, DescribeNodeReturnObject, GetDocument, GetDocumentReturnObject,
209 QuerySelector, QuerySelectorReturnObject,
210 };
211
212 let doc_result: GetDocumentReturnObject = self
214 .page
215 .session
216 .send_command(
217 GetDocument {
218 depth: Some(0), pierce: Some(false),
220 },
221 None,
222 )
223 .await?;
224
225 let root_node_id = doc_result.root.node_id;
226
227 let query_result = self
229 .page
230 .session
231 .send_command::<_, QuerySelectorReturnObject>(
232 QuerySelector {
233 node_id: root_node_id,
234 selector: selector.to_string(),
235 },
236 None,
237 )
238 .await?;
239
240 if query_result.node_id == 0 {
241 return Ok(None);
242 }
243
244 let query_node_id = query_result.node_id;
245
246 let describe_result = self
248 .page
249 .session
250 .send_command::<_, DescribeNodeReturnObject>(
251 DescribeNode {
252 node_id: Some(query_node_id),
253 backend_node_id: None,
254 object_id: None,
255 depth: None,
256 pierce: None,
257 },
258 None,
259 )
260 .await?;
261
262 Ok(Some(element::ElementHandle {
263 backend_node_id: describe_result.node.backend_node_id,
264 node_id: Some(query_node_id),
265 page: Arc::clone(&self.page),
266 }))
267 }
268
269 pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<element::ElementHandle>> {
271 if Self::is_xpath(selector) {
273 return self.query_selector_all_xpath(selector).await;
274 }
275
276 use cdp_protocol::dom::{
278 DescribeNode, DescribeNodeReturnObject, GetDocument, GetDocumentReturnObject,
279 QuerySelectorAll, QuerySelectorAllReturnObject,
280 };
281
282 let doc_result: GetDocumentReturnObject = self
284 .page
285 .session
286 .send_command(
287 GetDocument {
288 depth: Some(0), pierce: Some(false),
290 },
291 None,
292 )
293 .await?;
294
295 let root_node_id = doc_result.root.node_id;
296
297 let query_result = self
299 .page
300 .session
301 .send_command::<_, QuerySelectorAllReturnObject>(
302 QuerySelectorAll {
303 node_id: root_node_id,
304 selector: selector.to_string(),
305 },
306 None,
307 )
308 .await?;
309
310 let mut elements = Vec::new();
311 for node_id in query_result.node_ids {
312 if node_id == 0 {
313 continue;
314 }
315
316 let describe_result = self
317 .page
318 .session
319 .send_command::<_, DescribeNodeReturnObject>(
320 DescribeNode {
321 node_id: Some(node_id),
322 backend_node_id: None,
323 object_id: None,
324 depth: None,
325 pierce: None,
326 },
327 None,
328 )
329 .await?;
330
331 elements.push(element::ElementHandle {
332 backend_node_id: describe_result.node.backend_node_id,
333 node_id: Some(node_id),
334 page: Arc::clone(&self.page),
335 });
336 }
337
338 Ok(elements)
339 }
340
341 pub async fn call_function_on_with_retry(
345 &self,
346 function_declaration: &str,
347 args: Vec<Value>,
348 config: RetryConfig,
349 ) -> Result<Value> {
350 let mut attempt = 0;
351 let mut delay_ms = config.initial_delay_ms;
352
353 loop {
354 match self
355 .call_function_on(function_declaration, args.clone())
356 .await
357 {
358 Ok(result) => return Ok(result),
359 Err(err) => {
360 if attempt >= config.max_retries {
361 return Err(CdpError::frame(format!(
362 "call_function_on failed after {} retries: {}",
363 config.max_retries, err
364 )));
365 }
366
367 if self.is_detached().await {
369 return Err(CdpError::frame(format!(
370 "Frame '{}' is detached; cannot retry",
371 self.id
372 )));
373 }
374
375 sleep(Duration::from_millis(delay_ms)).await;
377 delay_ms = ((delay_ms as f64) * config.backoff_multiplier) as u64;
378 delay_ms = delay_ms.min(config.max_delay_ms);
379 attempt += 1;
380 }
381 }
382 }
383 }
384
385 pub async fn evaluate_with_retry(&self, script: &str, config: RetryConfig) -> Result<Value> {
387 let mut attempt = 0;
388 let mut delay_ms = config.initial_delay_ms;
389
390 loop {
391 match self.evaluate(script).await {
392 Ok(result) => return Ok(result),
393 Err(err) => {
394 if attempt >= config.max_retries {
395 return Err(CdpError::frame(format!(
396 "evaluate failed after {} retries: {}",
397 config.max_retries, err
398 )));
399 }
400
401 if self.is_detached().await {
403 return Err(CdpError::frame(format!(
404 "Frame '{}' is detached; cannot retry",
405 self.id
406 )));
407 }
408
409 sleep(Duration::from_millis(delay_ms)).await;
411 delay_ms = ((delay_ms as f64) * config.backoff_multiplier) as u64;
412 delay_ms = delay_ms.min(config.max_delay_ms);
413 attempt += 1;
414 }
415 }
416 }
417 }
418
419 fn is_xpath(selector: &str) -> bool {
424 selector.starts_with("xpath:") || selector.starts_with("/") || selector.starts_with("(")
425 }
426
427 fn normalize_xpath(selector: &str) -> &str {
429 selector.strip_prefix("xpath:").unwrap_or(selector)
430 }
431
432 async fn query_selector_xpath(&self, xpath: &str) -> Result<Option<element::ElementHandle>> {
434 use cdp_protocol::dom::{
435 DescribeNode, DescribeNodeReturnObject, DiscardSearchResults, GetSearchResults,
436 GetSearchResultsReturnObject, PerformSearch, PerformSearchReturnObject,
437 };
438
439 let xpath = Self::normalize_xpath(xpath);
440
441 let search_result: PerformSearchReturnObject = self
443 .page
444 .session
445 .send_command(
446 PerformSearch {
447 query: xpath.to_string(),
448 include_user_agent_shadow_dom: Some(true),
449 },
450 None,
451 )
452 .await?;
453
454 let search_id = search_result.search_id;
455 let result_count = search_result.result_count;
456
457 if result_count == 0 {
458 let _ = self
460 .page
461 .session
462 .send_command::<_, ()>(
463 DiscardSearchResults {
464 search_id: search_id.clone(),
465 },
466 None,
467 )
468 .await;
469 return Ok(None);
470 }
471
472 let get_results: GetSearchResultsReturnObject = self
474 .page
475 .session
476 .send_command(
477 GetSearchResults {
478 search_id: search_id.clone(),
479 from_index: 0,
480 to_index: 1,
481 },
482 None,
483 )
484 .await?;
485
486 let _ = self
488 .page
489 .session
490 .send_command::<_, ()>(DiscardSearchResults { search_id }, None)
491 .await;
492
493 if get_results.node_ids.is_empty() {
494 return Ok(None);
495 }
496
497 let node_id = get_results.node_ids[0];
498
499 if node_id == 0 {
500 eprintln!("Warning: XPath search returned an invalid node_id (0)");
501 return Ok(None);
502 }
503
504 let describe_result = self
506 .page
507 .session
508 .send_command::<_, DescribeNodeReturnObject>(
509 DescribeNode {
510 node_id: Some(node_id),
511 backend_node_id: None,
512 object_id: None,
513 depth: None,
514 pierce: None,
515 },
516 None,
517 )
518 .await?;
519
520 Ok(Some(element::ElementHandle {
521 backend_node_id: describe_result.node.backend_node_id,
522 node_id: Some(node_id),
523 page: Arc::clone(&self.page),
524 }))
525 }
526
527 async fn query_selector_all_xpath(&self, xpath: &str) -> Result<Vec<element::ElementHandle>> {
529 use cdp_protocol::dom::{
530 DescribeNode, DescribeNodeReturnObject, DiscardSearchResults, GetSearchResults,
531 GetSearchResultsReturnObject, PerformSearch, PerformSearchReturnObject,
532 };
533
534 let xpath = Self::normalize_xpath(xpath);
535
536 let search_result: PerformSearchReturnObject = self
538 .page
539 .session
540 .send_command(
541 PerformSearch {
542 query: xpath.to_string(),
543 include_user_agent_shadow_dom: Some(true),
544 },
545 None,
546 )
547 .await?;
548
549 let search_id = search_result.search_id;
550 let result_count = search_result.result_count;
551
552 if result_count == 0 {
553 let _ = self
555 .page
556 .session
557 .send_command::<_, ()>(
558 DiscardSearchResults {
559 search_id: search_id.clone(),
560 },
561 None,
562 )
563 .await;
564 return Ok(Vec::new());
565 }
566
567 let get_results: GetSearchResultsReturnObject = self
569 .page
570 .session
571 .send_command(
572 GetSearchResults {
573 search_id: search_id.clone(),
574 from_index: 0,
575 to_index: result_count,
576 },
577 None,
578 )
579 .await?;
580
581 let _ = self
583 .page
584 .session
585 .send_command::<_, ()>(DiscardSearchResults { search_id }, None)
586 .await;
587
588 let mut elements = Vec::new();
590 for node_id in get_results.node_ids {
591 if node_id == 0 {
592 continue;
593 }
594
595 let describe_result = self
596 .page
597 .session
598 .send_command::<_, DescribeNodeReturnObject>(
599 DescribeNode {
600 node_id: Some(node_id),
601 backend_node_id: None,
602 object_id: None,
603 depth: None,
604 pierce: None,
605 },
606 None,
607 )
608 .await?;
609
610 elements.push(element::ElementHandle {
611 backend_node_id: describe_result.node.backend_node_id,
612 node_id: Some(node_id),
613 page: Arc::clone(&self.page),
614 });
615 }
616
617 Ok(elements)
618 }
619}