1use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::cdp::{Cookie, MouseButton, MouseEventType, Session};
9use crate::error::{Error, Result};
10use crate::stealth::Human;
11use crate::StealthConfig;
12
13const POLL_INTERVAL_MS: u64 = 100;
15
16const SETTLE_MS: u64 = 100;
19
20const INTERACTION_DELAY_MS: u64 = 50;
22
23async fn sleep_ms(ms: u64) {
25 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
26}
27
28fn escape_js_string(s: &str) -> String {
30 let mut out = String::with_capacity(s.len());
31 let mut chars = s.chars().peekable();
32 while let Some(ch) = chars.next() {
33 match ch {
34 '\\' => out.push_str("\\\\"),
35 '\'' => out.push_str("\\'"),
36 '"' => out.push_str("\\\""),
37 '`' => out.push_str("\\`"),
38 '\n' => out.push_str("\\n"),
39 '\r' => out.push_str("\\r"),
40 '\0' => out.push_str("\\0"),
41 '\u{2028}' => out.push_str("\\u2028"),
42 '\u{2029}' => out.push_str("\\u2029"),
43 '$' if chars.peek() == Some(&'{') => {
44 out.push_str("\\${");
45 chars.next();
46 }
47 _ => out.push(ch),
48 }
49 }
50 out
51}
52
53fn is_element_cdp_error(e: &Error) -> bool {
55 match e {
56 Error::ElementNotFound(_) | Error::ElementNotVisible { .. } => true,
57 Error::Cdp { message, .. } => {
58 message.contains("box model")
59 || message.contains("Could not find node")
60 || message.contains("No node with given id")
61 || message.contains("Node is not an element")
62 }
63 _ => false,
64 }
65}
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum TextMatch {
69 Exact,
71 #[default]
73 Contains,
74 StartsWith,
76 EndsWith,
78}
79
80pub struct Page {
82 session: Session,
83 config: Arc<StealthConfig>,
84}
85
86impl Page {
87 pub(crate) fn new(session: Session, config: Arc<StealthConfig>) -> Self {
89 Self { session, config }
90 }
91
92 pub fn session(&self) -> &Session {
94 &self.session
95 }
96
97 pub fn target_id(&self) -> &str {
99 self.session.target_id()
100 }
101
102 fn check_nav_result(result: &crate::cdp::types::PageNavigateResult) -> Result<()> {
105 if let Some(ref error) = result.error_text {
106 if error != "net::ERR_HTTP_RESPONSE_CODE_FAILURE" {
107 return Err(Error::Navigation(error.clone()));
108 }
109 }
110 Ok(())
111 }
112
113 async fn cookie_url(&self) -> Result<Option<String>> {
116 let url = self.url().await?;
117 Ok(if url == "about:blank" { None } else { Some(url) })
118 }
119
120 pub async fn goto(&self, url: &str) -> Result<()> {
122 self.navigate_impl(url, None).await
123 }
124
125 pub async fn reload(&self) -> Result<()> {
127 self.session.reload(false).await
128 }
129
130 pub async fn back(&self) -> Result<()> {
132 self.session.go_back().await
133 }
134
135 pub async fn forward(&self) -> Result<()> {
137 self.session.go_forward().await
138 }
139 pub async fn url(&self) -> Result<String> {
141 let frame_tree = self.session.get_frame_tree().await?;
142 Ok(frame_tree.frame.url)
143 }
144
145 pub async fn title(&self) -> Result<String> {
147 self.evaluate_sync("document.title || ''").await
149 }
150
151 pub async fn content(&self) -> Result<String> {
153 self.evaluate_sync("document.documentElement.outerHTML")
154 .await
155 }
156
157 pub async fn text(&self) -> Result<String> {
159 self.evaluate_sync("document.body?.innerText || ''").await
160 }
161 pub async fn screenshot(&self) -> Result<Vec<u8>> {
163 self.session.capture_screenshot(Some("png"), None).await
164 }
165
166 pub async fn screenshot_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
168 self.session
169 .capture_screenshot(Some("jpeg"), Some(quality))
170 .await
171 }
172 pub async fn find(&self, selector: &str) -> Result<Element<'_>> {
174 let doc = self.session.get_document(Some(0)).await?;
175 let node_id = self.session.query_selector(doc.node_id, selector).await?;
176
177 if node_id == 0 {
178 return Err(Error::ElementNotFound(selector.to_string()));
179 }
180
181 Ok(Element {
182 page: self,
183 node_id,
184 })
185 }
186
187 pub async fn find_all(&self, selector: &str) -> Result<Vec<Element<'_>>> {
189 let doc = self.session.get_document(Some(0)).await?;
190 let node_ids = self
191 .session
192 .query_selector_all(doc.node_id, selector)
193 .await?;
194
195 Ok(node_ids
196 .into_iter()
197 .filter(|&id| id != 0)
198 .map(|node_id| Element {
199 page: self,
200 node_id,
201 })
202 .collect())
203 }
204
205 #[must_use = "returns true if element exists"]
207 pub async fn exists(&self, selector: &str) -> bool {
208 self.find(selector).await.is_ok()
209 }
210 pub async fn find_by_text(&self, text: &str) -> Result<Element<'_>> {
212 self.find_by_text_match(text, TextMatch::Contains).await
213 }
214
215 pub async fn find_by_text_match(
220 &self,
221 text: &str,
222 match_type: TextMatch,
223 ) -> Result<Element<'_>> {
224 let escaped_text = escape_js_string(text);
225 let match_js = match match_type {
226 TextMatch::Exact => format!("t.trim() === '{}'", escaped_text),
227 TextMatch::Contains => format!(
228 "t.toLowerCase().includes('{}')",
229 escaped_text.to_lowercase()
230 ),
231 TextMatch::StartsWith => format!(
232 "t.toLowerCase().startsWith('{}')",
233 escaped_text.to_lowercase()
234 ),
235 TextMatch::EndsWith => format!(
236 "t.toLowerCase().endsWith('{}')",
237 escaped_text.to_lowercase()
238 ),
239 };
240
241 let js = format!(
242 r#"
243 (() => {{
244 const interactive = 'a, button, input[type="submit"], input[type="button"], [role="button"], [onclick]';
245 for (const el of document.querySelectorAll(interactive)) {{
246 const t = el.innerText || el.textContent || el.value || '';
247 if ({match_js}) return el;
248 }}
249 const secondary = 'label, span, div, p, h1, h2, h3, h4, h5, h6, li, td, th';
250 for (const el of document.querySelectorAll(secondary)) {{
251 const t = el.innerText || el.textContent || el.value || '';
252 if ({match_js}) return el;
253 }}
254 return null;
255 }})()
256 "#,
257 );
258
259 let result = self.session.evaluate_for_remote_object(&js).await?;
260 let remote = self.check_js_result(result)?;
261
262 if remote.subtype.as_deref() == Some("null") {
263 return Err(Error::ElementNotFound(format!("text: {}", text)));
264 }
265
266 let object_id = remote
267 .object_id
268 .ok_or_else(|| Error::ElementNotFound(format!("text: {}", text)))?;
269
270 let node_id = self.session.request_node(&object_id).await?;
272
273 if node_id == 0 {
274 return Err(Error::ElementNotFound(format!("text: {}", text)));
275 }
276
277 Ok(Element {
278 page: self,
279 node_id,
280 })
281 }
282
283 pub async fn find_all_by_text(&self, text: &str) -> Result<Vec<Element<'_>>> {
285 let escaped_text = escape_js_string(text).to_lowercase();
286
287 let js = format!(
288 r#"
289 (() => {{
290 const selectors = 'a, button, input, label, span, div, p, h1, h2, h3, h4, h5, h6, li, td, th';
291 const elements = document.querySelectorAll(selectors);
292 const matches = [];
293 for (const el of elements) {{
294 const t = (el.innerText || el.textContent || el.value || '').toLowerCase();
295 if (t.includes('{escaped_text}')) {{
296 matches.push(el);
297 }}
298 }}
299 return matches;
300 }})()
301 "#,
302 );
303
304 let result = self.session.evaluate_for_remote_object(&js).await?;
306
307 let remote = self.check_js_result(result)?;
308
309 let array_object_id = match &remote.object_id {
310 Some(id) => id.clone(),
311 None => return Ok(Vec::new()),
312 };
313
314 let properties = self.session.get_properties(&array_object_id).await?;
316
317 let mut elements = Vec::new();
318 for prop in &properties {
319 if prop.name.parse::<usize>().is_err() {
321 continue;
322 }
323 if let Some(ref obj_id) = prop.value.as_ref().and_then(|v| v.object_id.clone()) {
324 if let Ok(node_id) = self.session.request_node(obj_id).await {
325 if node_id != 0 {
326 elements.push(Element {
327 page: self,
328 node_id,
329 });
330 }
331 }
332 }
333 }
334
335 Ok(elements)
336 }
337
338 #[must_use = "returns true if text exists on page"]
340 pub async fn text_exists(&self, text: &str) -> bool {
341 self.find_by_text(text).await.is_ok()
342 }
343 pub async fn click_at(&self, x: f64, y: f64) -> Result<()> {
345 self.session
347 .dispatch_mouse_event(
348 MouseEventType::MousePressed,
349 x,
350 y,
351 Some(MouseButton::Left),
352 Some(1),
353 )
354 .await?;
355
356 sleep_ms(INTERACTION_DELAY_MS).await;
357
358 self.session
360 .dispatch_mouse_event(
361 MouseEventType::MouseReleased,
362 x,
363 y,
364 Some(MouseButton::Left),
365 Some(1),
366 )
367 .await?;
368
369 Ok(())
370 }
371
372 pub async fn click(&self, selector: &str) -> Result<()> {
374 let element = self.find(selector).await?;
375 element.click().await
376 }
377
378 pub async fn type_text(&self, text: &str) -> Result<()> {
380 self.session.insert_text(text).await
381 }
382
383 pub async fn type_into(&self, selector: &str, text: &str) -> Result<()> {
385 let element = self.find(selector).await?;
386 element.click().await?;
387 sleep_ms(INTERACTION_DELAY_MS).await;
388 self.session.insert_text(text).await
389 }
390
391 pub async fn click_by_text(&self, text: &str) -> Result<()> {
393 let element = self.find_by_text(text).await?;
394 element.click().await
395 }
396
397 #[must_use = "returns true if clicked, false if not found/visible"]
399 pub async fn try_click(&self, selector: &str) -> Result<bool> {
400 self.try_click_impl(self.find(selector).await).await
401 }
402
403 #[must_use = "returns true if clicked, false if not found/visible"]
405 pub async fn try_click_by_text(&self, text: &str) -> Result<bool> {
406 self.try_click_impl(self.find_by_text(text).await).await
407 }
408
409 async fn try_click_impl(&self, find_result: Result<Element<'_>>) -> Result<bool> {
411 match find_result {
412 Ok(element) => match element.click().await {
413 Ok(()) => Ok(true),
414 Err(e) if is_element_cdp_error(&e) => Ok(false),
415 Err(e) => Err(e),
416 },
417 Err(e) if is_element_cdp_error(&e) => Ok(false),
418 Err(e) => Err(e),
419 }
420 }
421
422 pub async fn fill(&self, selector: &str, value: &str) -> Result<()> {
424 let element = self.find(selector).await?;
425 element.click().await?;
426 sleep_ms(INTERACTION_DELAY_MS).await;
427
428 let escaped = escape_js_string(selector);
430 self.execute(&format!(
431 "(() => {{ const el = document.querySelector('{}'); if (el) {{ el.focus(); el.select(); }} }})()",
432 escaped
433 )).await?;
434 self.session.insert_text("").await?;
435
436 self.session.insert_text(value).await
438 }
439 pub fn human(&self) -> Human<'_> {
441 Human::new(&self.session)
442 }
443
444 pub async fn human_click(&self, selector: &str) -> Result<()> {
446 let element = self.find(selector).await?;
447 let (x, y) = element.center().await?;
448 self.human_click_at_center_xy(x, y).await
449 }
450
451 pub async fn human_type(&self, selector: &str, text: &str) -> Result<()> {
453 self.human_click(selector).await?;
454 sleep_ms(SETTLE_MS).await;
455 self.human_type_text(text).await
456 }
457
458 pub async fn human_click_by_text(&self, text: &str) -> Result<()> {
460 let element = self.find_by_text(text).await?;
461 let (x, y) = element.center().await?;
462 self.human_click_at_center_xy(x, y).await
463 }
464
465 #[must_use = "returns true if clicked, false if not found/visible"]
467 pub async fn try_human_click(&self, selector: &str) -> Result<bool> {
468 self.try_human_click_impl(self.find(selector).await).await
469 }
470
471 #[must_use = "returns true if clicked, false if not found/visible"]
473 pub async fn try_human_click_by_text(&self, text: &str) -> Result<bool> {
474 self.try_human_click_impl(self.find_by_text(text).await)
475 .await
476 }
477
478 pub async fn human_fill(&self, selector: &str, value: &str) -> Result<()> {
480 self.human_click(selector).await?;
481 sleep_ms(SETTLE_MS).await;
482 self.execute("document.activeElement.select()").await?;
483 sleep_ms(INTERACTION_DELAY_MS).await;
484 self.human_type_text(value).await
485 }
486
487 async fn human_type_text(&self, text: &str) -> Result<()> {
489 if self.config.human_typing {
490 self.human().type_text(text).await
491 } else {
492 self.session.insert_text(text).await
493 }
494 }
495
496 async fn human_click_at_center_xy(&self, x: f64, y: f64) -> Result<()> {
497 if self.config.human_mouse {
498 self.human().move_and_click(x, y).await
499 } else {
500 self.click_at(x, y).await
501 }
502 }
503
504 async fn try_human_click_impl(&self, find_result: Result<Element<'_>>) -> Result<bool> {
506 match find_result {
507 Ok(element) => match element.center().await {
508 Ok((x, y)) => {
509 self.human_click_at_center_xy(x, y).await?;
510 Ok(true)
511 }
512 Err(e) if is_element_cdp_error(&e) => Ok(false),
513 Err(e) => Err(e),
514 },
515 Err(e) if is_element_cdp_error(&e) => Ok(false),
516 Err(e) => Err(e),
517 }
518 }
519 pub async fn evaluate<T: serde::de::DeserializeOwned>(&self, expression: &str) -> Result<T> {
521 self.eval_impl(self.session.evaluate(expression).await?)
522 }
523
524 pub async fn evaluate_sync<T: serde::de::DeserializeOwned>(
527 &self,
528 expression: &str,
529 ) -> Result<T> {
530 self.eval_impl(self.session.evaluate_sync(expression).await?)
531 }
532
533 fn eval_impl<T: serde::de::DeserializeOwned>(
535 &self,
536 result: crate::cdp::types::RuntimeEvaluateResult,
537 ) -> Result<T> {
538 let remote = self.check_js_result(result)?;
539 let value = remote
540 .value
541 .ok_or_else(|| Error::CdpSimple("No value returned from evaluate".into()))?;
542 Ok(serde_json::from_value(value)?)
543 }
544
545 pub async fn execute(&self, expression: &str) -> Result<()> {
547 self.check_js_result(self.session.evaluate(expression).await?)?;
548 Ok(())
549 }
550
551 pub async fn execute_sync(&self, expression: &str) -> Result<()> {
553 self.check_js_result(self.session.evaluate_sync(expression).await?)?;
554 Ok(())
555 }
556
557 fn check_js_result(
559 &self,
560 result: crate::cdp::types::RuntimeEvaluateResult,
561 ) -> Result<crate::cdp::types::RemoteObject> {
562 if let Some(exception) = result.exception_details {
563 return Err(Error::CdpSimple(format!(
564 "JavaScript error: {} at {}:{}",
565 exception.text, exception.line_number, exception.column_number
566 )));
567 }
568 Ok(result.result)
569 }
570 pub async fn cookies(&self) -> Result<Vec<Cookie>> {
572 self.session.get_cookies(None).await
573 }
574
575 pub async fn set_cookie(
577 &self,
578 name: &str,
579 value: &str,
580 domain: Option<&str>,
581 path: Option<&str>,
582 ) -> Result<()> {
583 let url = self.cookie_url().await?;
584 let success = self
585 .session
586 .set_cookie(name, value, url.as_deref(), domain, path)
587 .await?;
588 if !success {
589 return Err(Error::CdpSimple("Failed to set cookie".into()));
590 }
591 Ok(())
592 }
593
594 pub async fn delete_cookie(&self, name: &str, domain: Option<&str>) -> Result<()> {
596 let url = self.cookie_url().await?;
597 self.session
598 .delete_cookies(name, url.as_deref(), domain)
599 .await
600 }
601
602 pub async fn clear_all_cookies(&self) -> Result<()> {
604 self.session.clear_all_cookies().await
605 }
606
607 pub async fn set_cookies_bulk(
609 &self,
610 cookies: Vec<crate::cdp::types::NetworkSetCookie>,
611 ) -> Result<()> {
612 self.session.set_cookies(cookies).await
613 }
614
615 pub async fn set_extra_headers(&self, headers: HashMap<String, String>) -> Result<()> {
618 self.session.set_extra_headers(headers).await
619 }
620
621 pub async fn clear_extra_headers(&self) -> Result<()> {
623 self.session.clear_extra_headers().await
624 }
625
626 pub async fn goto_with_headers(
629 &self,
630 url: &str,
631 headers: HashMap<String, String>,
632 ) -> Result<()> {
633 self.session.set_extra_headers(headers).await?;
634 let result = self.goto(url).await;
635 let _ = self.session.clear_extra_headers().await;
636 result
637 }
638
639 pub async fn goto_with_referrer(&self, url: &str, referrer: &str) -> Result<()> {
641 self.navigate_impl(url, Some(referrer)).await
642 }
643
644 async fn navigate_impl(&self, url: &str, referrer: Option<&str>) -> Result<()> {
646 let result = self.session.navigate(url, referrer).await?;
647 Self::check_nav_result(&result)?;
648 sleep_ms(SETTLE_MS).await;
649 Ok(())
650 }
651
652 pub async fn set_bypass_csp(&self, enabled: bool) -> Result<()> {
655 self.session.set_bypass_csp(enabled).await
656 }
657
658 pub async fn set_user_agent(&self, user_agent: &str) -> Result<()> {
660 self.session.set_user_agent(user_agent, None).await
661 }
662
663 pub async fn ignore_cert_errors(&self, ignore: bool) -> Result<()> {
665 self.session.set_ignore_cert_errors(ignore).await
666 }
667
668 pub async fn accept_dialog(&self, prompt_text: Option<&str>) -> Result<()> {
671 self.session.handle_dialog(true, prompt_text).await
672 }
673
674 pub async fn dismiss_dialog(&self) -> Result<()> {
676 self.session.handle_dialog(false, None).await
677 }
678
679 async fn poll_until<T, F, Fut>(&self, timeout_ms: u64, error_msg: String, check: F) -> Result<T>
683 where
684 F: Fn() -> Fut,
685 Fut: std::future::Future<Output = Option<T>>,
686 {
687 let start = std::time::Instant::now();
688 let timeout = std::time::Duration::from_millis(timeout_ms);
689 loop {
690 if let Some(val) = check().await {
691 return Ok(val);
692 }
693 if start.elapsed() > timeout {
694 return Err(Error::Timeout(error_msg));
695 }
696 sleep_ms(POLL_INTERVAL_MS).await;
697 }
698 }
699
700 pub async fn wait_for(&self, selector: &str, timeout_ms: u64) -> Result<Element<'_>> {
702 self.poll_until(
703 timeout_ms,
704 format!("Element '{}' not found within {}ms", selector, timeout_ms),
705 || async { self.find(selector).await.ok() },
706 )
707 .await
708 }
709
710 pub async fn wait_for_visible(&self, selector: &str, timeout_ms: u64) -> Result<Element<'_>> {
712 self.poll_until(
713 timeout_ms,
714 format!("Element '{}' not visible within {}ms", selector, timeout_ms),
715 || async {
716 if let Ok(elem) = self.find(selector).await {
717 if elem.center().await.is_ok() {
718 return Some(elem);
719 }
720 }
721 None
722 },
723 )
724 .await
725 }
726
727 pub async fn wait_for_hidden(&self, selector: &str, timeout_ms: u64) -> Result<()> {
729 self.poll_until(
730 timeout_ms,
731 format!(
732 "Element '{}' still visible after {}ms",
733 selector, timeout_ms
734 ),
735 || async { self.find(selector).await.is_err().then_some(()) },
736 )
737 .await
738 }
739
740 pub async fn wait(&self, ms: u64) {
742 sleep_ms(ms).await;
743 }
744
745 pub async fn wait_for_text(&self, text: &str, timeout_ms: u64) -> Result<Element<'_>> {
747 self.poll_until(
748 timeout_ms,
749 format!(
750 "Element with text '{}' not found within {}ms",
751 text, timeout_ms
752 ),
753 || async { self.find_by_text(text).await.ok() },
754 )
755 .await
756 }
757
758 pub async fn wait_for_url_contains(&self, pattern: &str, timeout_ms: u64) -> Result<()> {
760 self.poll_until(
761 timeout_ms,
762 format!("URL did not contain '{}' within {}ms", pattern, timeout_ms),
763 || async {
764 if let Ok(url) = self.url().await {
765 if url.contains(pattern) {
766 return Some(());
767 }
768 }
769 None
770 },
771 )
772 .await
773 }
774
775 pub async fn wait_for_url_change(&self, timeout_ms: u64) -> Result<String> {
777 let original_url = self.url().await?;
778 self.poll_until(
779 timeout_ms,
780 format!(
781 "URL did not change from '{}' within {}ms",
782 original_url, timeout_ms
783 ),
784 || async {
785 if let Ok(url) = self.url().await {
786 if url != original_url {
787 return Some(url);
788 }
789 }
790 None
791 },
792 )
793 .await
794 }
795 pub async fn enable_request_capture(&self) -> Result<()> {
798 self.session.network_enable().await
799 }
800
801 pub async fn disable_request_capture(&self) -> Result<()> {
803 self.session.network_disable().await
804 }
805
806 pub async fn get_response_body(&self, request_id: &str) -> Result<ResponseBody> {
809 let (body, base64_encoded) = self.session.get_response_body(request_id).await?;
810
811 if base64_encoded {
812 use base64::Engine;
813 let bytes = base64::engine::general_purpose::STANDARD
814 .decode(&body)
815 .map_err(|e| Error::Decode(e.to_string()))?;
816 Ok(ResponseBody::Binary(bytes))
817 } else {
818 Ok(ResponseBody::Text(body))
819 }
820 }
821 pub async fn find_any(&self, selectors: &[&str]) -> Result<Element<'_>> {
823 for selector in selectors {
824 if let Ok(element) = self.find(selector).await {
825 return Ok(element);
826 }
827 }
828 Err(Error::ElementNotFound(format!(
829 "None of selectors found: {:?}",
830 selectors
831 )))
832 }
833
834 pub async fn wait_for_any(&self, selectors: &[&str], timeout_ms: u64) -> Result<Element<'_>> {
838 self.poll_until(
839 timeout_ms,
840 format!(
841 "None of selectors found within {}ms: {:?}",
842 timeout_ms, selectors
843 ),
844 || async { self.find_any(selectors).await.ok() },
845 )
846 .await
847 }
848 pub async fn wait_for_network_idle(&self, idle_time_ms: u64, timeout_ms: u64) -> Result<()> {
850 let start = std::time::Instant::now();
851 let timeout = std::time::Duration::from_millis(timeout_ms);
852 let idle_duration = std::time::Duration::from_millis(idle_time_ms);
853
854 let check_idle_js = r#"
856 (() => {
857 // Check if there are pending fetches/XHRs
858 if (window.__eoka_pending_requests === undefined) {
859 window.__eoka_pending_requests = 0;
860
861 // Intercept fetch
862 const originalFetch = window.fetch;
863 window.fetch = function(...args) {
864 window.__eoka_pending_requests++;
865 return originalFetch.apply(this, args).finally(() => {
866 window.__eoka_pending_requests--;
867 });
868 };
869
870 // Intercept XHR
871 const originalOpen = XMLHttpRequest.prototype.open;
872 const originalSend = XMLHttpRequest.prototype.send;
873 XMLHttpRequest.prototype.open = function(...args) {
874 this.__eoka_tracked = true;
875 return originalOpen.apply(this, args);
876 };
877 XMLHttpRequest.prototype.send = function(...args) {
878 if (this.__eoka_tracked) {
879 window.__eoka_pending_requests++;
880 this.addEventListener('loadend', () => {
881 window.__eoka_pending_requests--;
882 });
883 }
884 return originalSend.apply(this, args);
885 };
886 }
887 return window.__eoka_pending_requests;
888 })()
889 "#;
890
891 let _: i32 = self.evaluate_sync(check_idle_js).await.unwrap_or(0);
895
896 let mut idle_start: Option<std::time::Instant> = None;
897
898 loop {
899 let pending: i32 = self.evaluate_sync(check_idle_js).await.unwrap_or(0);
903
904 if pending == 0 {
905 match idle_start {
906 Some(start) if start.elapsed() >= idle_duration => {
907 return Ok(());
908 }
909 None => {
910 idle_start = Some(std::time::Instant::now());
911 }
912 _ => {}
913 }
914 } else {
915 idle_start = None;
916 }
917
918 if start.elapsed() > timeout {
919 tracing::warn!(
920 "wait_for_network_idle timed out after {}ms with {} pending request(s)",
921 timeout_ms,
922 pending
923 );
924 return Err(Error::Timeout(format!(
925 "Network not idle after {}ms ({} pending requests)",
926 timeout_ms, pending
927 )));
928 }
929
930 sleep_ms(INTERACTION_DELAY_MS).await;
931 }
932 }
933 pub async fn frames(&self) -> Result<Vec<FrameInfo>> {
935 let frame_tree = self.session.get_frame_tree().await?;
936 let mut frames = vec![FrameInfo {
937 id: frame_tree.frame.id.clone(),
938 url: frame_tree.frame.url.clone(),
939 name: frame_tree.frame.name.clone(),
940 }];
941
942 fn collect_frames(children: &[crate::cdp::types::FrameTree], frames: &mut Vec<FrameInfo>) {
943 for child in children {
944 frames.push(FrameInfo {
945 id: child.frame.id.clone(),
946 url: child.frame.url.clone(),
947 name: child.frame.name.clone(),
948 });
949 collect_frames(&child.child_frames, frames);
950 }
951 }
952
953 collect_frames(&frame_tree.child_frames, &mut frames);
954 Ok(frames)
955 }
956
957 pub async fn evaluate_in_frame<T: serde::de::DeserializeOwned>(
965 &self,
966 frame_selector: &str,
967 expression: &str,
968 ) -> Result<T> {
969 let escaped_frame = escape_js_string(frame_selector);
970 let escaped_expr = escape_js_string(expression);
971
972 let js = format!(
974 r#"
975 (() => {{
976 const iframe = document.querySelector('{escaped_frame}');
977 if (!iframe || !iframe.contentWindow) throw new Error('Frame not found: {escaped_frame}');
978 const _exec = new iframe.contentWindow.Function('return (' + '{escaped_expr}' + ')');
979 return _exec.call(iframe.contentWindow);
980 }})()
981 "#,
982 );
983
984 self.evaluate(&js).await
985 }
986 pub async fn with_retry<F, Fut, T>(
988 &self,
989 attempts: u32,
990 delay_ms: u64,
991 operation: F,
992 ) -> Result<T>
993 where
994 F: Fn() -> Fut,
995 Fut: std::future::Future<Output = Result<T>>,
996 {
997 let mut last_error = String::new();
998
999 for attempt in 1..=attempts {
1000 match operation().await {
1001 Ok(result) => return Ok(result),
1002 Err(e) => {
1003 last_error = e.to_string();
1004 if attempt < attempts {
1005 sleep_ms(delay_ms).await;
1006 }
1007 }
1008 }
1009 }
1010
1011 Err(Error::RetryExhausted {
1012 attempts,
1013 last_error,
1014 })
1015 }
1016 pub async fn debug_screenshot(&self, prefix: &str) -> Result<String> {
1021 let timestamp = std::time::SystemTime::now()
1022 .duration_since(std::time::UNIX_EPOCH)
1023 .unwrap_or_default()
1024 .as_millis();
1025
1026 let filename = match &self.config.debug_dir {
1027 Some(dir) => {
1028 std::fs::create_dir_all(dir)?;
1030 format!("{}/{}_{}.png", dir, prefix, timestamp)
1031 }
1032 None => format!("{}_{}.png", prefix, timestamp),
1033 };
1034
1035 let screenshot = self.screenshot().await?;
1036 std::fs::write(&filename, screenshot)?;
1037 Ok(filename)
1038 }
1039
1040 pub async fn debug_state(&self) -> Result<PageState> {
1042 let state: PageState = self
1043 .evaluate(
1044 r#"({
1045 url: location.href,
1046 title: document.title,
1047 input_count: document.querySelectorAll('input').length,
1048 button_count: document.querySelectorAll('button').length,
1049 link_count: document.querySelectorAll('a').length,
1050 form_count: document.querySelectorAll('form').length
1051 })"#,
1052 )
1053 .await
1054 .unwrap_or_else(|_| PageState {
1055 url: "unknown".to_string(),
1056 title: "unknown".to_string(),
1057 input_count: 0,
1058 button_count: 0,
1059 link_count: 0,
1060 form_count: 0,
1061 });
1062 Ok(state)
1063 }
1064
1065 pub async fn upload_file(&self, selector: &str, path: &str) -> Result<()> {
1067 self.upload_files(selector, &[path]).await
1068 }
1069
1070 pub async fn upload_files(&self, selector: &str, paths: &[&str]) -> Result<()> {
1072 let element = self.find(selector).await?;
1073 self.session
1074 .set_file_input_files(
1075 element.node_id,
1076 paths.iter().map(|p| p.to_string()).collect(),
1077 )
1078 .await
1079 }
1080
1081 pub async fn select(&self, selector: &str, value: &str) -> Result<()> {
1083 let (sel, val) = (escape_js_string(selector), escape_js_string(value));
1084 self.execute(&format!(
1085 r#"(()=>{{const el=document.querySelector('{sel}');if(!el)throw new Error('Select not found');const opt=[...el.options].find(o=>o.value==='{val}');if(!opt)throw new Error('Option not found: {val}');el.value='{val}';el.dispatchEvent(new Event('change',{{bubbles:true}}))}})()"#
1086 )).await
1087 }
1088
1089 pub async fn select_by_text(&self, selector: &str, text: &str) -> Result<()> {
1091 let (sel, txt) = (escape_js_string(selector), escape_js_string(text));
1092 self.execute(&format!(
1093 r#"(()=>{{const el=document.querySelector('{sel}');if(!el)throw new Error('Select not found');const opt=[...el.options].find(o=>o.text.trim()==='{txt}');if(!opt)throw new Error('Option not found: {txt}');el.value=opt.value;el.dispatchEvent(new Event('change',{{bubbles:true}}))}})()"#
1094 )).await
1095 }
1096
1097 pub async fn select_multiple(&self, selector: &str, values: &[&str]) -> Result<()> {
1099 let sel = escape_js_string(selector);
1100 let vals = serde_json::to_string(values).unwrap_or_else(|_| "[]".into());
1101 self.execute(&format!(
1102 r#"(()=>{{const el=document.querySelector('{sel}');if(!el)throw new Error('Select not found');const v={vals};for(const o of el.options)o.selected=v.includes(o.value);el.dispatchEvent(new Event('change',{{bubbles:true}}))}})()"#
1103 )).await
1104 }
1105
1106 pub async fn hover(&self, selector: &str) -> Result<()> {
1108 let (x, y) = self.find(selector).await?.center().await?;
1109 self.session
1110 .dispatch_mouse_event(MouseEventType::MouseMoved, x, y, None, None)
1111 .await
1112 }
1113
1114 pub async fn human_hover(&self, selector: &str) -> Result<()> {
1116 let element = self.find(selector).await?;
1117 element.scroll_into_view().await?;
1118 let (x, y) = element.center().await?;
1119 Human::new(&self.session).move_to(x, y).await?;
1120 sleep_ms(SETTLE_MS).await;
1121 Ok(())
1122 }
1123
1124 pub async fn press_key(&self, key: &str) -> Result<()> {
1126 use crate::cdp::types::{InputDispatchKeyEventFull, KeyEventType};
1127
1128 let (mods, key_name) = parse_key_combo(key);
1129 let (key_str, code_str, vk) = key_to_codes(key_name);
1130 let modifiers = if mods != 0 { Some(mods) } else { None };
1131
1132 let make_event = |event_type| InputDispatchKeyEventFull {
1133 r#type: event_type,
1134 modifiers,
1135 key: Some(key_str.into()),
1136 code: Some(code_str.into()),
1137 windows_virtual_key_code: vk,
1138 native_virtual_key_code: vk,
1139 ..Default::default()
1140 };
1141
1142 self.session
1143 .dispatch_key_event_full(make_event(KeyEventType::KeyDown))
1144 .await?;
1145 sleep_ms(INTERACTION_DELAY_MS).await;
1146 self.session
1147 .dispatch_key_event_full(make_event(KeyEventType::KeyUp))
1148 .await
1149 }
1150
1151 pub async fn select_all(&self) -> Result<()> {
1153 self.press_key(if cfg!(target_os = "macos") { "Cmd+A" } else { "Ctrl+A" }).await
1154 }
1155
1156 pub async fn copy(&self) -> Result<()> {
1158 self.press_key(if cfg!(target_os = "macos") { "Cmd+C" } else { "Ctrl+C" }).await
1159 }
1160
1161 pub async fn paste(&self) -> Result<()> {
1163 self.press_key(if cfg!(target_os = "macos") { "Cmd+V" } else { "Ctrl+V" }).await
1164 }
1165}
1166
1167fn parse_key_combo(combo: &str) -> (i32, &str) {
1168 use crate::cdp::types::modifiers;
1169 let parts: Vec<&str> = combo.split('+').collect();
1170 let mut mods = 0;
1171 let mut key = combo;
1172 for (i, part) in parts.iter().enumerate() {
1173 match part.to_lowercase().as_str() {
1174 "ctrl" | "control" => mods |= modifiers::CTRL,
1175 "alt" | "option" => mods |= modifiers::ALT,
1176 "shift" => mods |= modifiers::SHIFT,
1177 "cmd" | "meta" | "command" => mods |= modifiers::META,
1178 _ => key = parts[i],
1179 }
1180 }
1181 (mods, key)
1182}
1183
1184fn key_to_codes(key: &str) -> (&str, &str, Option<i32>) {
1185 static KEYS: &[(&str, &str, &str, i32)] = &[
1186 ("enter", "Enter", "Enter", 13),
1187 ("return", "Enter", "Enter", 13),
1188 ("tab", "Tab", "Tab", 9),
1189 ("escape", "Escape", "Escape", 27),
1190 ("esc", "Escape", "Escape", 27),
1191 ("backspace", "Backspace", "Backspace", 8),
1192 ("delete", "Delete", "Delete", 46),
1193 ("arrowup", "ArrowUp", "ArrowUp", 38),
1194 ("up", "ArrowUp", "ArrowUp", 38),
1195 ("arrowdown", "ArrowDown", "ArrowDown", 40),
1196 ("down", "ArrowDown", "ArrowDown", 40),
1197 ("arrowleft", "ArrowLeft", "ArrowLeft", 37),
1198 ("left", "ArrowLeft", "ArrowLeft", 37),
1199 ("arrowright", "ArrowRight", "ArrowRight", 39),
1200 ("right", "ArrowRight", "ArrowRight", 39),
1201 ("home", "Home", "Home", 36),
1202 ("end", "End", "End", 35),
1203 ("pageup", "PageUp", "PageUp", 33),
1204 ("pagedown", "PageDown", "PageDown", 34),
1205 ("space", " ", "Space", 32),
1206 ("a", "a", "KeyA", 65),
1207 ("b", "b", "KeyB", 66),
1208 ("c", "c", "KeyC", 67),
1209 ("d", "d", "KeyD", 68),
1210 ("e", "e", "KeyE", 69),
1211 ("f", "f", "KeyF", 70),
1212 ("g", "g", "KeyG", 71),
1213 ("h", "h", "KeyH", 72),
1214 ("i", "i", "KeyI", 73),
1215 ("j", "j", "KeyJ", 74),
1216 ("k", "k", "KeyK", 75),
1217 ("l", "l", "KeyL", 76),
1218 ("m", "m", "KeyM", 77),
1219 ("n", "n", "KeyN", 78),
1220 ("o", "o", "KeyO", 79),
1221 ("p", "p", "KeyP", 80),
1222 ("q", "q", "KeyQ", 81),
1223 ("r", "r", "KeyR", 82),
1224 ("s", "s", "KeyS", 83),
1225 ("t", "t", "KeyT", 84),
1226 ("u", "u", "KeyU", 85),
1227 ("v", "v", "KeyV", 86),
1228 ("w", "w", "KeyW", 87),
1229 ("x", "x", "KeyX", 88),
1230 ("y", "y", "KeyY", 89),
1231 ("z", "z", "KeyZ", 90),
1232 ("f1", "F1", "F1", 112),
1233 ("f2", "F2", "F2", 113),
1234 ("f3", "F3", "F3", 114),
1235 ("f4", "F4", "F4", 115),
1236 ("f5", "F5", "F5", 116),
1237 ("f6", "F6", "F6", 117),
1238 ("f7", "F7", "F7", 118),
1239 ("f8", "F8", "F8", 119),
1240 ("f9", "F9", "F9", 120),
1241 ("f10", "F10", "F10", 121),
1242 ("f11", "F11", "F11", 122),
1243 ("f12", "F12", "F12", 123),
1244 ];
1245 let lower = key.to_lowercase();
1246 KEYS.iter()
1247 .find(|(name, _, _, _)| *name == lower)
1248 .map(|(_, k, c, vk)| (*k, *c, Some(*vk)))
1249 .unwrap_or((key, key, None))
1250}
1251
1252#[derive(Debug, Clone)]
1254pub struct CapturedRequest {
1255 pub request_id: String,
1256 pub url: String,
1257 pub method: String,
1258 pub headers: HashMap<String, String>,
1259 pub post_data: Option<String>,
1260 pub resource_type: Option<String>,
1261 pub status: Option<i32>,
1262 pub status_text: Option<String>,
1263 pub response_headers: Option<HashMap<String, String>>,
1264 pub mime_type: Option<String>,
1265 pub timestamp: f64,
1266 pub complete: bool,
1267}
1268
1269#[derive(Debug)]
1271pub enum ResponseBody {
1272 Text(String),
1273 Binary(Vec<u8>),
1274}
1275
1276impl ResponseBody {
1277 pub fn as_text(&self) -> Option<&str> {
1279 match self {
1280 ResponseBody::Text(s) => Some(s),
1281 ResponseBody::Binary(_) => None,
1282 }
1283 }
1284
1285 pub fn as_bytes(&self) -> &[u8] {
1287 match self {
1288 ResponseBody::Text(s) => s.as_bytes(),
1289 ResponseBody::Binary(b) => b,
1290 }
1291 }
1292}
1293
1294#[derive(Debug, Clone)]
1296pub struct FrameInfo {
1297 pub id: String,
1299 pub url: String,
1301 pub name: Option<String>,
1303}
1304
1305#[derive(Debug, Clone, serde::Deserialize)]
1307pub struct PageState {
1308 pub url: String,
1309 pub title: String,
1310 pub input_count: u32,
1311 pub button_count: u32,
1312 pub link_count: u32,
1313 pub form_count: u32,
1314}
1315
1316#[derive(Debug, Clone, Copy)]
1318pub struct BoundingBox {
1319 pub x: f64,
1320 pub y: f64,
1321 pub width: f64,
1322 pub height: f64,
1323}
1324
1325impl BoundingBox {
1326 pub fn center(&self) -> (f64, f64) {
1327 (self.x + self.width / 2.0, self.y + self.height / 2.0)
1328 }
1329}
1330
1331pub struct Element<'a> {
1333 page: &'a Page,
1334 node_id: i32,
1335}
1336
1337impl<'a> Element<'a> {
1338 pub async fn center(&self) -> Result<(f64, f64)> {
1340 let model = self.page.session.get_box_model(self.node_id).await?;
1341 Ok(model.center())
1342 }
1343
1344 pub async fn click(&self) -> Result<()> {
1346 let (x, y) = self.center().await?;
1347 self.page.click_at(x, y).await
1348 }
1349
1350 pub async fn human_click(&self) -> Result<()> {
1352 let (x, y) = self.center().await?;
1353 self.page.human().move_and_click(x, y).await
1354 }
1355
1356 pub async fn outer_html(&self) -> Result<String> {
1358 self.page.session.get_outer_html(self.node_id).await
1359 }
1360
1361 pub async fn text(&self) -> Result<String> {
1365 self.eval_str("this.textContent || ''").await
1366 }
1367
1368 async fn eval_on_element(&self, js_body: &str) -> Result<serde_json::Value> {
1373 let object_id = self.page.session.resolve_node(self.node_id).await?;
1374 let func = format!("function() {{ return {}; }}", js_body);
1375 let result = self.page.session.call_function_on(&object_id, &func).await?;
1376 Ok(result.result.value.unwrap_or(serde_json::Value::Null))
1377 }
1378
1379 async fn eval_str(&self, js_body: &str) -> Result<String> {
1381 let value = self.eval_on_element(js_body).await?;
1382 Ok(value.as_str().unwrap_or("").to_string())
1383 }
1384
1385 async fn eval_bool(&self, js_body: &str, default: bool) -> Result<bool> {
1387 let value = self.eval_on_element(js_body).await?;
1388 Ok(value.as_bool().unwrap_or(default))
1389 }
1390
1391 pub async fn type_text(&self, text: &str) -> Result<()> {
1393 self.click().await?;
1394 sleep_ms(INTERACTION_DELAY_MS).await;
1395 self.page.session.insert_text(text).await
1396 }
1397
1398 pub async fn focus(&self) -> Result<()> {
1400 self.page.session.focus(self.node_id).await
1401 }
1402 pub async fn is_visible(&self) -> Result<bool> {
1404 match self.page.session.get_box_model(self.node_id).await {
1405 Ok(_) => Ok(true),
1406 Err(Error::Cdp { message, .. }) if message.contains("box model") => Ok(false),
1407 Err(e) => Err(e),
1408 }
1409 }
1410
1411 pub async fn bounding_box(&self) -> Option<BoundingBox> {
1415 match self.page.session.get_box_model(self.node_id).await {
1416 Ok(model) => {
1417 let content = &model.content;
1418 if content.len() >= 8 {
1419 let xs = [content[0], content[2], content[4], content[6]];
1422 let ys = [content[1], content[3], content[5], content[7]];
1423
1424 let min_x = xs.iter().copied().fold(f64::INFINITY, f64::min);
1425 let max_x = xs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1426 let min_y = ys.iter().copied().fold(f64::INFINITY, f64::min);
1427 let max_y = ys.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1428
1429 Some(BoundingBox {
1430 x: min_x,
1431 y: min_y,
1432 width: max_x - min_x,
1433 height: max_y - min_y,
1434 })
1435 } else {
1436 None
1437 }
1438 }
1439 Err(_) => None,
1440 }
1441 }
1442
1443 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
1445 let escaped_name = escape_js_string(name);
1446 let value = self
1447 .eval_on_element(&format!("this.getAttribute('{}')", escaped_name))
1448 .await?;
1449
1450 if value.is_null() {
1451 return Ok(None);
1452 }
1453 if let Some(s) = value.as_str() {
1454 return Ok(Some(s.to_string()));
1455 }
1456 Ok(None)
1457 }
1458
1459 pub async fn tag_name(&self) -> Result<String> {
1461 self.eval_str("this.tagName.toLowerCase()").await
1462 }
1463
1464 pub async fn is_enabled(&self) -> Result<bool> {
1466 self.eval_bool("!this.disabled", true).await
1467 }
1468
1469 pub async fn is_checked(&self) -> Result<bool> {
1471 self.eval_bool("this.checked === true", false).await
1472 }
1473
1474 pub async fn value(&self) -> Result<String> {
1476 self.eval_str("this.value || ''").await
1477 }
1478
1479 pub async fn css(&self, property: &str) -> Result<String> {
1481 let escaped = escape_js_string(property);
1482 self.eval_str(&format!(
1483 "getComputedStyle(this).getPropertyValue('{}')",
1484 escaped
1485 ))
1486 .await
1487 }
1488
1489 pub async fn scroll_into_view(&self) -> Result<()> {
1491 let object_id = self.page.session.resolve_node(self.node_id).await?;
1492 self.page
1493 .session
1494 .call_function_on(
1495 &object_id,
1496 "function() { this.scrollIntoView({ behavior: 'smooth', block: 'center' }); }",
1497 )
1498 .await?;
1499 Ok(())
1500 }
1501}
1502
1503#[cfg(test)]
1504mod tests {
1505 use super::*;
1506
1507 #[test]
1508 fn test_parse_key_combo_simple() {
1509 let (mods, key) = parse_key_combo("Enter");
1510 assert_eq!(mods, 0);
1511 assert_eq!(key, "Enter");
1512 }
1513
1514 #[test]
1515 fn test_parse_key_combo_ctrl() {
1516 use crate::cdp::types::modifiers;
1517 let (mods, key) = parse_key_combo("Ctrl+A");
1518 assert_eq!(mods, modifiers::CTRL);
1519 assert_eq!(key, "A");
1520 }
1521
1522 #[test]
1523 fn test_parse_key_combo_cmd_shift() {
1524 use crate::cdp::types::modifiers;
1525 let (mods, key) = parse_key_combo("Cmd+Shift+S");
1526 assert_eq!(mods, modifiers::META | modifiers::SHIFT);
1527 assert_eq!(key, "S");
1528 }
1529
1530 #[test]
1531 fn test_parse_key_combo_all_modifiers() {
1532 use crate::cdp::types::modifiers;
1533 let (mods, key) = parse_key_combo("Ctrl+Alt+Shift+Cmd+X");
1534 assert_eq!(
1535 mods,
1536 modifiers::CTRL | modifiers::ALT | modifiers::SHIFT | modifiers::META
1537 );
1538 assert_eq!(key, "X");
1539 }
1540
1541 #[test]
1542 fn test_parse_key_combo_case_insensitive() {
1543 use crate::cdp::types::modifiers;
1544 let (mods, key) = parse_key_combo("ctrl+a");
1545 assert_eq!(mods, modifiers::CTRL);
1546 assert_eq!(key, "a");
1547 }
1548
1549 #[test]
1550 fn test_key_to_codes_enter() {
1551 let (key, code, vk) = key_to_codes("Enter");
1552 assert_eq!(key, "Enter");
1553 assert_eq!(code, "Enter");
1554 assert_eq!(vk, Some(13));
1555 }
1556
1557 #[test]
1558 fn test_key_to_codes_tab() {
1559 let (key, code, vk) = key_to_codes("Tab");
1560 assert_eq!(key, "Tab");
1561 assert_eq!(code, "Tab");
1562 assert_eq!(vk, Some(9));
1563 }
1564
1565 #[test]
1566 fn test_key_to_codes_letter() {
1567 let (key, code, vk) = key_to_codes("a");
1568 assert_eq!(key, "a");
1569 assert_eq!(code, "KeyA");
1570 assert_eq!(vk, Some(65));
1571 }
1572
1573 #[test]
1574 fn test_key_to_codes_arrow() {
1575 let (key, code, vk) = key_to_codes("ArrowDown");
1576 assert_eq!(key, "ArrowDown");
1577 assert_eq!(code, "ArrowDown");
1578 assert_eq!(vk, Some(40));
1579 }
1580
1581 #[test]
1582 fn test_key_to_codes_case_insensitive() {
1583 let (key, code, vk) = key_to_codes("ESCAPE");
1584 assert_eq!(key, "Escape");
1585 assert_eq!(code, "Escape");
1586 assert_eq!(vk, Some(27));
1587 }
1588
1589 #[test]
1590 fn test_key_to_codes_alias() {
1591 let (key, code, vk) = key_to_codes("esc");
1593 assert_eq!(key, "Escape");
1594 assert_eq!(code, "Escape");
1595 assert_eq!(vk, Some(27));
1596
1597 let (key, code, vk) = key_to_codes("up");
1599 assert_eq!(key, "ArrowUp");
1600 assert_eq!(code, "ArrowUp");
1601 assert_eq!(vk, Some(38));
1602 }
1603
1604 #[test]
1605 fn test_key_to_codes_unknown() {
1606 let (key, code, vk) = key_to_codes("SomeWeirdKey");
1608 assert_eq!(key, "SomeWeirdKey");
1609 assert_eq!(code, "SomeWeirdKey");
1610 assert_eq!(vk, None);
1611 }
1612
1613 #[test]
1614 fn test_escape_js_string() {
1615 assert_eq!(escape_js_string("hello"), "hello");
1616 assert_eq!(escape_js_string("it's"), "it\\'s");
1617 assert_eq!(escape_js_string("line1\nline2"), "line1\\nline2");
1618 assert_eq!(escape_js_string("back\\slash"), "back\\\\slash");
1619 assert_eq!(escape_js_string("${var}"), "\\${var}");
1620 assert_eq!(escape_js_string("a\0b"), "a\\0b");
1622 assert_eq!(escape_js_string("a\u{2028}b"), "a\\u2028b");
1623 assert_eq!(escape_js_string("a\u{2029}b"), "a\\u2029b");
1624 }
1625}