1use crate::protocol::cdp::{
2 types::{Event, JsUInt},
3 Browser,
4 Network::{CookieParam, DeleteCookies},
5 Page,
6 Page::PrintToPDF,
7 DOM::Node,
8};
9
10use serde::{Deserialize, Serialize};
11
12use serde_json::Value;
13
14pub type CallId = JsUInt;
15
16use thiserror::Error;
17
18use anyhow::Result;
19
20type JsInt = i32;
21
22#[derive(Deserialize, Debug, PartialEq, Clone, Error)]
23#[error("Method call error {}: {}", code, message)]
24pub struct RemoteError {
25 pub code: JsInt,
26 pub message: String,
27}
28
29#[derive(Deserialize, Debug, PartialEq, Clone)]
30pub struct Response {
31 #[serde(rename(deserialize = "id"))]
32 pub call_id: CallId,
33 pub result: Option<Value>,
34 pub error: Option<RemoteError>,
35}
36
37pub fn parse_response<T>(response: Response) -> Result<T>
38where
39 T: serde::de::DeserializeOwned + std::fmt::Debug,
40{
41 if let Some(error) = response.error {
42 return Err(error.into());
43 }
44
45 let result: T = serde_json::from_value(response.result.unwrap())?;
46
47 Ok(result)
48}
49
50#[derive(Deserialize, Debug, Clone)]
51#[serde(untagged)]
52#[allow(clippy::large_enum_variant)]
53pub enum Message {
54 Event(Event),
55 Response(Response),
56 ConnectionShutdown,
57}
58
59#[derive(Deserialize, Serialize, Debug)]
60pub struct TransferMode {
61 mode: String,
62}
63
64impl From<TransferMode> for Option<Page::PrintToPDFTransfer_modeOption> {
65 fn from(val: TransferMode) -> Self {
66 if val.mode == "base64" {
67 Some(Page::PrintToPDFTransfer_modeOption::ReturnAsBase64)
68 } else if val.mode == "stream" {
69 Some(Page::PrintToPDFTransfer_modeOption::ReturnAsStream)
70 } else {
71 None
72 }
73 }
74}
75
76#[derive(Deserialize, Serialize, Debug, Default)]
77#[serde(rename_all = "camelCase")]
78pub struct PrintToPdfOptions {
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub landscape: Option<bool>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub display_header_footer: Option<bool>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub print_background: Option<bool>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub scale: Option<f64>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub paper_width: Option<f64>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub paper_height: Option<f64>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub margin_top: Option<f64>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub margin_bottom: Option<f64>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub margin_left: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub margin_right: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub page_ranges: Option<String>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub ignore_invalid_page_ranges: Option<bool>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub header_template: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub footer_template: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub prefer_css_page_size: Option<bool>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub transfer_mode: Option<TransferMode>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub generate_document_outline: Option<bool>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub generate_tagged_pdf: Option<bool>,
115}
116
117pub fn parse_raw_message(raw_message: &str) -> Result<Message> {
118 Ok(serde_json::from_str::<Message>(raw_message)?)
119}
120
121#[derive(Clone, Debug)]
122pub enum Bounds {
123 Minimized,
124 Maximized,
125 Fullscreen,
126 Normal {
127 left: Option<JsUInt>,
129 top: Option<JsUInt>,
131 width: Option<f64>,
133 height: Option<f64>,
135 },
136}
137
138impl Bounds {
139 pub fn normal() -> Self {
141 Self::Normal {
142 left: None,
143 top: None,
144 width: None,
145 height: None,
146 }
147 }
148}
149
150impl From<CookieParam> for DeleteCookies {
151 fn from(v: CookieParam) -> Self {
152 Self {
153 name: v.name,
154 url: v.url,
155 domain: v.domain,
156 path: v.path,
157 partition_key: v.partition_key,
158 }
159 }
160}
161
162impl From<Bounds> for Browser::Bounds {
163 fn from(val: Bounds) -> Self {
164 match val {
165 Bounds::Minimized => Browser::Bounds {
166 left: None,
167 top: None,
168 width: None,
169 height: None,
170 window_state: Some(Browser::WindowState::Minimized),
171 },
172 Bounds::Maximized => Browser::Bounds {
173 left: None,
174 top: None,
175 width: None,
176 height: None,
177 window_state: Some(Browser::WindowState::Maximized),
178 },
179 Bounds::Fullscreen => Browser::Bounds {
180 left: None,
181 top: None,
182 width: None,
183 height: None,
184 window_state: Some(Browser::WindowState::Fullscreen),
185 },
186 Bounds::Normal {
187 left,
188 top,
189 width,
190 height,
191 } => Browser::Bounds {
192 left,
193 top,
194 width: width.map(|f| f as u32),
195 height: height.map(|f| f as u32),
196 window_state: Some(Browser::WindowState::Normal),
197 },
198 }
199 }
200}
201
202#[derive(Clone, Debug)]
203pub struct CurrentBounds {
204 pub left: JsUInt,
205 pub top: JsUInt,
206 pub width: f64,
207 pub height: f64,
208 pub state: Browser::WindowState,
209}
210
211impl From<Browser::Bounds> for CurrentBounds {
212 fn from(bounds: Browser::Bounds) -> Self {
213 Self {
214 left: bounds.left.unwrap(),
215 top: bounds.top.unwrap(),
216 width: f64::from(bounds.width.unwrap()),
217 height: f64::from(bounds.height.unwrap()),
218 state: bounds.window_state.unwrap(),
219 }
220 }
221}
222
223impl Default for PrintToPDF {
224 fn default() -> Self {
225 PrintToPDF {
226 display_header_footer: None,
227 footer_template: None,
228 generate_document_outline: None,
229 generate_tagged_pdf: None,
230 header_template: None,
231 landscape: None,
232 margin_bottom: None,
233 margin_left: None,
234 margin_right: None,
235 margin_top: None,
236 page_ranges: None,
237 paper_height: None,
238 paper_width: None,
239 prefer_css_page_size: None,
240 print_background: None,
241 scale: None,
242 transfer_mode: None,
243 }
244 }
245}
246
247struct SearchVisitor<'a, F> {
248 predicate: F,
249 item: Option<&'a Node>,
250}
251
252impl<'a, F: FnMut(&Node) -> bool> SearchVisitor<'a, F> {
253 fn new(predicate: F) -> Self {
254 SearchVisitor {
255 predicate,
256 item: None,
257 }
258 }
259
260 fn visit(&mut self, n: &'a Node) {
261 if (self.predicate)(n) {
262 self.item = Some(n);
263 } else if self.item.is_none() {
264 if let Some(c) = &n.children {
265 c.iter().for_each(|n| self.visit(n));
266 }
267 }
268 }
269}
270
271impl Node {
272 pub fn find<F: FnMut(&Self) -> bool>(&self, predicate: F) -> Option<&Self> {
276 let mut s = SearchVisitor::new(predicate);
277 s.visit(self);
278 s.item
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use log::trace;
285 use serde_json::json;
286
287 use super::*;
288
289 #[test]
290 fn pass_through_channel() {
291 env_logger::try_init().unwrap_or(());
292
293 let attached_to_target_json = json!({
294 "method": "Target.attachedToTarget",
295 "params": {
296 "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
297 "targetInfo": {
298 "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A",
299 "type": "page",
300 "title": "",
301 "url": "about:blank",
302 "attached": true,
303 "browserContextId": "946423F3D201EFA1A5FCF3462E340C15"
304 },
305 "waitingForDebugger": false
306 }
307 });
308
309 let _event: Message = serde_json::from_value(attached_to_target_json).unwrap();
310 }
311
312 #[test]
313 fn parse_event_fully() {
314 env_logger::try_init().unwrap_or(());
315
316 let attached_to_target_json = json!({
317 "method": "Target.attachedToTarget",
318 "params": {
319 "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
320 "targetInfo": {
321 "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A",
322 "type": "page",
323 "title": "",
324 "url": "about:blank",
325 "attached": true,
326 "browserContextId": "946423F3D201EFA1A5FCF3462E340C15"
327 },
328 "waitingForDebugger": false
329 }
330 });
331
332 if let Ok(Event::AttachedToTarget(_)) = serde_json::from_value(attached_to_target_json) {
333 } else {
334 panic!("Failed to parse event properly");
335 }
336
337 let received_target_msg_event = json!({
338 "method": "Target.receivedMessageFromTarget",
339 "params": {
340 "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
341 "message": "{\"id\":43473,\"result\":{\"data\":\"kDEgAABII=\"}}",
342 "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A"
343 }
344 });
345 let event: Event = serde_json::from_value(received_target_msg_event).unwrap();
346 match event {
347 Event::ReceivedMessageFromTarget(ev) => {
348 trace!("{:?}", ev);
349 }
350 _ => panic!("bad news"),
351 }
352 }
353
354 #[test]
355 fn easy_parse_messages() {
356 env_logger::try_init().unwrap_or(());
357
358 let example_message_strings = [
359 "{\"id\":1,\"result\":{\"browserContextIds\":[\"C2652EACAAA12B41038F1F2137C57A6E\"]}}",
361 "{\"id\":2,\"result\":{\"targetInfos\":[{\"targetId\":\"225A1B90036320AB4DB2E28F04AA6EE0\",\"type\":\"page\",\"title\":\"\",\"url\":\"about:blank\",\"attached\":false,\"browserContextId\":\"04FB807A65CFCA420C03E1134EB9214E\"}]}}",
362 "{\"id\":3,\"result\":{}}",
363 "{\"method\":\"Target.attachedToTarget\",\"params\":{\"sessionId\":\"8BEF122ABAB0C43B5729585A537F424A\",\"targetInfo\":{\"targetId\":\"26DEBCB2A45BEFC67A84012AC32C8B2A\",\"type\":\"page\",\"title\":\"\",\"url\":\"about:blank\",\"attached\":true,\"browserContextId\":\"946423F3D201EFA1A5FCF3462E340C15\"},\"waitingForDebugger\":false}}",
365 "{\"method\":\"Target.receivedMessageFromTarget\",\"params\":{\"sessionId\":\"8BEF122ABAB0C43B5729585A537F424A\",\"message\":\"{\\\"id\\\":43473,\\\"result\\\":{\\\"data\\\":\\\"iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAYAAACadoJwAAAMa0lEQVR4nO3XMQEAIAzAMMC/5+GiHCQK+nbPzCwAAIDAeR0AAAD8w4AAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABII=\\\"}}\",\"targetId\":\"26DEBCB2A45BEFC67A84012AC32C8B2A\"}}"
367 ];
368
369 for msg_string in &example_message_strings {
370 let _message: super::Message = parse_raw_message(msg_string).unwrap();
371 }
372 }
373}