use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize)]
pub struct CdpRequest {
pub id: u64,
pub method: &'static str,
pub params: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CdpMessage {
Response(CdpResponse),
Event(CdpEvent),
}
#[derive(Debug, Deserialize)]
pub struct CdpResponse {
pub id: u64,
#[serde(default)]
pub result: Option<Value>,
#[serde(default)]
pub error: Option<CdpError>,
}
#[derive(Debug, Deserialize)]
pub struct CdpError {
pub code: i64,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CdpEvent {
pub method: String,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTargetParams {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_window: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTargetResult {
pub target_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTargetsResult {
pub target_infos: Vec<TargetInfo>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetInfo {
pub target_id: String,
#[serde(rename = "type")]
pub target_type: String,
pub title: String,
pub url: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigateParams {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub referrer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transition_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigateResult {
#[serde(default)]
pub error_text: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureScreenshotParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub clip: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_surface: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capture_beyond_viewport: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub optimize_for_speed: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureScreenshotResult {
pub data: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EvaluateResult {
pub result: RemoteObject,
#[serde(default)]
pub exception_details: Option<ExceptionDetails>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoteObject {
#[serde(rename = "type")]
pub remote_type: String,
#[serde(default)]
pub value: Option<Value>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub object_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExceptionDetails {
pub text: String,
#[serde(default)]
pub exception: Option<RemoteObject>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolveNodeParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub node_id: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub backend_node_id: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution_context_id: Option<u64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolveNodeResult {
pub object: RemoteObject,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetBoxModelResult {
pub model: BoxModel,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BoxModel {
pub content: Quad,
pub border: Quad,
}
pub type Quad = Vec<f64>;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DispatchMouseEventParams {
#[serde(rename = "type")]
pub event_type: MouseEventType,
pub x: f64,
pub y: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub button: Option<MouseButton>,
#[serde(skip_serializing_if = "Option::is_none")]
pub buttons: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modifiers: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub click_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta_x: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta_y: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pointer_type: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum MouseEventType {
MousePressed,
MouseReleased,
MouseMoved,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum MouseButton {
Left,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetFullAXTreeResult {
pub nodes: Vec<AXNode>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AXNode {
pub node_id: String,
#[serde(default)]
pub ignored: bool,
#[serde(default)]
pub role: Option<AXValue>,
#[serde(default)]
pub name: Option<AXValue>,
#[serde(default)]
pub value: Option<AXValue>,
#[serde(default)]
pub properties: Option<Vec<AXProperty>>,
#[serde(default)]
pub child_ids: Option<Vec<String>>,
#[serde(default, rename = "backendDOMNodeId")]
pub backend_dom_node_id: Option<i64>,
#[serde(default)]
pub parent_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AXValue {
#[serde(default)]
pub value: Option<Value>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AXProperty {
pub name: String,
pub value: AXValue,
}
impl BoxModel {
pub fn content_center(&self) -> (f64, f64) {
if self.content.len() < 8 {
return (0.0, 0.0);
}
let cx = (self.content[0] + self.content[2] + self.content[4] + self.content[6]) / 4.0;
let cy = (self.content[1] + self.content[3] + self.content[5] + self.content[7]) / 4.0;
(cx, cy)
}
}
impl AXNode {
pub fn role_name(&self) -> Option<&str> {
self.role.as_ref()?.value.as_ref()?.as_str()
}
pub fn name_value(&self) -> Option<&str> {
self.name.as_ref()?.value.as_ref()?.as_str()
}
}
impl std::fmt::Display for CdpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CDP error {}: {}", self.code, self.message)
}
}
impl std::error::Error for CdpError {}
#[cfg(test)]
mod serde_proof {
use super::*;
#[test]
fn an_undeclared_field_cannot_break_a_parse() {
let json = r#"{"id":7,"error":{"code":-32000,"message":"boom","data":{"detail":"x"}},
"sessionId":"S1","whatever":42}"#;
let r: CdpResponse = serde_json::from_str(json).expect("undeclared fields are ignored");
assert_eq!(r.id, 7);
assert_eq!(r.error.expect("error parsed").code, -32000);
}
#[test]
fn only_what_is_read_is_demanded_of_chrome() {
let two_quads_only = r#"{"content":[0,0,1,0,1,1,0,1],"border":[0,0,1,0,1,1,0,1]}"#;
assert!(serde_json::from_str::<BoxModel>(two_quads_only).is_ok());
let err = serde_json::from_str::<BoxModel>(r#"{"border":[0,0,1,0,1,1,0,1]}"#)
.expect_err("content is read, so it is still demanded")
.to_string();
assert!(err.contains("content"), "expected a missing-field error naming content: {err}");
assert!(serde_json::from_str::<ExceptionDetails>(r#"{"text":"ReferenceError"}"#).is_ok());
assert!(serde_json::from_str::<AXValue>(r#"{"value":"button"}"#).is_ok());
assert!(serde_json::from_str::<NavigateResult>("{}").is_ok());
}
}