use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::identifiers::{ElementId, InterceptId};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Command {
BrowsingContext(BrowsingContextCommand),
Element(ElementCommand),
Session(SessionCommand),
Script(ScriptCommand),
Input(InputCommand),
Network(NetworkCommand),
Proxy(ProxyCommand),
Storage(StorageCommand),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum BrowsingContextCommand {
#[serde(rename = "browsingContext.navigate")]
Navigate {
url: String,
},
#[serde(rename = "browsingContext.reload")]
Reload,
#[serde(rename = "browsingContext.goBack")]
GoBack,
#[serde(rename = "browsingContext.goForward")]
GoForward,
#[serde(rename = "browsingContext.getTitle")]
GetTitle,
#[serde(rename = "browsingContext.getUrl")]
GetUrl,
#[serde(rename = "browsingContext.newTab")]
NewTab,
#[serde(rename = "browsingContext.closeTab")]
CloseTab,
#[serde(rename = "browsingContext.focusTab")]
FocusTab,
#[serde(rename = "browsingContext.focusWindow")]
FocusWindow,
#[serde(rename = "browsingContext.switchToFrame")]
SwitchToFrame {
#[serde(rename = "elementId")]
element_id: ElementId,
},
#[serde(rename = "browsingContext.switchToFrameByIndex")]
SwitchToFrameByIndex {
index: usize,
},
#[serde(rename = "browsingContext.switchToFrameByUrl")]
SwitchToFrameByUrl {
#[serde(rename = "urlPattern")]
url_pattern: String,
},
#[serde(rename = "browsingContext.switchToParentFrame")]
SwitchToParentFrame,
#[serde(rename = "browsingContext.getFrameCount")]
GetFrameCount,
#[serde(rename = "browsingContext.getAllFrames")]
GetAllFrames,
#[serde(rename = "browsingContext.captureScreenshot")]
CaptureScreenshot {
format: String,
#[serde(skip_serializing_if = "Option::is_none")]
quality: Option<u8>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum ElementCommand {
#[serde(rename = "element.find")]
Find {
strategy: String,
value: String,
#[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
parent_id: Option<ElementId>,
},
#[serde(rename = "element.findAll")]
FindAll {
strategy: String,
value: String,
#[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
parent_id: Option<ElementId>,
},
#[serde(rename = "element.getProperty")]
GetProperty {
#[serde(rename = "elementId")]
element_id: ElementId,
name: String,
},
#[serde(rename = "element.setProperty")]
SetProperty {
#[serde(rename = "elementId")]
element_id: ElementId,
name: String,
value: Value,
},
#[serde(rename = "element.callMethod")]
CallMethod {
#[serde(rename = "elementId")]
element_id: ElementId,
name: String,
#[serde(default)]
args: Vec<Value>,
},
#[serde(rename = "element.subscribe")]
Subscribe {
strategy: String,
value: String,
#[serde(rename = "oneShot")]
one_shot: bool,
#[serde(skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
},
#[serde(rename = "element.unsubscribe")]
Unsubscribe {
#[serde(rename = "subscriptionId")]
subscription_id: String,
},
#[serde(rename = "element.watchRemoval")]
WatchRemoval {
#[serde(rename = "elementId")]
element_id: ElementId,
},
#[serde(rename = "element.unwatchRemoval")]
UnwatchRemoval {
#[serde(rename = "elementId")]
element_id: ElementId,
},
#[serde(rename = "element.watchAttribute")]
WatchAttribute {
#[serde(rename = "elementId")]
element_id: ElementId,
#[serde(rename = "attributeName", skip_serializing_if = "Option::is_none")]
attribute_name: Option<String>,
},
#[serde(rename = "element.unwatchAttribute")]
UnwatchAttribute {
#[serde(rename = "elementId")]
element_id: ElementId,
},
#[serde(rename = "element.captureScreenshot")]
CaptureScreenshot {
#[serde(rename = "elementId")]
element_id: ElementId,
format: String,
#[serde(skip_serializing_if = "Option::is_none")]
quality: Option<u8>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum SessionCommand {
#[serde(rename = "session.status")]
Status,
#[serde(rename = "session.stealLogs")]
StealLogs,
#[serde(rename = "session.subscribe")]
Subscribe {
events: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
selectors: Option<Vec<String>>,
},
#[serde(rename = "session.unsubscribe")]
Unsubscribe {
subscription_id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum ScriptCommand {
#[serde(rename = "script.evaluate")]
Evaluate {
script: String,
#[serde(default)]
args: Vec<Value>,
},
#[serde(rename = "script.evaluateAsync")]
EvaluateAsync {
script: String,
#[serde(default)]
args: Vec<Value>,
},
#[serde(rename = "script.addPreloadScript")]
AddPreloadScript {
script: String,
},
#[serde(rename = "script.removePreloadScript")]
RemovePreloadScript {
script_id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum InputCommand {
#[serde(rename = "input.typeKey")]
TypeKey {
#[serde(rename = "elementId")]
element_id: ElementId,
key: String,
code: String,
#[serde(rename = "keyCode")]
key_code: u32,
printable: bool,
#[serde(default)]
ctrl: bool,
#[serde(default)]
shift: bool,
#[serde(default)]
alt: bool,
#[serde(default)]
meta: bool,
},
#[serde(rename = "input.typeText")]
TypeText {
#[serde(rename = "elementId")]
element_id: ElementId,
text: String,
},
#[serde(rename = "input.mouseClick")]
MouseClick {
#[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
element_id: Option<ElementId>,
#[serde(skip_serializing_if = "Option::is_none")]
x: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
y: Option<i32>,
#[serde(default)]
button: u8,
},
#[serde(rename = "input.mouseMove")]
MouseMove {
#[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
element_id: Option<ElementId>,
#[serde(skip_serializing_if = "Option::is_none")]
x: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
y: Option<i32>,
},
#[serde(rename = "input.mouseDown")]
MouseDown {
#[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
element_id: Option<ElementId>,
#[serde(skip_serializing_if = "Option::is_none")]
x: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
y: Option<i32>,
#[serde(default)]
button: u8,
},
#[serde(rename = "input.mouseUp")]
MouseUp {
#[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
element_id: Option<ElementId>,
#[serde(skip_serializing_if = "Option::is_none")]
x: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
y: Option<i32>,
#[serde(default)]
button: u8,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum NetworkCommand {
#[serde(rename = "network.addIntercept")]
AddIntercept {
#[serde(default, rename = "interceptRequests")]
intercept_requests: bool,
#[serde(default, rename = "interceptRequestHeaders")]
intercept_request_headers: bool,
#[serde(default, rename = "interceptRequestBody")]
intercept_request_body: bool,
#[serde(default, rename = "interceptResponses")]
intercept_responses: bool,
#[serde(default, rename = "interceptResponseBody")]
intercept_response_body: bool,
#[serde(rename = "urlPatterns", skip_serializing_if = "Option::is_none")]
url_patterns: Option<Vec<String>>,
#[serde(rename = "resourceTypes", skip_serializing_if = "Option::is_none")]
resource_types: Option<Vec<String>>,
},
#[serde(rename = "network.removeIntercept")]
RemoveIntercept {
#[serde(rename = "interceptId")]
intercept_id: InterceptId,
},
#[serde(rename = "network.setBlockRules")]
SetBlockRules {
patterns: Vec<String>,
},
#[serde(rename = "network.clearBlockRules")]
ClearBlockRules,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum ProxyCommand {
#[serde(rename = "proxy.setWindowProxy")]
SetWindowProxy {
#[serde(rename = "type")]
proxy_type: String,
host: String,
port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
password: Option<String>,
#[serde(rename = "proxyDns", default)]
proxy_dns: bool,
},
#[serde(rename = "proxy.clearWindowProxy")]
ClearWindowProxy,
#[serde(rename = "proxy.setTabProxy")]
SetTabProxy {
#[serde(rename = "type")]
proxy_type: String,
host: String,
port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
password: Option<String>,
#[serde(rename = "proxyDns", default)]
proxy_dns: bool,
},
#[serde(rename = "proxy.clearTabProxy")]
ClearTabProxy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
pub enum StorageCommand {
#[serde(rename = "storage.getCookie")]
GetCookie {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
},
#[serde(rename = "storage.setCookie")]
SetCookie {
cookie: Cookie,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
},
#[serde(rename = "storage.deleteCookie")]
DeleteCookie {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
},
#[serde(rename = "storage.getAllCookies")]
GetAllCookies {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cookie {
pub name: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secure: Option<bool>,
#[serde(rename = "httpOnly", skip_serializing_if = "Option::is_none")]
pub http_only: Option<bool>,
#[serde(rename = "sameSite", skip_serializing_if = "Option::is_none")]
pub same_site: Option<String>,
#[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
pub expiration_date: Option<f64>,
}
impl Cookie {
#[inline]
#[must_use]
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
domain: None,
path: None,
secure: None,
http_only: None,
same_site: None,
expiration_date: None,
}
}
#[inline]
#[must_use]
pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
self.domain = Some(domain.into());
self
}
#[inline]
#[must_use]
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
#[inline]
#[must_use]
pub fn with_secure(mut self, secure: bool) -> Self {
self.secure = Some(secure);
self
}
#[inline]
#[must_use]
pub fn with_http_only(mut self, http_only: bool) -> Self {
self.http_only = Some(http_only);
self
}
#[inline]
#[must_use]
pub fn with_same_site(mut self, same_site: impl Into<String>) -> Self {
self.same_site = Some(same_site.into());
self
}
#[inline]
#[must_use]
pub fn with_expiration_date(mut self, expiration_date: f64) -> Self {
self.expiration_date = Some(expiration_date);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_browsing_context_navigate() {
let cmd = BrowsingContextCommand::Navigate {
url: "https://example.com".to_string(),
};
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(json.contains("browsingContext.navigate"));
assert!(json.contains("https://example.com"));
}
#[test]
fn test_element_find() {
let cmd = ElementCommand::Find {
strategy: "css".to_string(),
value: "button.submit".to_string(),
parent_id: None,
};
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(json.contains("element.find"));
assert!(json.contains("button.submit"));
}
#[test]
fn test_element_get_property() {
let cmd = ElementCommand::GetProperty {
element_id: ElementId::new("test-uuid"),
name: "textContent".to_string(),
};
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(json.contains("element.getProperty"));
assert!(json.contains("test-uuid"));
assert!(json.contains("textContent"));
}
#[test]
fn test_cookie_builder() {
let cookie = Cookie::new("session", "abc123")
.with_domain(".example.com")
.with_path("/")
.with_secure(true)
.with_http_only(true)
.with_same_site("strict");
assert_eq!(cookie.name, "session");
assert_eq!(cookie.value, "abc123");
assert_eq!(cookie.domain, Some(".example.com".to_string()));
assert_eq!(cookie.secure, Some(true));
}
#[test]
fn test_network_add_intercept() {
let cmd = NetworkCommand::AddIntercept {
intercept_requests: true,
intercept_request_headers: false,
intercept_request_body: false,
intercept_responses: false,
intercept_response_body: false,
url_patterns: None,
resource_types: None,
};
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(json.contains("network.addIntercept"));
assert!(!json.contains("urlPatterns"));
assert!(!json.contains("resourceTypes"));
}
#[test]
fn test_network_add_intercept_with_filters() {
let cmd = NetworkCommand::AddIntercept {
intercept_requests: true,
intercept_request_headers: false,
intercept_request_body: false,
intercept_responses: false,
intercept_response_body: false,
url_patterns: Some(vec!["*api*".to_string(), "*example.com*".to_string()]),
resource_types: Some(vec!["xhr".to_string(), "fetch".to_string()]),
};
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(json.contains("network.addIntercept"));
assert!(json.contains("urlPatterns"));
assert!(json.contains("resourceTypes"));
assert!(json.contains("*api*"));
assert!(json.contains("xhr"));
}
#[test]
fn test_browsing_context_capture_screenshot() {
let cmd = BrowsingContextCommand::CaptureScreenshot {
format: "png".to_string(),
quality: None,
};
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(json.contains("browsingContext.captureScreenshot"));
assert!(json.contains("\"format\":\"png\""));
let cmd_jpeg = BrowsingContextCommand::CaptureScreenshot {
format: "jpeg".to_string(),
quality: Some(85),
};
let json_jpeg = serde_json::to_string(&cmd_jpeg).expect("serialize");
assert!(json_jpeg.contains("\"quality\":85"));
}
#[test]
fn test_element_capture_screenshot() {
let cmd = ElementCommand::CaptureScreenshot {
element_id: ElementId::new("elem-uuid"),
format: "png".to_string(),
quality: None,
};
let json = serde_json::to_string(&cmd).expect("serialize");
assert!(json.contains("element.captureScreenshot"));
assert!(json.contains("elem-uuid"));
assert!(json.contains("\"format\":\"png\""));
}
}