Skip to main content

drission/cdp/
types.rs

1//! CDP 后端的后端无关**值类型**(与 Camoufox 后端同名同形,prelude 按 feature 出统一名):
2//! [`LoadMode`] / [`GetOptions`] / [`ImageFormat`] / [`ShotOpts`] / [`PageRect`] / [`Cookie`] /
3//! [`CookieParam`]。这些是纯数据 + builder,供 [`ChromiumTab`](crate::cdp::ChromiumTab) 用。
4
5use std::time::Duration;
6
7/// 页面加载等待模式(对齐 camoufox `LoadMode`)。
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum LoadMode {
10    /// 等待 `load` 事件(整页资源加载完成)。默认。
11    #[default]
12    Normal,
13    /// 等待 `DOMContentLoaded`(DOM 就绪即可)。
14    Eager,
15    /// 不等待,导航下发后立即返回。
16    None,
17}
18
19impl LoadMode {
20    /// 该模式要等待的事件名(CDP `Page.loadEventFired` / `Page.domContentEventFired`)。
21    pub(crate) fn cdp_event(self) -> Option<&'static str> {
22        match self {
23            LoadMode::Normal => Some("Page.loadEventFired"),
24            LoadMode::Eager => Some("Page.domContentEventFired"),
25            LoadMode::None => None,
26        }
27    }
28}
29
30/// [`ChromiumTab::get_with`](crate::cdp::ChromiumTab) 的可选参数(对齐 camoufox `GetOptions`)。
31#[derive(Debug, Clone)]
32pub struct GetOptions {
33    /// 失败重试次数(总尝试 = `retry + 1`)。默认 0。
34    pub retry: u32,
35    /// 重试间隔。默认 1s。
36    pub interval: Duration,
37    /// 本次导航超时;`None` 用标签默认超时。
38    pub timeout: Option<Duration>,
39    /// 本次加载模式;`None` 用 `Normal`。
40    pub load_mode: Option<LoadMode>,
41    /// 可选 referer。
42    pub referer: Option<String>,
43}
44
45impl Default for GetOptions {
46    fn default() -> Self {
47        Self {
48            retry: 0,
49            interval: Duration::from_secs(1),
50            timeout: None,
51            load_mode: None,
52            referer: None,
53        }
54    }
55}
56
57impl GetOptions {
58    pub fn new() -> Self {
59        Self::default()
60    }
61    pub fn retry(mut self, n: u32) -> Self {
62        self.retry = n;
63        self
64    }
65    pub fn interval(mut self, secs: f64) -> Self {
66        self.interval = Duration::from_secs_f64(secs.max(0.0));
67        self
68    }
69    pub fn timeout(mut self, d: Duration) -> Self {
70        self.timeout = Some(d);
71        self
72    }
73    pub fn load_mode(mut self, m: LoadMode) -> Self {
74        self.load_mode = Some(m);
75        self
76    }
77    pub fn referer(mut self, r: impl Into<String>) -> Self {
78        self.referer = Some(r.into());
79        self
80    }
81}
82
83/// 截图图片格式(对齐 camoufox `ImageFormat`)。
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum ImageFormat {
86    /// PNG(无损,默认)。
87    #[default]
88    Png,
89    /// JPEG(有损,可配合 `quality`)。
90    Jpeg,
91}
92
93impl ImageFormat {
94    /// 对应 `Page.captureScreenshot` 的 `format`。
95    pub(crate) fn cdp_format(self) -> &'static str {
96        match self {
97            ImageFormat::Png => "png",
98            ImageFormat::Jpeg => "jpeg",
99        }
100    }
101}
102
103/// 截图选项(对齐 camoufox `ShotOpts`)。
104#[derive(Debug, Clone, Default)]
105pub struct ShotOpts {
106    /// 是否整页截图(`true` 截整个文档,`false` 截可视视口)。
107    pub full_page: bool,
108    /// 指定矩形区域 `((left, top), (right, bottom))`(页面坐标,含滚动)。设置后忽略 `full_page`。
109    pub region: Option<((f64, f64), (f64, f64))>,
110    /// 图片格式(默认 PNG)。
111    pub format: ImageFormat,
112    /// JPEG 质量 0–100(仅 `Jpeg` 有效)。
113    pub quality: Option<u8>,
114}
115
116impl ShotOpts {
117    pub fn new() -> Self {
118        Self::default()
119    }
120    pub fn full_page(mut self, yes: bool) -> Self {
121        self.full_page = yes;
122        self
123    }
124    pub fn region(mut self, left_top: (f64, f64), right_bottom: (f64, f64)) -> Self {
125        self.region = Some((left_top, right_bottom));
126        self
127    }
128    pub fn format(mut self, format: ImageFormat) -> Self {
129        self.format = format;
130        self
131    }
132    pub fn quality(mut self, q: u8) -> Self {
133        self.quality = Some(q);
134        self
135    }
136}
137
138/// 页面尺寸/滚动信息(由 [`ChromiumTab::rect`](crate::cdp::ChromiumTab) 返回,对齐 camoufox `PageRect`)。
139#[derive(Debug, Clone, Copy, PartialEq)]
140pub struct PageRect {
141    /// 可视视口宽(`innerWidth`)。
142    pub window_width: f64,
143    /// 可视视口高(`innerHeight`)。
144    pub window_height: f64,
145    /// 整个文档内容宽(`scrollWidth`)。
146    pub page_width: f64,
147    /// 整个文档内容高(`scrollHeight`)。
148    pub page_height: f64,
149    /// 横向滚动位置(`scrollX`)。
150    pub scroll_x: f64,
151    /// 纵向滚动位置(`scrollY`)。
152    pub scroll_y: f64,
153    /// 设备像素比(`devicePixelRatio`)。
154    pub device_pixel_ratio: f64,
155}
156
157/// 一条 cookie(由 [`ChromiumTab::cookies`](crate::cdp::ChromiumTab) 返回,对齐 camoufox `Cookie`)。
158#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
159#[serde(default)]
160pub struct Cookie {
161    pub name: String,
162    pub value: String,
163    pub domain: String,
164    pub path: String,
165    pub expires: f64,
166    pub http_only: bool,
167    pub secure: bool,
168}
169
170/// 设置 cookie 的参数(对齐 camoufox `CookieParam`)。至少需要 `name`/`value`,及 `url` 或 `domain` 其一。
171#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
172#[serde(default)]
173pub struct CookieParam {
174    pub name: String,
175    pub value: String,
176    pub url: Option<String>,
177    pub domain: Option<String>,
178    pub path: Option<String>,
179    pub secure: Option<bool>,
180    pub http_only: Option<bool>,
181    pub expires: Option<f64>,
182}
183
184/// JS 对话框信息(由 [`ChromiumTab::handle_next_dialog`](crate::cdp::ChromiumTab) 返回,对齐 camoufox `DialogInfo`)。
185#[derive(Debug, Clone, Default)]
186pub struct DialogInfo {
187    /// 对话框文本。
188    pub message: String,
189    /// 类型(alert/confirm/prompt/beforeunload)。
190    pub dialog_type: String,
191    /// prompt 的默认值。
192    pub default_prompt: String,
193}
194
195/// 一次下载的信息(由 [`ChromiumTab::wait_download`](crate::cdp::ChromiumTab) 返回,对齐 camoufox `DownloadInfo`)。
196#[derive(Debug, Clone, Default)]
197pub struct DownloadInfo {
198    /// 下载来源 URL。
199    pub url: String,
200    /// 建议文件名。
201    pub suggested_filename: String,
202    /// 落盘完整路径(若已知下载目录)。
203    pub path: String,
204    /// 状态(`completed`/`canceled`/`inProgress`)。
205    pub state: String,
206}
207
208impl CookieParam {
209    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
210        Self {
211            name: name.into(),
212            value: value.into(),
213            ..Default::default()
214        }
215    }
216    pub fn url(mut self, url: impl Into<String>) -> Self {
217        self.url = Some(url.into());
218        self
219    }
220    pub fn domain(mut self, domain: impl Into<String>) -> Self {
221        self.domain = Some(domain.into());
222        self
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn load_mode_maps_to_cdp_wait_event() {
232        assert_eq!(LoadMode::Normal.cdp_event(), Some("Page.loadEventFired"));
233        assert_eq!(
234            LoadMode::Eager.cdp_event(),
235            Some("Page.domContentEventFired")
236        );
237        assert_eq!(LoadMode::None.cdp_event(), None);
238        assert_eq!(LoadMode::default(), LoadMode::Normal);
239    }
240
241    #[test]
242    fn image_format_maps_to_cdp_format() {
243        assert_eq!(ImageFormat::Png.cdp_format(), "png");
244        assert_eq!(ImageFormat::Jpeg.cdp_format(), "jpeg");
245        assert_eq!(ImageFormat::default(), ImageFormat::Png);
246    }
247
248    #[test]
249    fn get_options_builder_sets_fields() {
250        let o = GetOptions::new()
251            .retry(3)
252            .interval(2.5)
253            .timeout(Duration::from_secs(9))
254            .load_mode(LoadMode::Eager)
255            .referer("https://ref.example");
256        assert_eq!(o.retry, 3);
257        assert_eq!(o.interval, Duration::from_secs_f64(2.5));
258        assert_eq!(o.timeout, Some(Duration::from_secs(9)));
259        assert_eq!(o.load_mode, Some(LoadMode::Eager));
260        assert_eq!(o.referer.as_deref(), Some("https://ref.example"));
261    }
262
263    #[test]
264    fn get_options_default_and_negative_interval_clamps() {
265        let d = GetOptions::default();
266        assert_eq!(d.retry, 0);
267        assert_eq!(d.interval, Duration::from_secs(1));
268        assert!(d.timeout.is_none() && d.load_mode.is_none() && d.referer.is_none());
269        // 负间隔被夹到 0,不会 panic 于 Duration::from_secs_f64。
270        assert_eq!(GetOptions::new().interval(-5.0).interval, Duration::ZERO);
271    }
272
273    #[test]
274    fn shot_opts_builder_sets_fields() {
275        let s = ShotOpts::new()
276            .full_page(true)
277            .format(ImageFormat::Jpeg)
278            .quality(80);
279        assert!(s.full_page);
280        assert_eq!(s.format, ImageFormat::Jpeg);
281        assert_eq!(s.quality, Some(80));
282        assert!(s.region.is_none());
283
284        let r = ShotOpts::new().region((1.0, 2.0), (3.0, 4.0));
285        assert_eq!(r.region, Some(((1.0, 2.0), (3.0, 4.0))));
286    }
287
288    #[test]
289    fn cookie_param_builder_sets_identity_fields() {
290        let c = CookieParam::new("sid", "abc")
291            .url("https://x.example")
292            .domain(".example.com");
293        assert_eq!(c.name, "sid");
294        assert_eq!(c.value, "abc");
295        assert_eq!(c.url.as_deref(), Some("https://x.example"));
296        assert_eq!(c.domain.as_deref(), Some(".example.com"));
297        // 未设置的可选字段保持 None。
298        assert!(c.path.is_none() && c.secure.is_none() && c.expires.is_none());
299    }
300}