1use serde_json::{json, Value};
6
7use crate::EventSender;
8
9#[derive(Debug, Clone, PartialEq)]
23pub enum BaoEvent {
24 FetchRequestPaused {
25 request_id: String,
26 url: String,
27 method: String,
28 headers: Value,
29 post_data: Option<String>,
30 resource_type: String,
31 },
32 NetworkRequestWillBeSent {
33 request_id: String,
34 url: String,
35 method: String,
36 headers: Value,
37 request: Value,
38 timestamp: f64,
39 resource_type: String,
40 },
41 NetworkResponseReceived {
42 request_id: String,
43 url: String,
44 status: i32,
45 status_text: String,
46 headers: Value,
47 timestamp: f64,
48 resource_type: String,
49 },
50 NetworkLoadingFailed {
51 request_id: String,
52 resource_type: String,
53 error_text: String,
54 timestamp: f64,
55 },
56 DebuggerScriptParsed {
57 script_id: String,
58 url: String,
59 start_line: i32,
60 end_line: i32,
61 },
62 DebuggerPaused {
63 call_frames: Value,
64 reason: String,
65 hit_breakpoints: Value,
66 },
67 RuntimeExceptionThrown {
68 timestamp: f64,
69 text: String,
70 url: String,
71 line: i32,
72 column: i32,
73 stack_trace: Value,
74 },
75 PageLoadEventFired {
76 timestamp: f64,
77 },
78 PageFrameNavigated {
79 frame_id: String,
80 url: String,
81 loader_id: String,
82 },
83 SecurityCertificateError {
84 event_id: i32,
85 error_type: String,
86 url: String,
87 },
88}
89
90#[derive(Debug, Clone, PartialEq)]
98pub enum ConsoleMessage {
99 Log { level: String, text: String },
100 Event(BaoEvent),
101}
102
103impl BaoEvent {
119 pub fn from_console_text(text: &str) -> Option<ConsoleMessage> {
120 let rest = match text.strip_prefix("__BAO_EVT__") {
121 Some(r) => r,
122 None => return None,
123 };
124
125 let (method, json_str): (_, String) = match rest.split_once('\n') {
126 Some((m, j)) => (m, j.to_string()),
127 None => {
128 match rest.split_once('{') {
131 Some((m, j_rest)) => (m, format!("{{{j_rest}")),
132 None => return None,
133 }
134 }
135 };
136
137 let v: Value = serde_json::from_str(&json_str).unwrap_or_default();
138
139 match method {
140 "Fetch.requestPaused" => Some(ConsoleMessage::Event(BaoEvent::FetchRequestPaused {
141 request_id: v["id"].as_str().unwrap_or_default().to_string(),
142 url: v["url"].as_str().unwrap_or_default().to_string(),
143 method: v["method"].as_str().unwrap_or_default().to_string(),
144 headers: v.get("headers").cloned().unwrap_or(json!({})),
145 post_data: v.get("postData").and_then(|p| p.as_str()).map(String::from),
146 resource_type: v
147 .get("resourceType")
148 .and_then(|r| r.as_str())
149 .unwrap_or("Other")
150 .to_string(),
151 })),
152 "Network.requestWillBeSent" => {
153 Some(ConsoleMessage::Event(BaoEvent::NetworkRequestWillBeSent {
154 request_id: v["id"].as_str().unwrap_or_default().to_string(),
155 url: v["url"].as_str().unwrap_or_default().to_string(),
156 method: v["method"].as_str().unwrap_or_default().to_string(),
157 headers: v.get("headers").cloned().unwrap_or(json!({})),
158 request: v.get("request").cloned().unwrap_or_else(|| {
159 json!({
160 "url": v["url"],
161 "method": v["method"],
162 })
163 }),
164 timestamp: v.get("timestamp").and_then(|t| t.as_f64()).unwrap_or(0.0),
165 resource_type: v
166 .get("type")
167 .and_then(|r| r.as_str())
168 .unwrap_or("Other")
169 .to_string(),
170 }))
171 }
172 "Network.responseReceived" => {
173 Some(ConsoleMessage::Event(BaoEvent::NetworkResponseReceived {
174 request_id: v["id"].as_str().unwrap_or_default().to_string(),
175 url: v["url"].as_str().unwrap_or_default().to_string(),
176 status: v["status"].as_i64().unwrap_or(0) as i32,
177 status_text: v
178 .get("statusText")
179 .and_then(|s| s.as_str())
180 .unwrap_or_default()
181 .to_string(),
182 headers: v.get("headers").cloned().unwrap_or(json!({})),
183 timestamp: v.get("timestamp").and_then(|t| t.as_f64()).unwrap_or(0.0),
184 resource_type: v
185 .get("type")
186 .and_then(|r| r.as_str())
187 .unwrap_or("Other")
188 .to_string(),
189 }))
190 }
191 "Network.loadingFailed" => {
192 Some(ConsoleMessage::Event(BaoEvent::NetworkLoadingFailed {
193 request_id: v["id"].as_str().unwrap_or_default().to_string(),
194 resource_type: v
195 .get("type")
196 .and_then(|r| r.as_str())
197 .unwrap_or("Other")
198 .to_string(),
199 error_text: v
200 .get("errorText")
201 .and_then(|e| e.as_str())
202 .unwrap_or("Network error")
203 .to_string(),
204 timestamp: v.get("timestamp").and_then(|t| t.as_f64()).unwrap_or(0.0),
205 }))
206 }
207 "Debugger.scriptParsed" => {
208 Some(ConsoleMessage::Event(BaoEvent::DebuggerScriptParsed {
209 script_id: v["id"].as_str().unwrap_or_default().to_string(),
210 url: v["url"].as_str().unwrap_or_default().to_string(),
211 start_line: v.get("startLine").and_then(|l| l.as_i64()).unwrap_or(0) as i32,
212 end_line: v.get("endLine").and_then(|l| l.as_i64()).unwrap_or(0) as i32,
213 }))
214 }
215 "Debugger.paused" => Some(ConsoleMessage::Event(BaoEvent::DebuggerPaused {
216 call_frames: v.get("callFrames").cloned().unwrap_or(json!([])),
217 reason: v
218 .get("reason")
219 .and_then(|r| r.as_str())
220 .unwrap_or("other")
221 .to_string(),
222 hit_breakpoints: v.get("hitBreakpoints").cloned().unwrap_or(json!([])),
223 })),
224 "Runtime.exceptionThrown" => {
225 Some(ConsoleMessage::Event(BaoEvent::RuntimeExceptionThrown {
226 timestamp: v.get("timestamp").and_then(|t| t.as_f64()).unwrap_or(0.0),
227 text: v
228 .get("text")
229 .and_then(|t| t.as_str())
230 .unwrap_or_default()
231 .to_string(),
232 url: v
233 .get("url")
234 .and_then(|u| u.as_str())
235 .unwrap_or_default()
236 .to_string(),
237 line: v.get("line").and_then(|l| l.as_i64()).unwrap_or(0) as i32,
238 column: v.get("column").and_then(|c| c.as_i64()).unwrap_or(0) as i32,
239 stack_trace: v.get("stackTrace").cloned().unwrap_or(Value::Null),
240 }))
241 }
242 "Page.loadEventFired" => Some(ConsoleMessage::Event(BaoEvent::PageLoadEventFired {
243 timestamp: v.get("timestamp").and_then(|t| t.as_f64()).unwrap_or(0.0),
244 })),
245 "Page.frameNavigated" => Some(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
246 frame_id: v["frameId"].as_str().unwrap_or("0").to_string(),
247 url: v["url"].as_str().unwrap_or_default().to_string(),
248 loader_id: v["loaderId"].as_str().unwrap_or_default().to_string(),
249 })),
250 "Security.certificateError" => {
251 Some(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
252 event_id: v["eventId"].as_i64().unwrap_or(0) as i32,
253 error_type: v["errorType"].as_str().unwrap_or_default().to_string(),
254 url: v["url"].as_str().unwrap_or_default().to_string(),
255 }))
256 }
257 _ => None,
258 }
259 }
260}
261
262impl BaoEvent {
270 pub fn broadcast(&self, sender: &dyn EventSender) {
271 match self {
272 BaoEvent::FetchRequestPaused {
273 request_id,
274 url,
275 method,
276 headers,
277 post_data: _,
278 resource_type,
279 } => {
280 sender.send_event(
281 "Fetch.requestPaused",
282 serde_json::json!({
283 "requestId": request_id,
284 "request": {
285 "url": url,
286 "method": method,
287 "headers": headers,
288 },
289 "resourceType": resource_type,
290 "networkStage": "Request",
291 }),
292 );
293 }
294 BaoEvent::NetworkRequestWillBeSent {
295 request_id,
296 request,
297 timestamp,
298 resource_type,
299 ..
300 } => {
301 sender.send_event(
302 "Network.requestWillBeSent",
303 serde_json::json!({
304 "requestId": request_id,
305 "request": request,
306 "timestamp": *timestamp,
307 "type": resource_type,
308 }),
309 );
310 }
311 BaoEvent::NetworkResponseReceived {
312 request_id,
313 url,
314 status,
315 status_text,
316 headers,
317 timestamp,
318 resource_type,
319 } => {
320 sender.send_event(
321 "Network.responseReceived",
322 serde_json::json!({
323 "requestId": request_id,
324 "response": {
325 "url": url,
326 "status": *status,
327 "statusText": status_text,
328 "headers": headers,
329 },
330 "timestamp": *timestamp,
331 "type": resource_type,
332 }),
333 );
334 sender.send_event(
335 "Network.loadingFinished",
336 serde_json::json!({
337 "requestId": request_id,
338 "timestamp": *timestamp,
339 }),
340 );
341 }
342 BaoEvent::NetworkLoadingFailed {
343 request_id,
344 resource_type,
345 error_text,
346 timestamp,
347 } => {
348 sender.send_event(
349 "Network.loadingFailed",
350 serde_json::json!({
351 "requestId": request_id,
352 "type": resource_type,
353 "errorText": error_text,
354 "timestamp": *timestamp,
355 }),
356 );
357 }
358 BaoEvent::DebuggerScriptParsed {
359 script_id,
360 url,
361 start_line,
362 end_line,
363 } => {
364 sender.send_event(
365 "Debugger.scriptParsed",
366 serde_json::json!({
367 "scriptId": script_id,
368 "url": url,
369 "startLine": *start_line,
370 "endLine": *end_line,
371 }),
372 );
373 }
374 BaoEvent::DebuggerPaused {
375 call_frames,
376 reason,
377 hit_breakpoints,
378 } => {
379 sender.send_event(
380 "Debugger.paused",
381 serde_json::json!({
382 "callFrames": call_frames,
383 "reason": reason,
384 "hitBreakpoints": hit_breakpoints,
385 }),
386 );
387 }
388 BaoEvent::RuntimeExceptionThrown {
389 timestamp,
390 text,
391 url,
392 line,
393 column,
394 stack_trace,
395 } => {
396 sender.send_event(
397 "Runtime.exceptionThrown",
398 serde_json::json!({
399 "timestamp": *timestamp,
400 "exceptionDetails": {
401 "text": text,
402 "url": url,
403 "lineNumber": *line,
404 "columnNumber": *column,
405 "stackTrace": stack_trace,
406 },
407 }),
408 );
409 }
410 BaoEvent::PageLoadEventFired { timestamp } => {
411 sender.send_event(
412 "Page.loadEventFired",
413 serde_json::json!({
414 "timestamp": *timestamp,
415 }),
416 );
417 }
418 BaoEvent::PageFrameNavigated {
419 frame_id,
420 url,
421 loader_id,
422 } => {
423 sender.send_event(
424 "Page.frameNavigated",
425 serde_json::json!({
426 "frame": {
427 "id": frame_id,
428 "url": url,
429 "loaderId": loader_id,
430 "mimeType": "text/html",
431 "securityOrigin": "",
432 "secureContextType": "Secure",
433 },
434 }),
435 );
436 }
437 BaoEvent::SecurityCertificateError {
438 event_id,
439 error_type,
440 url,
441 } => {
442 sender.send_event(
443 "Security.certificateError",
444 serde_json::json!({
445 "eventId": event_id,
446 "errorType": error_type,
447 "resourceUrl": url,
448 }),
449 );
450 }
451 }
452 }
453}
454
455#[cfg(test)]
460mod tests {
461 use super::*;
462 use std::cell::RefCell;
463
464 struct RecordingSender {
466 events: RefCell<Vec<(String, Value)>>,
467 }
468
469 impl RecordingSender {
470 fn new() -> Self {
471 RecordingSender {
472 events: RefCell::new(Vec::new()),
473 }
474 }
475
476 fn events(&self) -> Vec<(String, Value)> {
477 self.events.borrow().clone()
478 }
479 }
480
481 impl EventSender for RecordingSender {
482 fn send_event(&self, method: &str, params: Value) {
483 self.events.borrow_mut().push((method.to_string(), params));
484 }
485 }
486
487 unsafe impl Send for RecordingSender {}
490 unsafe impl Sync for RecordingSender {}
491
492 #[test]
495 fn from_console_text_fetch_request_paused() {
496 let input = "__BAO_EVT__Fetch.requestPaused\n{\"id\":\"r1\",\"url\":\"http://test.com\",\"method\":\"GET\",\"headers\":{\"X-Custom\":\"val\"},\"resourceType\":\"Document\"}";
497 let msg = BaoEvent::from_console_text(input).expect("should parse");
498 match msg {
499 ConsoleMessage::Event(BaoEvent::FetchRequestPaused {
500 request_id,
501 url,
502 method,
503 headers,
504 post_data,
505 resource_type,
506 }) => {
507 assert_eq!(request_id, "r1");
508 assert_eq!(url, "http://test.com");
509 assert_eq!(method, "GET");
510 assert_eq!(headers["X-Custom"], "val");
511 assert!(post_data.is_none());
512 assert_eq!(resource_type, "Document");
513 }
514 other => panic!("expected FetchRequestPaused, got {:?}", other),
515 }
516 }
517
518 #[test]
519 fn from_console_text_fetch_request_paused_with_post_data() {
520 let input = "__BAO_EVT__Fetch.requestPaused\n{\"id\":\"r2\",\"url\":\"http://test.com\",\"method\":\"POST\",\"headers\":{},\"postData\":\"body\",\"resourceType\":\"XHR\"}";
521 let msg = BaoEvent::from_console_text(input).expect("should parse");
522 match msg {
523 ConsoleMessage::Event(BaoEvent::FetchRequestPaused { post_data, .. }) => {
524 assert_eq!(post_data, Some("body".to_string()));
525 }
526 other => panic!("expected FetchRequestPaused, got {:?}", other),
527 }
528 }
529
530 #[test]
531 fn from_console_text_network_request() {
532 let input = "__BAO_EVT__Network.requestWillBeSent\n{\"id\":\"req1\",\"url\":\"http://example.com\",\"method\":\"GET\",\"headers\":{},\"request\":{\"url\":\"http://example.com\",\"method\":\"GET\"},\"timestamp\":12345.0,\"type\":\"Document\"}";
533 let msg = BaoEvent::from_console_text(input).expect("should parse");
534 match msg {
535 ConsoleMessage::Event(BaoEvent::NetworkRequestWillBeSent {
536 request_id,
537 url,
538 method,
539 timestamp,
540 resource_type,
541 ..
542 }) => {
543 assert_eq!(request_id, "req1");
544 assert_eq!(url, "http://example.com");
545 assert_eq!(method, "GET");
546 assert_eq!(timestamp, 12345.0);
547 assert_eq!(resource_type, "Document");
548 }
549 other => panic!("expected NetworkRequestWillBeSent, got {:?}", other),
550 }
551 }
552
553 #[test]
554 fn from_console_text_network_request_defaults_when_request_missing() {
555 let input = "__BAO_EVT__Network.requestWillBeSent\n{\"id\":\"req1\",\"url\":\"http://example.com\",\"method\":\"GET\",\"headers\":{},\"timestamp\":100.0,\"type\":\"Script\"}";
556 let msg = BaoEvent::from_console_text(input).expect("should parse");
557 match msg {
558 ConsoleMessage::Event(BaoEvent::NetworkRequestWillBeSent { request, .. }) => {
559 assert_eq!(request["url"], "http://example.com");
560 assert_eq!(request["method"], "GET");
561 }
562 other => panic!("expected NetworkRequestWillBeSent, got {:?}", other),
563 }
564 }
565
566 #[test]
567 fn from_console_text_network_response() {
568 let input = "__BAO_EVT__Network.responseReceived\n{\"id\":\"req2\",\"url\":\"http://example.com\",\"status\":200,\"statusText\":\"OK\",\"headers\":{\"Content-Type\":\"text/html\"},\"timestamp\":12346.0,\"type\":\"Document\"}";
569 let msg = BaoEvent::from_console_text(input).expect("should parse");
570 match msg {
571 ConsoleMessage::Event(BaoEvent::NetworkResponseReceived {
572 request_id,
573 url,
574 status,
575 status_text,
576 headers,
577 timestamp,
578 resource_type,
579 }) => {
580 assert_eq!(request_id, "req2");
581 assert_eq!(url, "http://example.com");
582 assert_eq!(status, 200);
583 assert_eq!(status_text, "OK");
584 assert_eq!(headers["Content-Type"], "text/html");
585 assert_eq!(timestamp, 12346.0);
586 assert_eq!(resource_type, "Document");
587 }
588 other => panic!("expected NetworkResponseReceived, got {:?}", other),
589 }
590 }
591
592 #[test]
593 fn from_console_text_network_loading_failed() {
594 let input = "__BAO_EVT__Network.loadingFailed\n{\"id\":\"req3\",\"type\":\"XHR\",\"timestamp\":12347.0}";
595 let msg = BaoEvent::from_console_text(input).expect("should parse");
596 match msg {
597 ConsoleMessage::Event(BaoEvent::NetworkLoadingFailed {
598 request_id,
599 resource_type,
600 error_text,
601 timestamp,
602 }) => {
603 assert_eq!(request_id, "req3");
604 assert_eq!(resource_type, "XHR");
605 assert_eq!(error_text, "Network error");
606 assert_eq!(timestamp, 12347.0);
607 }
608 other => panic!("expected NetworkLoadingFailed, got {:?}", other),
609 }
610 }
611
612 #[test]
613 fn from_console_text_network_loading_failed_with_custom_error() {
614 let input = "__BAO_EVT__Network.loadingFailed\n{\"id\":\"req3\",\"type\":\"XHR\",\"errorText\":\"Connection refused\",\"timestamp\":100.0}";
615 let msg = BaoEvent::from_console_text(input).expect("should parse");
616 match msg {
617 ConsoleMessage::Event(BaoEvent::NetworkLoadingFailed { error_text, .. }) => {
618 assert_eq!(error_text, "Connection refused");
619 }
620 other => panic!("expected NetworkLoadingFailed, got {:?}", other),
621 }
622 }
623
624 #[test]
625 fn from_console_text_debugger_script_parsed() {
626 let input = "__BAO_EVT__Debugger.scriptParsed\n{\"id\":\"1\",\"url\":\"test.js\",\"startLine\":0,\"endLine\":10}";
627 let msg = BaoEvent::from_console_text(input).expect("should parse");
628 match msg {
629 ConsoleMessage::Event(BaoEvent::DebuggerScriptParsed {
630 script_id,
631 url,
632 start_line,
633 end_line,
634 }) => {
635 assert_eq!(script_id, "1");
636 assert_eq!(url, "test.js");
637 assert_eq!(start_line, 0);
638 assert_eq!(end_line, 10);
639 }
640 other => panic!("expected DebuggerScriptParsed, got {:?}", other),
641 }
642 }
643
644 #[test]
645 fn from_console_text_debugger_paused() {
646 let input = "__BAO_EVT__Debugger.paused\n{\"callFrames\":[],\"reason\":\"breakpoint\",\"hitBreakpoints\":[]}";
647 let msg = BaoEvent::from_console_text(input).expect("should parse");
648 match msg {
649 ConsoleMessage::Event(BaoEvent::DebuggerPaused {
650 call_frames,
651 reason,
652 hit_breakpoints,
653 }) => {
654 assert_eq!(call_frames, json!([]));
655 assert_eq!(reason, "breakpoint");
656 assert_eq!(hit_breakpoints, json!([]));
657 }
658 other => panic!("expected DebuggerPaused, got {:?}", other),
659 }
660 }
661
662 #[test]
663 fn from_console_text_runtime_exception() {
664 let input = "__BAO_EVT__Runtime.exceptionThrown\n{\"timestamp\":12348.0,\"text\":\"TypeError: x is not a function\",\"url\":\"test.js\",\"line\":10,\"column\":5,\"stackTrace\":null}";
665 let msg = BaoEvent::from_console_text(input).expect("should parse");
666 match msg {
667 ConsoleMessage::Event(BaoEvent::RuntimeExceptionThrown {
668 timestamp,
669 text,
670 url,
671 line,
672 column,
673 stack_trace,
674 }) => {
675 assert_eq!(timestamp, 12348.0);
676 assert_eq!(text, "TypeError: x is not a function");
677 assert_eq!(url, "test.js");
678 assert_eq!(line, 10);
679 assert_eq!(column, 5);
680 assert!(stack_trace.is_null());
681 }
682 other => panic!("expected RuntimeExceptionThrown, got {:?}", other),
683 }
684 }
685
686 #[test]
687 fn from_console_text_page_load() {
688 let input = "__BAO_EVT__Page.loadEventFired\n{\"timestamp\":12345.0}";
689 let msg = BaoEvent::from_console_text(input).expect("should parse");
690 match msg {
691 ConsoleMessage::Event(BaoEvent::PageLoadEventFired { timestamp }) => {
692 assert_eq!(timestamp, 12345.0);
693 }
694 other => panic!("expected PageLoadEventFired, got {:?}", other),
695 }
696 }
697
698 #[test]
699 fn from_console_text_unknown_method_returns_none() {
700 let input = "__BAO_EVT__Unknown.method\n{}";
701 assert!(BaoEvent::from_console_text(input).is_none());
702 }
703
704 #[test]
705 fn from_console_text_no_prefix_returns_none() {
706 let input = "plain text log message";
707 assert!(BaoEvent::from_console_text(input).is_none());
708 }
709
710 #[test]
711 fn from_console_text_malformed_json_still_parses() {
712 let input = "__BAO_EVT__Page.loadEventFired\n{bad json}";
714 let msg = BaoEvent::from_console_text(input).expect("should parse with defaults");
715 match msg {
716 ConsoleMessage::Event(BaoEvent::PageLoadEventFired { timestamp }) => {
717 assert_eq!(timestamp, 0.0);
718 }
719 other => panic!("expected PageLoadEventFired, got {:?}", other),
720 }
721 }
722
723 #[test]
724 fn from_console_text_missing_newline() {
725 let input = "__BAO_EVT__Page.loadEventFired{\"timestamp\":1.0}";
727 let msg = BaoEvent::from_console_text(input).expect("should parse without newline");
728 match msg {
729 ConsoleMessage::Event(BaoEvent::PageLoadEventFired { timestamp }) => {
730 assert_eq!(timestamp, 1.0);
731 }
732 other => panic!("expected PageLoadEventFired, got {:?}", other),
733 }
734 }
735
736 #[test]
739 fn broadcast_fetch_request_paused() {
740 let sender = RecordingSender::new();
741 let event = BaoEvent::FetchRequestPaused {
742 request_id: "r1".into(),
743 url: "http://test.com".into(),
744 method: "GET".into(),
745 headers: json!({"X-Custom": "val"}),
746 post_data: None,
747 resource_type: "Document".into(),
748 };
749 event.broadcast(&sender);
750 let events = sender.events();
751 assert_eq!(events.len(), 1);
752 assert_eq!(events[0].0, "Fetch.requestPaused");
753 let params = &events[0].1;
754 assert_eq!(params["requestId"], "r1");
755 assert_eq!(params["request"]["url"], "http://test.com");
756 assert_eq!(params["request"]["method"], "GET");
757 assert_eq!(params["request"]["headers"]["X-Custom"], "val");
758 assert_eq!(params["resourceType"], "Document");
759 assert_eq!(params["networkStage"], "Request");
760 }
761
762 #[test]
763 fn broadcast_network_request_will_be_sent() {
764 let sender = RecordingSender::new();
765 let event = BaoEvent::NetworkRequestWillBeSent {
766 request_id: "req1".into(),
767 url: "http://example.com".into(),
768 method: "GET".into(),
769 headers: json!({}),
770 request: json!({"url": "http://example.com", "method": "GET"}),
771 timestamp: 12345.0,
772 resource_type: "Document".into(),
773 };
774 event.broadcast(&sender);
775 let events = sender.events();
776 assert_eq!(events.len(), 1);
777 assert_eq!(events[0].0, "Network.requestWillBeSent");
778 let params = &events[0].1;
779 assert_eq!(params["requestId"], "req1");
780 assert_eq!(params["request"]["url"], "http://example.com");
781 assert_eq!(params["timestamp"], 12345.0);
782 assert_eq!(params["type"], "Document");
783 }
784
785 #[test]
786 fn broadcast_network_response_received_also_emits_loading_finished() {
787 let sender = RecordingSender::new();
788 let event = BaoEvent::NetworkResponseReceived {
789 request_id: "req2".into(),
790 url: "http://example.com".into(),
791 status: 200,
792 status_text: "OK".into(),
793 headers: json!({"Content-Type": "text/html"}),
794 timestamp: 12346.0,
795 resource_type: "Document".into(),
796 };
797 event.broadcast(&sender);
798 let events = sender.events();
799 assert_eq!(events.len(), 2);
800
801 assert_eq!(events[0].0, "Network.responseReceived");
802 let p0 = &events[0].1;
803 assert_eq!(p0["requestId"], "req2");
804 assert_eq!(p0["response"]["url"], "http://example.com");
805 assert_eq!(p0["response"]["status"], 200);
806 assert_eq!(p0["response"]["statusText"], "OK");
807 assert_eq!(p0["response"]["headers"]["Content-Type"], "text/html");
808 assert_eq!(p0["timestamp"], 12346.0);
809 assert_eq!(p0["type"], "Document");
810
811 assert_eq!(events[1].0, "Network.loadingFinished");
812 let p1 = &events[1].1;
813 assert_eq!(p1["requestId"], "req2");
814 assert_eq!(p1["timestamp"], 12346.0);
815 }
816
817 #[test]
818 fn broadcast_network_loading_failed() {
819 let sender = RecordingSender::new();
820 let event = BaoEvent::NetworkLoadingFailed {
821 request_id: "req3".into(),
822 resource_type: "XHR".into(),
823 error_text: "Network error".into(),
824 timestamp: 12347.0,
825 };
826 event.broadcast(&sender);
827 let events = sender.events();
828 assert_eq!(events.len(), 1);
829 assert_eq!(events[0].0, "Network.loadingFailed");
830 let params = &events[0].1;
831 assert_eq!(params["requestId"], "req3");
832 assert_eq!(params["type"], "XHR");
833 assert_eq!(params["errorText"], "Network error");
834 assert_eq!(params["timestamp"], 12347.0);
835 }
836
837 #[test]
838 fn broadcast_debugger_script_parsed() {
839 let sender = RecordingSender::new();
840 let event = BaoEvent::DebuggerScriptParsed {
841 script_id: "1".into(),
842 url: "test.js".into(),
843 start_line: 0,
844 end_line: 10,
845 };
846 event.broadcast(&sender);
847 let events = sender.events();
848 assert_eq!(events.len(), 1);
849 assert_eq!(events[0].0, "Debugger.scriptParsed");
850 let params = &events[0].1;
851 assert_eq!(params["scriptId"], "1");
852 assert_eq!(params["url"], "test.js");
853 assert_eq!(params["startLine"], 0);
854 assert_eq!(params["endLine"], 10);
855 }
856
857 #[test]
858 fn broadcast_debugger_paused() {
859 let sender = RecordingSender::new();
860 let event = BaoEvent::DebuggerPaused {
861 call_frames: json!([]),
862 reason: "breakpoint".into(),
863 hit_breakpoints: json!([]),
864 };
865 event.broadcast(&sender);
866 let events = sender.events();
867 assert_eq!(events.len(), 1);
868 assert_eq!(events[0].0, "Debugger.paused");
869 let params = &events[0].1;
870 assert_eq!(params["callFrames"], json!([]));
871 assert_eq!(params["reason"], "breakpoint");
872 assert_eq!(params["hitBreakpoints"], json!([]));
873 }
874
875 #[test]
876 fn broadcast_runtime_exception_thrown() {
877 let sender = RecordingSender::new();
878 let event = BaoEvent::RuntimeExceptionThrown {
879 timestamp: 12348.0,
880 text: "TypeError: x is not a function".into(),
881 url: "test.js".into(),
882 line: 10,
883 column: 5,
884 stack_trace: Value::Null,
885 };
886 event.broadcast(&sender);
887 let events = sender.events();
888 assert_eq!(events.len(), 1);
889 assert_eq!(events[0].0, "Runtime.exceptionThrown");
890 let params = &events[0].1;
891 assert_eq!(params["timestamp"], 12348.0);
892 let details = ¶ms["exceptionDetails"];
893 assert_eq!(details["text"], "TypeError: x is not a function");
894 assert_eq!(details["url"], "test.js");
895 assert_eq!(details["lineNumber"], 10);
896 assert_eq!(details["columnNumber"], 5);
897 assert!(details["stackTrace"].is_null());
898 }
899
900 #[test]
901 fn broadcast_page_load_event_fired() {
902 let sender = RecordingSender::new();
903 let event = BaoEvent::PageLoadEventFired { timestamp: 12345.0 };
904 event.broadcast(&sender);
905 let events = sender.events();
906 assert_eq!(events.len(), 1);
907 assert_eq!(events[0].0, "Page.loadEventFired");
908 let params = &events[0].1;
909 assert_eq!(params["timestamp"], 12345.0);
910 }
911
912 #[test]
913 fn broadcast_produces_correct_cdp_events() {
914 let sender = RecordingSender::new();
916
917 let input = "__BAO_EVT__Fetch.requestPaused\n{\"id\":\"r1\",\"url\":\"http://test.com\",\"method\":\"GET\",\"headers\":{},\"resourceType\":\"Document\"}";
918 let msg = BaoEvent::from_console_text(input).expect("should parse");
919 if let ConsoleMessage::Event(evt) = msg {
920 evt.broadcast(&sender);
921 }
922
923 let input = "__BAO_EVT__Page.loadEventFired\n{\"timestamp\":999.0}";
924 let msg = BaoEvent::from_console_text(input).expect("should parse");
925 if let ConsoleMessage::Event(evt) = msg {
926 evt.broadcast(&sender);
927 }
928
929 let events = sender.events();
930 assert_eq!(events.len(), 2);
931 assert_eq!(events[0].0, "Fetch.requestPaused");
932 assert_eq!(events[0].1["requestId"], "r1");
933 assert_eq!(events[1].0, "Page.loadEventFired");
934 assert_eq!(events[1].1["timestamp"], 999.0);
935 }
936
937 #[test]
938 fn console_message_log_variant() {
939 let log = ConsoleMessage::Log {
940 level: "info".into(),
941 text: "hello".into(),
942 };
943 match log {
944 ConsoleMessage::Log { level, text } => {
945 assert_eq!(level, "info");
946 assert_eq!(text, "hello");
947 }
948 ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
949 }
950 }
951
952 #[test]
953 fn console_message_event_variant() {
954 let evt = ConsoleMessage::Event(BaoEvent::PageLoadEventFired { timestamp: 1.0 });
955 match evt {
956 ConsoleMessage::Event(BaoEvent::PageLoadEventFired { timestamp }) => {
957 assert_eq!(timestamp, 1.0);
958 }
959 _ => panic!("expected Event(PageLoadEventFired)"),
960 }
961 }
962
963 #[test]
966 fn from_console_text_page_frame_navigated() {
967 let input = "__BAO_EVT__Page.frameNavigated\n{\"frameId\":\"0\",\"url\":\"https://example.com\",\"loaderId\":\"abc\"}";
968 let msg = BaoEvent::from_console_text(input).expect("should parse");
969 match msg {
970 ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
971 frame_id,
972 url,
973 loader_id,
974 }) => {
975 assert_eq!(frame_id, "0");
976 assert_eq!(url, "https://example.com");
977 assert_eq!(loader_id, "abc");
978 }
979 other => panic!("expected PageFrameNavigated, got {:?}", other),
980 }
981 }
982
983 #[test]
984 fn from_console_text_page_frame_navigated_defaults() {
985 let input = "__BAO_EVT__Page.frameNavigated\n{}";
986 let msg = BaoEvent::from_console_text(input).expect("should parse with defaults");
987 match msg {
988 ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
989 frame_id,
990 url,
991 loader_id,
992 }) => {
993 assert_eq!(frame_id, "0");
994 assert_eq!(url, "");
995 assert_eq!(loader_id, "");
996 }
997 other => panic!("expected PageFrameNavigated, got {:?}", other),
998 }
999 }
1000
1001 #[test]
1002 fn broadcast_page_frame_navigated() {
1003 let sender = RecordingSender::new();
1004 let event = BaoEvent::PageFrameNavigated {
1005 frame_id: "0".into(),
1006 url: "https://example.com".into(),
1007 loader_id: "abc".into(),
1008 };
1009 event.broadcast(&sender);
1010 let events = sender.events();
1011 assert_eq!(events.len(), 1);
1012 assert_eq!(events[0].0, "Page.frameNavigated");
1013 let params = &events[0].1;
1014 assert_eq!(params["frame"]["id"], "0");
1015 assert_eq!(params["frame"]["url"], "https://example.com");
1016 assert_eq!(params["frame"]["loaderId"], "abc");
1017 assert_eq!(params["frame"]["mimeType"], "text/html");
1018 assert_eq!(params["frame"]["securityOrigin"], "");
1019 assert_eq!(params["frame"]["secureContextType"], "Secure");
1020 }
1021
1022 #[test]
1025 fn from_console_text_security_certificate_error() {
1026 let input = "__BAO_EVT__Security.certificateError\n{\"eventId\":1,\"errorType\":\"net::ERR_CERT_AUTHORITY_INVALID\",\"url\":\"https://bad.example.com\"}";
1027 let msg = BaoEvent::from_console_text(input).expect("should parse");
1028 match msg {
1029 ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
1030 event_id,
1031 error_type,
1032 url,
1033 }) => {
1034 assert_eq!(event_id, 1);
1035 assert_eq!(error_type, "net::ERR_CERT_AUTHORITY_INVALID");
1036 assert_eq!(url, "https://bad.example.com");
1037 }
1038 other => panic!("expected SecurityCertificateError, got {:?}", other),
1039 }
1040 }
1041
1042 #[test]
1043 fn from_console_text_security_certificate_error_defaults() {
1044 let input = "__BAO_EVT__Security.certificateError\n{}";
1045 let msg = BaoEvent::from_console_text(input).expect("should parse with defaults");
1046 match msg {
1047 ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
1048 event_id,
1049 error_type,
1050 url,
1051 }) => {
1052 assert_eq!(event_id, 0);
1053 assert_eq!(error_type, "");
1054 assert_eq!(url, "");
1055 }
1056 other => panic!("expected SecurityCertificateError, got {:?}", other),
1057 }
1058 }
1059
1060 #[test]
1061 fn broadcast_security_certificate_error() {
1062 let sender = RecordingSender::new();
1063 let event = BaoEvent::SecurityCertificateError {
1064 event_id: 1,
1065 error_type: "net::ERR_CERT_AUTHORITY_INVALID".into(),
1066 url: "https://bad.example.com".into(),
1067 };
1068 event.broadcast(&sender);
1069 let events = sender.events();
1070 assert_eq!(events.len(), 1);
1071 assert_eq!(events[0].0, "Security.certificateError");
1072 let params = &events[0].1;
1073 assert_eq!(params["eventId"], 1);
1074 assert_eq!(params["errorType"], "net::ERR_CERT_AUTHORITY_INVALID");
1075 assert_eq!(params["resourceUrl"], "https://bad.example.com");
1076 }
1077}