Skip to main content

agent_first_http/sdk/fetch/
wait.rs

1//! Wait condition for browser-backed fetches (`architecture.md ยง5`).
2
3use crate::shared::error::{Error, ErrorCode};
4
5/// When to consider a page ready.
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub enum Wait {
8    /// CDP `Page.loadEventFired`. Default.
9    #[default]
10    Load,
11    /// CDP `Network.idle` (no requests for ~500 ms).
12    Idle,
13    /// A CSS selector matches `document.querySelector(...)`. Existence-only,
14    /// not visibility โ€” a node hidden by CSS or with zero dimensions still
15    /// satisfies the wait. Use [`Wait::SelectorVisible`] if you need the
16    /// node to actually paint.
17    Selector(String),
18    /// A CSS selector matches `document.querySelector(...)` AND the matched
19    /// node has a non-zero bounding box, `display != "none"` on every
20    /// ancestor (`offsetParent != null` on non-fixed elements), and
21    /// `visibility != "hidden"`. Catches the common framework pattern of
22    /// rendering the node into the DOM before the layout has painted it.
23    SelectorVisible(String),
24    /// A fixed wall-clock delay after navigation start.
25    Ms(u64),
26}
27
28impl Wait {
29    pub fn parse(s: &str) -> Result<Self, Error> {
30        if s == "load" {
31            Ok(Self::Load)
32        } else if s == "idle" {
33            Ok(Self::Idle)
34        } else if let Some(sel) = s.strip_prefix("selector-visible:") {
35            if sel.is_empty() {
36                Err(Error::new(
37                    ErrorCode::InvalidArgument,
38                    "--wait selector-visible: requires a non-empty CSS selector",
39                ))
40            } else {
41                Ok(Self::SelectorVisible(sel.to_string()))
42            }
43        } else if let Some(sel) = s.strip_prefix("selector:") {
44            if sel.is_empty() {
45                Err(Error::new(
46                    ErrorCode::InvalidArgument,
47                    "--wait selector: requires a non-empty CSS selector",
48                ))
49            } else {
50                Ok(Self::Selector(sel.to_string()))
51            }
52        } else if let Some(ms) = s.strip_prefix("ms:") {
53            let n: u64 = ms.parse().map_err(|_| {
54                Error::new(
55                    ErrorCode::InvalidArgument,
56                    format!("--wait ms: not a u64: {ms:?}"),
57                )
58            })?;
59            Ok(Self::Ms(n))
60        } else {
61            Err(Error::new(
62                ErrorCode::InvalidArgument,
63                format!(
64                    "--wait: unknown mode {s:?}; expected \
65                     load|idle|selector:<css>|selector-visible:<css>|ms:<n>"
66                ),
67            ))
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn parses_simple_modes() {
78        assert_eq!(Wait::parse("load").unwrap(), Wait::Load);
79        assert_eq!(Wait::parse("idle").unwrap(), Wait::Idle);
80        assert_eq!(
81            Wait::parse("selector:#root").unwrap(),
82            Wait::Selector("#root".into())
83        );
84        assert_eq!(
85            Wait::parse("selector-visible:#root").unwrap(),
86            Wait::SelectorVisible("#root".into())
87        );
88        assert_eq!(Wait::parse("ms:250").unwrap(), Wait::Ms(250));
89    }
90
91    #[test]
92    fn rejects_empty_selector() {
93        let err = Wait::parse("selector:").err();
94        assert_eq!(err.map(|e| e.error_code), Some(ErrorCode::InvalidArgument));
95        let err = Wait::parse("selector-visible:").err();
96        assert_eq!(err.map(|e| e.error_code), Some(ErrorCode::InvalidArgument));
97    }
98
99    #[test]
100    fn selector_visible_prefix_does_not_collide_with_selector() {
101        // The prefix check order matters: selector-visible: must be
102        // tested before selector: so we don't strip "selector:" and
103        // end up with "visible:#root" as the selector body.
104        let parsed = Wait::parse("selector-visible:.btn").unwrap();
105        assert_eq!(parsed, Wait::SelectorVisible(".btn".into()));
106    }
107
108    #[test]
109    fn rejects_bad_ms() {
110        let err = Wait::parse("ms:nope").err();
111        assert_eq!(err.map(|e| e.error_code), Some(ErrorCode::InvalidArgument));
112    }
113
114    #[test]
115    fn rejects_unknown_mode() {
116        let err = Wait::parse("forever").err();
117        assert_eq!(err.map(|e| e.error_code), Some(ErrorCode::InvalidArgument));
118    }
119}