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