Skip to main content

chromiumoxide/
navigate_deadline.rs

1//! Client binding for `Page.navigate` carrying a server-side navigation
2//! deadline.
3//!
4//! Some CDP servers accept an extra `timeout` key on `Page.navigate` params and
5//! abandon the navigation themselves once it elapses, so the caller gets a real
6//! navigate ack instead of a client-side abort with no protocol trace. The key
7//! is not part of the DevTools protocol, and stock Chrome ignores unknown
8//! params, so sending it is inert against an engine that does not implement it.
9//!
10//! Like the other vendor bindings in this crate (see
11//! [`crate::content_markdown`], or how `WebMCP.listTools` is sent), this is a
12//! hand-written [`Command`] implementation issued under a raw method string.
13//! Nothing is added to the PDL and nothing is regenerated, so
14//! [`NavigateParams`] keeps the exact shape and wire format it has always had
15//! and every existing caller is untouched.
16
17use std::time::Duration;
18
19use chromiumoxide_cdp::cdp::browser_protocol::page::{NavigateParams, NavigateReturns};
20use serde::Serialize;
21
22/// Standard `Page.navigate` params plus a `timeout` deadline in milliseconds.
23///
24/// Serializes as the flattened [`NavigateParams`] object with one extra
25/// `timeout` key, and is sent under the standard `Page.navigate` method string.
26#[derive(Debug, Clone, PartialEq, Serialize)]
27pub struct NavigateWithDeadlineParams {
28    #[serde(flatten)]
29    inner: NavigateParams,
30    /// Navigation deadline in milliseconds.
31    timeout: i64,
32}
33
34impl NavigateWithDeadlineParams {
35    /// Arm `params` with a navigation deadline.
36    ///
37    /// The deadline is sent as whole milliseconds. Durations too large for an
38    /// `i64` saturate at [`i64::MAX`] rather than wrapping, and the value is
39    /// never negative.
40    pub fn new(params: impl Into<NavigateParams>, navigation_timeout: Duration) -> Self {
41        Self {
42            inner: params.into(),
43            timeout: duration_as_millis_i64(navigation_timeout),
44        }
45    }
46
47    /// The standard navigate params this deadline is attached to.
48    pub fn params(&self) -> &NavigateParams {
49        &self.inner
50    }
51
52    /// The URL being navigated to.
53    pub fn url(&self) -> &str {
54        &self.inner.url
55    }
56
57    /// The deadline in milliseconds, as it goes on the wire.
58    pub fn timeout_millis(&self) -> i64 {
59        self.timeout
60    }
61
62    /// Drop the deadline and return the standard params.
63    pub fn into_params(self) -> NavigateParams {
64        self.inner
65    }
66}
67
68/// Whole milliseconds of `duration`, saturating at [`i64::MAX`].
69#[inline]
70fn duration_as_millis_i64(duration: Duration) -> i64 {
71    i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
72}
73
74impl chromiumoxide_types::Method for NavigateWithDeadlineParams {
75    fn identifier(&self) -> chromiumoxide_types::MethodId {
76        NavigateParams::IDENTIFIER.into()
77    }
78}
79
80impl chromiumoxide_types::MethodType for NavigateWithDeadlineParams {
81    fn method_id() -> chromiumoxide_types::MethodId
82    where
83        Self: Sized,
84    {
85        NavigateParams::IDENTIFIER.into()
86    }
87}
88
89impl chromiumoxide_types::Command for NavigateWithDeadlineParams {
90    type Response = NavigateReturns;
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use chromiumoxide_types::{Method, MethodType};
97
98    #[test]
99    fn method_string_is_page_navigate() {
100        let params = NavigateWithDeadlineParams::new(
101            NavigateParams::new("https://example.test/"),
102            Duration::from_secs(3),
103        );
104        assert_eq!(params.identifier().as_ref(), "Page.navigate");
105        assert_eq!(
106            NavigateWithDeadlineParams::method_id().as_ref(),
107            NavigateParams::IDENTIFIER
108        );
109    }
110
111    #[test]
112    fn timeout_is_integer_milliseconds() {
113        let params = NavigateWithDeadlineParams::new(
114            NavigateParams::new("https://example.test/"),
115            Duration::from_secs(3),
116        );
117        let value = serde_json::to_value(&params).expect("serialize");
118        assert_eq!(value["timeout"], serde_json::json!(3000));
119        assert_eq!(params.timeout_millis(), 3000);
120    }
121
122    #[test]
123    fn oversized_duration_saturates_instead_of_wrapping() {
124        let params = NavigateWithDeadlineParams::new(
125            NavigateParams::new("https://example.test/"),
126            Duration::from_secs(u64::MAX),
127        );
128        assert_eq!(params.timeout_millis(), i64::MAX);
129        let value = serde_json::to_value(&params).expect("serialize");
130        assert_eq!(value["timeout"], serde_json::json!(i64::MAX));
131        assert!(
132            value["timeout"].as_i64().is_some_and(|ms| ms > 0),
133            "deadline must never serialize as a negative value"
134        );
135    }
136
137    #[test]
138    fn zero_duration_is_zero_millis() {
139        let params = NavigateWithDeadlineParams::new(
140            NavigateParams::new("https://example.test/"),
141            Duration::ZERO,
142        );
143        assert_eq!(params.timeout_millis(), 0);
144    }
145
146    #[test]
147    fn flatten_keeps_every_standard_field_identical() {
148        let base = NavigateParams {
149            url: "https://example.test/page".into(),
150            referrer: Some("https://referrer.test/".into()),
151            transition_type: Some(
152                chromiumoxide_cdp::cdp::browser_protocol::page::TransitionType::Link,
153            ),
154            frame_id: Some(
155                chromiumoxide_cdp::cdp::browser_protocol::page::FrameId::from(
156                    "frame-abc".to_string(),
157                ),
158            ),
159            referrer_policy: Some(
160                chromiumoxide_cdp::cdp::browser_protocol::page::ReferrerPolicy::NoReferrer,
161            ),
162        };
163        let plain = serde_json::to_value(&base).expect("serialize plain");
164        let armed = serde_json::to_value(NavigateWithDeadlineParams::new(
165            base,
166            Duration::from_millis(1500),
167        ))
168        .expect("serialize armed");
169
170        let plain = plain.as_object().expect("plain object");
171        let armed_obj = armed.as_object().expect("armed object");
172
173        for (key, value) in plain {
174            assert_eq!(
175                armed_obj.get(key),
176                Some(value),
177                "key `{key}` diverged on the armed path"
178            );
179        }
180        let mut extra: Vec<&String> = armed_obj
181            .keys()
182            .filter(|k| !plain.contains_key(*k))
183            .collect();
184        extra.sort();
185        assert_eq!(extra, vec![&"timeout".to_string()]);
186    }
187
188    #[test]
189    fn plain_params_carry_no_timeout_key() {
190        let value =
191            serde_json::to_value(NavigateParams::new("https://example.test/")).expect("serialize");
192        let object = value.as_object().expect("object");
193        assert!(!object.contains_key("timeout"));
194        assert_eq!(object.keys().collect::<Vec<_>>(), vec!["url"]);
195    }
196}