1use crate::browser::Page;
12use anyhow::Result;
13use std::time::{Duration, Instant};
14
15pub const DEFAULT_FRAME_RETRY_INTERVAL: Duration = Duration::from_millis(100);
19
20pub const DEFAULT_FRAME_RETRY_TIMEOUT: Duration = Duration::from_secs(8);
24
25fn next_poll_sleep(now: Instant, deadline: Instant, interval: Duration) -> Option<Duration> {
31 if now >= deadline {
32 return None;
33 }
34 let remaining = deadline.saturating_duration_since(now);
35 Some(remaining.min(interval))
36}
37
38pub(crate) fn escape_js_string(s: &str) -> String {
50 let mut out = String::with_capacity(s.len());
51 for ch in s.chars() {
52 match ch {
53 '\\' => out.push_str("\\\\"),
54 '\'' => out.push_str("\\'"),
55 '"' => out.push_str("\\\""),
56 '\n' => out.push_str("\\n"),
57 '\r' => out.push_str("\\r"),
58 '\t' => out.push_str("\\t"),
59 '\0' => out.push_str("\\0"),
60 c => out.push(c),
61 }
62 }
63 out
64}
65
66fn lookup_iframe_offset(
72 iframe_offsets: &[(usize, String, String, f64, f64)],
73 url: &str,
74 iframe_idx: i64,
75) -> (f64, f64) {
76 if iframe_idx >= 0 {
77 iframe_offsets
78 .iter()
79 .find(|(idx, src, id, _, _)| *idx == iframe_idx as usize && (src == url || id == url))
80 .map(|(_, _, _, x, y)| (*x, *y))
81 } else {
82 iframe_offsets
83 .iter()
84 .find(|(_, src, id, _, _)| src == url || id == url)
85 .map(|(_, _, _, x, y)| (*x, *y))
86 }
87 .unwrap_or((0.0, 0.0))
88}
89
90pub async fn evaluate_in_all_frames<T>(page: &Page, expression: &str) -> Result<Vec<T>>
106where
107 T: serde::de::DeserializeOwned,
108{
109 let frame_ids = page.frames().await?;
110 let mut out = Vec::with_capacity(frame_ids.len());
111 for fid in frame_ids {
112 match page.evaluate_in_context(expression, &fid).await {
113 Ok(eval) => {
114 if let Ok(v) = eval.into_value::<T>() {
115 out.push(v);
116 }
117 }
118 Err(e) => {
119 tracing::debug!("frame {:?} disappeared during batch eval: {}", fid, e);
120 }
121 }
122 }
123 Ok(out)
124}
125
126pub async fn evaluate_in_frames_first<T, F>(
130 page: &Page,
131 expression: &str,
132 filter: F,
133 default: T,
134) -> Result<T>
135where
136 T: serde::de::DeserializeOwned + Clone,
137 F: Fn(&T) -> bool,
138{
139 let all = evaluate_in_all_frames::<T>(page, expression).await?;
140 Ok(all.into_iter().find(filter).unwrap_or(default))
141}
142
143async fn collect_iframe_offsets(
149 page: &Page,
150 frame: &crate::FrameId,
151) -> Result<Vec<(usize, String, String, f64, f64)>> {
152 let mut iframe_offsets: Vec<(usize, String, String, f64, f64)> = Vec::new();
153 let js = r#"
154 (function() {
155 const out = [];
156 const frames = document.querySelectorAll('iframe');
157 for (let i = 0; i < frames.length; i++) {
158 const f = frames[i];
159 const r = f.getBoundingClientRect();
160 out.push({ idx: i, src: f.src, id: f.id, x: r.left, y: r.top });
161 }
162 return out;
163 })()
164 "#;
165 let eval = page.evaluate_in_context(js, frame).await?;
166 if let Ok(vals) = eval.into_value::<Vec<serde_json::Value>>() {
167 for v in vals {
168 if let (Some(idx), Some(x), Some(y)) =
169 (v["idx"].as_u64(), v["x"].as_f64(), v["y"].as_f64())
170 {
171 let src = v["src"].as_str().unwrap_or("").to_string();
172 let id = v["id"].as_str().unwrap_or("").to_string();
173 iframe_offsets.push((idx as usize, src, id, x, y));
174 }
175 }
176 }
177 Ok(iframe_offsets)
178}
179
180async fn frame_self_index(page: &Page, frame: &crate::FrameId) -> i64 {
186 let js = r#"(function() {
187 try {
188 const fr = window.parent.frames;
189 for (let i = 0; i < fr.length; i++) { if (fr[i] === window) return i; }
190 } catch (e) {}
191 return -1;
192 })()"#;
193 page.evaluate_in_context(js, frame)
194 .await
195 .ok()
196 .and_then(|e| e.into_value::<i64>().ok())
197 .unwrap_or(-1)
198}
199
200async fn frame_viewport_offset(page: &Page, target: &crate::FrameId) -> Result<(f64, f64)> {
217 use std::collections::HashMap;
218
219 let tree = page.frame_tree().await?;
220 let by_id: HashMap<&str, &crate::browser::FrameTreeNode> =
221 tree.iter().map(|n| (n.id.inner().as_str(), n)).collect();
222
223 let mut chain: Vec<&crate::browser::FrameTreeNode> = Vec::new();
225 let mut cur = by_id.get(target.inner().as_str()).copied();
226 while let Some(node) = cur {
227 chain.push(node);
228 cur = node
229 .parent
230 .as_ref()
231 .and_then(|p| by_id.get(p.inner().as_str()).copied());
232 }
233
234 let mut ox = 0.0;
235 let mut oy = 0.0;
236 for i in 0..chain.len().saturating_sub(1) {
237 let child = chain[i];
238 let parent = chain[i + 1];
239 let kids = collect_iframe_offsets(page, &parent.id).await?;
240 let idx = frame_self_index(page, &child.id).await;
241
242 let off = kids
243 .iter()
244 .find(|(kidx, _, _, _, _)| idx >= 0 && *kidx == idx as usize)
245 .map(|(_, _, _, x, y)| (*x, *y))
246 .or_else(|| {
247 let m = lookup_iframe_offset(&kids, &child.url, -1);
249 (m != (0.0, 0.0)).then_some(m)
250 });
251
252 match off {
253 Some((x, y)) => {
254 ox += x;
255 oy += y;
256 }
257 None => tracing::warn!(
258 "frame_viewport_offset: unresolved iframe edge for {} (idx {idx}) within {}",
259 child.url,
260 parent.url
261 ),
262 }
263 }
264 Ok((ox, oy))
265}
266
267pub async fn find_element_centre_in_frames(
286 page: &Page,
287 selector: &str,
288) -> Result<Option<(f64, f64)>> {
289 let frame_ids = page.frames().await?;
290
291 let escaped = escape_js_string(selector);
292 let js = format!(
293 r#"(function() {{
294 const el = document.querySelector('{}');
295 if (!el) return null;
296 const r = el.getBoundingClientRect();
297 return {{ x: r.left + r.width / 2, y: r.top + r.height / 2 }};
298 }})()"#,
299 escaped
300 );
301
302 for fid in frame_ids {
303 match page.evaluate_in_context(&js, &fid).await {
304 Ok(eval) => {
305 if let Ok(val) = eval.into_value::<serde_json::Value>() {
306 if let (Some(x), Some(y)) = (val["x"].as_f64(), val["y"].as_f64()) {
307 let (offset_x, offset_y) = frame_viewport_offset(page, &fid).await?;
311 return Ok(Some((x + offset_x, y + offset_y)));
312 }
313 }
314 }
315 Err(e) => {
316 tracing::debug!("frame {:?} disappeared during element search: {}", fid, e);
317 }
318 }
319 }
320 Ok(None)
321}
322
323pub async fn find_element_centre_in_frames_retry(
342 page: &Page,
343 selector: &str,
344 timeout: Duration,
345 interval: Duration,
346) -> Result<Option<(f64, f64)>> {
347 let deadline = Instant::now() + timeout;
348 loop {
349 if let Some(centre) = find_element_centre_in_frames(page, selector).await? {
350 return Ok(Some(centre));
351 }
352 match next_poll_sleep(Instant::now(), deadline, interval) {
353 Some(d) => tokio::time::sleep(d).await,
354 None => return Ok(None),
355 }
356 }
357}
358
359#[derive(Debug, Clone, Copy, PartialEq)]
364pub struct FrameTile {
365 pub index: usize,
367 pub left: f64,
369 pub top: f64,
371 pub width: f64,
373 pub height: f64,
375}
376
377impl FrameTile {
378 pub fn centre(&self) -> (f64, f64) {
382 (self.left + self.width / 2.0, self.top + self.height / 2.0)
383 }
384}
385
386pub async fn find_tiles_in_frames(page: &Page, selector: &str) -> Result<Vec<FrameTile>> {
404 let frame_ids = page.frames().await?;
405
406 let escaped = escape_js_string(selector);
407 let js = format!(
408 r#"(function() {{
409 const els = document.querySelectorAll('{}');
410 if (!els || els.length === 0) return null;
411 const tiles = [];
412 for (let i = 0; i < els.length; i++) {{
413 const r = els[i].getBoundingClientRect();
414 tiles.push({{ index: i, left: r.left, top: r.top, width: r.width, height: r.height }});
415 }}
416 return {{ tiles: tiles }};
417 }})()"#,
418 escaped
419 );
420
421 for fid in frame_ids {
422 let eval = match page.evaluate_in_context(&js, &fid).await {
423 Ok(e) => e,
424 Err(e) => {
425 tracing::debug!("frame {:?} disappeared during tile search: {}", fid, e);
426 continue;
427 }
428 };
429 let Ok(val) = eval.into_value::<serde_json::Value>() else {
430 continue;
431 };
432 let Some(raw_tiles) = val["tiles"].as_array() else {
433 continue;
434 };
435 if raw_tiles.is_empty() {
436 continue;
437 }
438 let (offset_x, offset_y) = frame_viewport_offset(page, &fid).await?;
442 let mut out = Vec::with_capacity(raw_tiles.len());
443 for t in raw_tiles {
444 if let (Some(index), Some(left), Some(top), Some(width), Some(height)) = (
445 t["index"].as_u64(),
446 t["left"].as_f64(),
447 t["top"].as_f64(),
448 t["width"].as_f64(),
449 t["height"].as_f64(),
450 ) {
451 out.push(FrameTile {
452 index: index as usize,
453 left: left + offset_x,
454 top: top + offset_y,
455 width,
456 height,
457 });
458 }
459 }
460 if !out.is_empty() {
461 return Ok(out);
462 }
463 }
464 Ok(Vec::new())
465}
466
467pub async fn harvest_token_in_frames_retry(
475 page: &Page,
476 token_input_name: &str,
477 timeout: Duration,
478 interval: Duration,
479) -> Result<Option<String>> {
480 let deadline = Instant::now() + timeout;
481 loop {
482 if let Some(tok) = harvest_token_in_frames(page, token_input_name).await? {
483 return Ok(Some(tok));
484 }
485 match next_poll_sleep(Instant::now(), deadline, interval) {
486 Some(d) => tokio::time::sleep(d).await,
487 None => return Ok(None),
488 }
489 }
490}
491
492pub async fn find_iframe_rect_by_src(
498 page: &Page,
499 pattern: &str,
500) -> Result<Option<(f64, f64, f64, f64)>> {
501 let escaped = escape_js_string(pattern);
502 let js = format!(
503 r#"(() => {{
504 const frames = document.querySelectorAll('iframe');
505 for (const f of frames) {{
506 if (f.src && f.src.includes('{}')) {{
507 const r = f.getBoundingClientRect();
508 return {{ left: r.left, top: r.top, width: r.width, height: r.height }};
509 }}
510 }}
511 return null;
512 }})()"#,
513 escaped
514 );
515 let v = page.evaluate(js.as_str()).await?;
516 let val = v
517 .into_value::<serde_json::Value>()
518 .unwrap_or(serde_json::Value::Null);
519 if let (Some(l), Some(t), Some(w), Some(h)) = (
520 val["left"].as_f64(),
521 val["top"].as_f64(),
522 val["width"].as_f64(),
523 val["height"].as_f64(),
524 ) {
525 Ok(Some((l, t, w, h)))
526 } else {
527 Ok(None)
528 }
529}
530
531pub async fn verify_any_token_in_frames(page: &Page) -> Result<bool> {
554 const ANY_TOKEN_JS: &str = r#"(() => {
555 const sels = [
556 '[name="cf-turnstile-response"]',
557 '[name="g-recaptcha-response"]',
558 '#g-recaptcha-response',
559 '[name="h-captcha-response"]',
560 '[name="captchaToken"]',
561 '[name="frc-captcha-solution"]',
562 '[name="altcha"]',
563 '[name="mcaptcha__token"]',
564 '[name="cap_token"]',
565 ];
566 for (const sel of sels) {
567 try {
568 const els = document.querySelectorAll(sel);
569 for (const el of els) {
570 const v = (el.value || el.textContent || '').trim();
571 if (v) return true;
572 }
573 } catch (_) { /* keep going */ }
574 }
575 return false;
576 })()"#;
577 let results = evaluate_in_all_frames::<bool>(page, ANY_TOKEN_JS).await?;
578 Ok(results.into_iter().any(|v| v))
579}
580
581pub async fn verify_token_in_frames(page: &Page, token_input_name: &str) -> Result<bool> {
582 Ok(harvest_token_in_frames(page, token_input_name)
583 .await?
584 .is_some())
585}
586
587pub async fn harvest_token_in_frames(
598 page: &Page,
599 token_input_name: &str,
600) -> Result<Option<String>> {
601 let escaped = escape_js_string(token_input_name);
602 let js = format!(
605 r#"(() => {{
606 const els = document.querySelectorAll('input[name="{0}"], textarea[name="{0}"], #{0}');
607 for (const el of els) {{
608 const v = (el.value || el.textContent || '').trim();
609 if (v) return v;
610 }}
611 return null;
612 }})()"#,
613 escaped
614 );
615 let results = evaluate_in_all_frames::<Option<String>>(page, &js).await?;
616 Ok(results.into_iter().flatten().find(|v| !v.is_empty()))
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622
623 #[test]
624 fn escape_js_string_all_special_chars() {
625 let input = "\\'\"\n\r\t\0";
626 assert_eq!(escape_js_string(input), "\\\\\\\'\\\"\\n\\r\\t\\0");
627 }
628
629 #[test]
630 fn escape_js_string_backslash() {
631 assert_eq!(escape_js_string(r"\"), "\\\\");
632 }
633
634 #[test]
635 fn escape_js_string_single_quote() {
636 assert_eq!(escape_js_string("'"), "\\'");
637 }
638
639 #[test]
640 fn escape_js_string_double_quote() {
641 assert_eq!(escape_js_string("\""), "\\\"");
642 }
643
644 #[test]
645 fn escape_js_string_newline() {
646 assert_eq!(escape_js_string("a\nb"), "a\\nb");
647 }
648
649 #[test]
650 fn escape_js_string_carriage_return() {
651 assert_eq!(escape_js_string("a\rb"), "a\\rb");
652 }
653
654 #[test]
655 fn escape_js_string_tab() {
656 assert_eq!(escape_js_string("a\tb"), "a\\tb");
657 }
658
659 #[test]
660 fn escape_js_string_null_byte() {
661 assert_eq!(escape_js_string("a\0b"), "a\\0b");
662 }
663
664 #[test]
665 fn escape_js_string_mixed() {
666 let input = "line1\nline2\tcol\0end\\\"'";
667 assert_eq!(
668 escape_js_string(input),
669 "line1\\nline2\\tcol\\0end\\\\\\\"\\'"
670 );
671 }
672
673 #[test]
674 fn escape_js_string_no_special_chars() {
675 assert_eq!(escape_js_string("#simple-id"), "#simple-id");
676 }
677
678 #[test]
679 fn lookup_iframe_offset_by_index_and_url() {
680 let offsets = vec![
681 (0, "a.html".into(), "".into(), 10.0, 20.0),
682 (1, "b.html".into(), "".into(), 30.0, 40.0),
683 ];
684 assert_eq!(lookup_iframe_offset(&offsets, "a.html", 0), (10.0, 20.0));
685 assert_eq!(lookup_iframe_offset(&offsets, "b.html", 1), (30.0, 40.0));
686 }
687
688 #[test]
689 fn lookup_iframe_offset_fallback_when_index_missing() {
690 let offsets = vec![(0, "a.html".into(), "".into(), 10.0, 20.0)];
691 assert_eq!(lookup_iframe_offset(&offsets, "a.html", -1), (10.0, 20.0));
692 }
693
694 #[test]
695 fn lookup_iframe_offset_disambiguates_duplicate_src() {
696 let offsets = vec![
697 (0, "same.html".into(), "".into(), 10.0, 20.0),
698 (1, "same.html".into(), "".into(), 30.0, 40.0),
699 ];
700 assert_eq!(lookup_iframe_offset(&offsets, "same.html", 0), (10.0, 20.0));
702 assert_eq!(lookup_iframe_offset(&offsets, "same.html", 1), (30.0, 40.0));
703 assert_eq!(
705 lookup_iframe_offset(&offsets, "same.html", -1),
706 (10.0, 20.0)
707 );
708 }
709
710 #[test]
711 fn lookup_iframe_offset_empty_src_and_id() {
712 let offsets = vec![
713 (0, "".into(), "".into(), 5.0, 5.0),
714 (1, "".into(), "".into(), 15.0, 15.0),
715 ];
716 assert_eq!(lookup_iframe_offset(&offsets, "", 0), (5.0, 5.0));
717 assert_eq!(lookup_iframe_offset(&offsets, "", 1), (15.0, 15.0));
718 }
719
720 #[test]
721 fn lookup_iframe_offset_no_match() {
722 let offsets = vec![(0, "a.html".into(), "".into(), 10.0, 20.0)];
723 assert_eq!(
724 lookup_iframe_offset(&offsets, "missing.html", -1),
725 (0.0, 0.0)
726 );
727 }
728
729 #[test]
730 fn find_element_js_contains_query_selector() {
731 let selector = "#btn";
732 let escaped = escape_js_string(selector);
733 let js = format!(
734 r#"(function() {{ const el = document.querySelector('{}'); if (!el) return null; const r = el.getBoundingClientRect(); return {{ x: r.left + r.width / 2, y: r.top + r.height / 2, url: window.location.href }}; }})()"#,
735 escaped
736 );
737 assert!(js.contains("document.querySelector"));
738 assert!(js.contains("getBoundingClientRect"));
739 }
740
741 #[test]
742 fn frame_tile_centre_is_box_midpoint() {
743 let t = FrameTile {
744 index: 4,
745 left: 100.0,
746 top: 200.0,
747 width: 60.0,
748 height: 40.0,
749 };
750 assert_eq!(t.centre(), (130.0, 220.0));
751 }
752
753 #[test]
754 fn find_tiles_js_collects_all_matches_with_rects() {
755 let escaped = escape_js_string(".rc-imageselect-tile");
756 let js = format!(
757 r#"(function() {{
758 const els = document.querySelectorAll('{}');
759 if (!els || els.length === 0) return null;
760 const tiles = [];
761 for (let i = 0; i < els.length; i++) {{
762 const r = els[i].getBoundingClientRect();
763 tiles.push({{ index: i, left: r.left, top: r.top, width: r.width, height: r.height }});
764 }}
765 return {{ tiles: tiles }};
766 }})()"#,
767 escaped
768 );
769 assert!(js.contains("querySelectorAll"));
770 assert!(js.contains("getBoundingClientRect"));
771 assert!(js.contains("width: r.width"));
772 assert!(js.contains("index: i"));
773 }
774
775 #[test]
776 fn verify_token_js_contains_input_selector() {
777 let name = "g-recaptcha-response";
778 let escaped = escape_js_string(name);
779 let js = format!(
780 r#"!!document.querySelector('input[name="{}"][value]:not([value=""])')"#,
781 escaped
782 );
783 assert!(js.contains("input[name="));
784 assert!(js.contains("value]:not([value=\"\"])"));
785 }
786
787 #[test]
788 fn verify_token_escapes_quotes() {
789 let name = r#"token"value"#;
790 let escaped = escape_js_string(name);
791 assert!(escaped.contains("\\\""));
792 for (i, ch) in escaped.char_indices() {
793 if ch == '"' {
794 assert!(
795 i > 0 && escaped.as_bytes()[i - 1] == b'\\',
796 "quote at {} not escaped",
797 i
798 );
799 }
800 }
801 }
802
803 #[test]
804 fn next_poll_sleep_returns_interval_when_deadline_far() {
805 let now = Instant::now();
806 let deadline = now + Duration::from_secs(10);
807 let interval = Duration::from_millis(100);
808 let s = next_poll_sleep(now, deadline, interval).unwrap();
809 assert_eq!(s, Duration::from_millis(100));
810 }
811
812 #[test]
813 fn next_poll_sleep_clamps_to_remaining_when_close_to_deadline() {
814 let now = Instant::now();
815 let deadline = now + Duration::from_millis(40);
816 let interval = Duration::from_millis(100);
817 let s = next_poll_sleep(now, deadline, interval).unwrap();
818 assert!(s <= Duration::from_millis(40));
820 assert!(s >= Duration::from_millis(30));
821 }
822
823 #[test]
824 fn next_poll_sleep_returns_none_at_deadline() {
825 let now = Instant::now();
826 let deadline = now;
827 assert!(next_poll_sleep(now, deadline, Duration::from_millis(100)).is_none());
828 }
829
830 #[test]
831 fn next_poll_sleep_returns_none_past_deadline() {
832 let now = Instant::now();
833 let deadline = now - Duration::from_millis(1);
834 assert!(next_poll_sleep(now, deadline, Duration::from_millis(100)).is_none());
835 }
836
837 #[test]
838 fn next_poll_sleep_zero_interval_still_yields_zero_sleep() {
839 let now = Instant::now();
843 let deadline = now + Duration::from_millis(50);
844 let s = next_poll_sleep(now, deadline, Duration::ZERO).unwrap();
845 assert_eq!(s, Duration::ZERO);
846 }
847
848 #[test]
849 fn default_retry_constants_are_sane() {
850 assert!(DEFAULT_FRAME_RETRY_INTERVAL > Duration::ZERO);
854 assert!(DEFAULT_FRAME_RETRY_TIMEOUT > DEFAULT_FRAME_RETRY_INTERVAL);
855 let max_polls =
857 DEFAULT_FRAME_RETRY_TIMEOUT.as_millis() / DEFAULT_FRAME_RETRY_INTERVAL.as_millis() + 1;
858 assert!(
859 max_polls <= 200,
860 "default retry would issue {max_polls} CDP calls per attempt, too chatty",
861 );
862 }
863
864 #[test]
865 fn verify_token_escapes_null_and_newline() {
866 let name = "token\0value\n";
867 let escaped = escape_js_string(name);
868 assert!(escaped.contains("\\0"));
869 assert!(escaped.contains("\\n"));
870 assert!(!escaped.contains('\0'));
871 assert!(!escaped.contains('\n'));
872 }
873
874 #[test]
875 fn escape_js_string_empty() {
876 assert_eq!(escape_js_string(""), "");
877 }
878
879 #[test]
880 fn escape_js_string_unicode_untouched() {
881 let input = "emoji: 🎉 café ñ";
883 assert_eq!(escape_js_string(input), input);
884 }
885
886 #[test]
887 fn escape_js_string_preserves_length_hint() {
888 let input = "a".repeat(1000);
889 let out = escape_js_string(&input);
890 assert_eq!(out, input); }
892
893 #[test]
894 fn lookup_iframe_offset_matches_by_id() {
895 let offsets = vec![(0, "a.html".into(), "iframe-0".into(), 10.0, 20.0)];
896 assert_eq!(lookup_iframe_offset(&offsets, "iframe-0", -1), (10.0, 20.0));
897 }
898
899 #[test]
900 fn lookup_iframe_offset_index_mismatch_falls_back_to_first_match() {
901 let offsets = vec![
902 (0, "a.html".into(), "".into(), 10.0, 20.0),
903 (1, "b.html".into(), "".into(), 30.0, 40.0),
904 ];
905 assert_eq!(lookup_iframe_offset(&offsets, "a.html", 99), (0.0, 0.0));
908 assert_eq!(lookup_iframe_offset(&offsets, "a.html", -1), (10.0, 20.0));
909 }
910
911 #[test]
912 fn lookup_iframe_offset_negative_beyond_minus_one_treated_as_fallback() {
913 let offsets = vec![(0, "x".into(), "".into(), 5.0, 6.0)];
916 assert_eq!(lookup_iframe_offset(&offsets, "x", -5), (5.0, 6.0));
917 }
918
919 #[test]
920 fn next_poll_sleep_interval_larger_than_remaining() {
921 let now = Instant::now();
922 let deadline = now + Duration::from_millis(30);
923 let interval = Duration::from_millis(100);
924 let s = next_poll_sleep(now, deadline, interval).unwrap();
925 assert_eq!(s, Duration::from_millis(30));
926 }
927
928 #[test]
929 fn next_poll_sleep_very_small_remaining() {
930 let now = Instant::now();
931 let deadline = now + Duration::from_nanos(1);
932 let s = next_poll_sleep(now, deadline, Duration::from_millis(100)).unwrap();
933 assert_eq!(s, Duration::from_nanos(1));
934 }
935}