Skip to main content

playwright_cdp/cdp/
messages.rs

1//! CDP wire-format message types.
2//!
3//! CDP speaks one JSON object per WebSocket text frame:
4//! - Request:  `{ id, method, params, sessionId? }`
5//! - Response: `{ id, result?, error?: { code, message, data? } }`
6//! - Event:    `{ method, params, sessionId? }`
7//!
8//! Responses carry `id` and no `method`; events carry `method` and no `id`.
9//! We deserialize with an `#[serde(untagged)]` enum that tries `Response`
10//! first, falling back to `Event`.
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15/// An outgoing CDP request. `session_id` is `None` for browser-level commands.
16#[derive(Debug, Serialize)]
17pub struct CdpRequest {
18    pub id: u32,
19    pub method: String,
20    #[serde(skip_serializing_if = "Value::is_null", default)]
21    pub params: Value,
22    #[serde(skip_serializing_if = "Option::is_none", rename = "sessionId")]
23    pub session_id: Option<String>,
24}
25
26/// The flat error body returned on a failed CDP command.
27#[derive(Debug, Deserialize)]
28pub struct CdpErrorBody {
29    pub code: i64,
30    pub message: String,
31    #[serde(default)]
32    pub data: Option<Value>,
33}
34
35/// A CDP response. Exactly one of `result`/`error` is present on success/failure.
36#[derive(Debug, Deserialize)]
37pub struct CdpResponse {
38    pub id: u32,
39    #[serde(default)]
40    pub result: Option<Value>,
41    #[serde(default)]
42    pub error: Option<CdpErrorBody>,
43}
44
45/// A CDP event. `session_id` is `None` for browser-level events.
46#[derive(Debug, Clone, Deserialize)]
47pub struct CdpEvent {
48    pub method: String,
49    #[serde(default)]
50    pub params: Value,
51    #[serde(default, rename = "sessionId")]
52    pub session_id: Option<String>,
53}
54
55/// Discriminated union of incoming messages.
56///
57/// Ordering matters: `Response` (has `id`, no `method`) is tried first;
58/// `Event` (has `method`, no `id`) is the fallback.
59#[derive(Debug, Deserialize)]
60#[serde(untagged)]
61pub enum Message {
62    Response(CdpResponse),
63    Event(CdpEvent),
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn request_omits_empty_params_and_missing_session() {
72        let req = CdpRequest {
73            id: 1,
74            method: "Target.setAutoAttach".into(),
75            params: serde_json::json!({"autoAttach": true}),
76            session_id: None,
77        };
78        let s = serde_json::to_string(&req).unwrap();
79        assert!(!s.contains("sessionId"), "sessionId should be absent: {s}");
80        assert!(s.contains("\"params\""));
81    }
82
83    #[test]
84    fn request_serializes_session_id_when_present() {
85        let req = CdpRequest {
86            id: 2,
87            method: "Page.navigate".into(),
88            params: serde_json::json!({}),
89            session_id: Some("ABC".into()),
90        };
91        let s = serde_json::to_string(&req).unwrap();
92        assert!(s.contains("\"sessionId\":\"ABC\""), "{s}");
93    }
94
95    #[test]
96    fn demux_response_with_result() {
97        let v = serde_json::json!({ "id": 5, "result": { "ok": true } });
98        let m: Message = serde_json::from_value(v).unwrap();
99        match m {
100            Message::Response(r) => {
101                assert_eq!(r.id, 5);
102                assert!(r.error.is_none());
103            }
104            _ => panic!("expected Response"),
105        }
106    }
107
108    #[test]
109    fn demux_response_with_error() {
110        let v = serde_json::json!({
111            "id": 6,
112            "error": { "code": -32000, "message": "boom", "data": "ctx" }
113        });
114        let m: Message = serde_json::from_value(v).unwrap();
115        match m {
116            Message::Response(r) => {
117                let e = r.error.expect("error body");
118                assert_eq!(e.code, -32000);
119                assert_eq!(e.message, "boom");
120                assert_eq!(e.data, Some(serde_json::json!("ctx")));
121                assert!(r.result.is_none());
122            }
123            _ => panic!("expected Response"),
124        }
125    }
126
127    #[test]
128    fn demux_event_no_session() {
129        let v = serde_json::json!({ "method": "Target.attachedToTarget", "params": { "x": 1 } });
130        let m: Message = serde_json::from_value(v).unwrap();
131        match m {
132            Message::Event(e) => {
133                assert_eq!(e.method, "Target.attachedToTarget");
134                assert!(e.session_id.is_none());
135            }
136            _ => panic!("expected Event"),
137        }
138    }
139
140    #[test]
141    fn demux_event_with_session() {
142        let v = serde_json::json!({
143            "method": "Page.lifecycleEvent",
144            "params": { "name": "load" },
145            "sessionId": "S1"
146        });
147        let m: Message = serde_json::from_value(v).unwrap();
148        match m {
149            Message::Event(e) => {
150                assert_eq!(e.method, "Page.lifecycleEvent");
151                assert_eq!(e.session_id.as_deref(), Some("S1"));
152            }
153            _ => panic!("expected Event"),
154        }
155    }
156}