use std::sync::Arc;
use serde::Deserialize;
use serde_json::Value;
use crate::error::{WebDriverError, WebDriverErrorInner, WebDriverResult};
use crate::session::handle::SessionHandle;
#[derive(Debug, Deserialize)]
struct VersionInfo {
#[serde(rename = "webSocketDebuggerUrl")]
web_socket_debugger_url: Option<String>,
}
pub(crate) async fn resolve_cdp_websocket_url(
handle: &Arc<SessionHandle>,
) -> WebDriverResult<String> {
let caps = handle.capabilities();
if let Some(Value::String(url)) = caps.get("se:cdp") {
return Ok(url.clone());
}
if let Some(Value::String(url)) = caps.get("webSocketDebuggerUrl") {
return Ok(url.clone());
}
let debugger_address = caps
.get("goog:chromeOptions")
.and_then(|v| v.get("debuggerAddress"))
.and_then(Value::as_str)
.or_else(|| {
caps.get("ms:edgeOptions")
.and_then(|v| v.get("debuggerAddress"))
.and_then(Value::as_str)
})
.ok_or_else(|| not_found("debuggerAddress not present in session capabilities"))?
.to_string();
let url = format!("http://{debugger_address}/json/version");
use bytes::Bytes;
use http::Method;
let request = http::Request::builder()
.method(Method::GET)
.uri(&url)
.body(crate::session::http::Body::Empty)
.map_err(|e| WebDriverError::ParseError(format!("invalid /json/version URL: {e}")))?;
let response = handle.client.send(request).await?;
let bytes: &Bytes = response.body();
let info: VersionInfo = serde_json::from_slice(bytes)
.map_err(|e| WebDriverError::Json(format!("/json/version parse error: {e}")))?;
info.web_socket_debugger_url
.ok_or_else(|| not_found("/json/version did not contain webSocketDebuggerUrl"))
}
fn not_found(msg: &str) -> WebDriverError {
WebDriverError::from_inner(WebDriverErrorInner::NotFound(
"CDP WebSocket URL".to_string(),
msg.to_string(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Capabilities;
use serde_json::json;
fn lookup_static_caps(caps: &Capabilities) -> Option<String> {
if let Some(Value::String(url)) = caps.get("se:cdp") {
return Some(url.clone());
}
if let Some(Value::String(url)) = caps.get("webSocketDebuggerUrl") {
return Some(url.clone());
}
None
}
fn caps_with(pairs: &[(&str, Value)]) -> Capabilities {
let mut c = Capabilities::new();
for (k, v) in pairs {
c.set(*k, v.clone()).unwrap();
}
c
}
#[test]
fn se_cdp_capability_wins_first() {
let caps = caps_with(&[
("se:cdp", json!("ws://grid/cdp")),
("webSocketDebuggerUrl", json!("ws://other/cdp")),
]);
assert_eq!(lookup_static_caps(&caps).as_deref(), Some("ws://grid/cdp"));
}
#[test]
fn web_socket_debugger_url_used_when_no_se_cdp() {
let caps = caps_with(&[("webSocketDebuggerUrl", json!("ws://chrome/cdp"))]);
assert_eq!(lookup_static_caps(&caps).as_deref(), Some("ws://chrome/cdp"));
}
#[test]
fn falls_through_when_static_keys_absent() {
let caps =
caps_with(&[("goog:chromeOptions", json!({"debuggerAddress": "127.0.0.1:9222"}))]);
assert!(lookup_static_caps(&caps).is_none());
}
#[test]
fn debugger_address_extraction_prefers_goog_then_edge() {
let goog =
caps_with(&[("goog:chromeOptions", json!({"debuggerAddress": "127.0.0.1:9222"}))]);
let from_goog = goog
.get("goog:chromeOptions")
.and_then(|v| v.get("debuggerAddress"))
.and_then(Value::as_str);
assert_eq!(from_goog, Some("127.0.0.1:9222"));
let edge = caps_with(&[("ms:edgeOptions", json!({"debuggerAddress": "127.0.0.1:9223"}))]);
let from_edge = edge
.get("ms:edgeOptions")
.and_then(|v| v.get("debuggerAddress"))
.and_then(Value::as_str);
assert_eq!(from_edge, Some("127.0.0.1:9223"));
}
#[test]
fn version_info_parses_with_url_present() {
let body = json!({
"Browser": "Chrome/121.0",
"Protocol-Version": "1.3",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/ABC"
});
let info: VersionInfo = serde_json::from_value(body).unwrap();
assert_eq!(
info.web_socket_debugger_url.as_deref(),
Some("ws://127.0.0.1:9222/devtools/browser/ABC")
);
}
#[test]
fn version_info_missing_url_is_none() {
let body = json!({"Browser": "Chrome/121.0"});
let info: VersionInfo = serde_json::from_value(body).unwrap();
assert!(info.web_socket_debugger_url.is_none());
}
}