use std::sync::{Arc, Mutex};
use std::thread;
use cdp_server::{
error_response, ok_empty, ok_response, parse_message, serialize_response, CdpError, CdpEvent,
CdpMessage, CdpResponse, DomainHandler, DomainRegistry, EventSender, RegistryDispatch,
ERR_INVALID_REQUEST, ERR_METHOD_NOT_FOUND,
};
use serde_json::{json, Value};
#[derive(Clone)]
struct NoopSender;
impl EventSender for NoopSender {
fn send_event(&self, _method: &str, _params: Value) {}
}
static NOOP: NoopSender = NoopSender;
fn noop() -> &'static dyn EventSender {
&NOOP
}
struct SpyHandler {
domain: &'static str,
created: Mutex<Vec<String>>,
destroyed: Mutex<Vec<String>>,
commands: Mutex<Vec<String>>,
response: Result<Value, CdpError>,
}
impl SpyHandler {
fn new(domain: &'static str) -> Self {
Self {
domain,
created: Mutex::new(Vec::new()),
destroyed: Mutex::new(Vec::new()),
commands: Mutex::new(Vec::new()),
response: Ok(json!({"echo": true})),
}
}
#[allow(dead_code)]
fn with_response(domain: &'static str, response: Result<Value, CdpError>) -> Self {
let mut h = Self::new(domain);
h.response = response;
h
}
fn created_ids(&self) -> Vec<String> {
self.created.lock().unwrap().clone()
}
fn destroyed_ids(&self) -> Vec<String> {
self.destroyed.lock().unwrap().clone()
}
fn commands(&self) -> Vec<String> {
self.commands.lock().unwrap().clone()
}
}
impl DomainHandler for SpyHandler {
fn domain_name(&self) -> &'static str {
self.domain
}
fn handle_command(
&self,
command: &str,
_params: Value,
_sender: &dyn EventSender,
) -> Result<Value, CdpError> {
self.commands.lock().unwrap().push(command.to_string());
self.response.clone()
}
fn on_session_created(&self, session_id: &str) {
self.created.lock().unwrap().push(session_id.to_string());
}
fn on_session_destroyed(&self, session_id: &str) {
self.destroyed.lock().unwrap().push(session_id.to_string());
}
}
struct Observed {
inner: Arc<SpyHandler>,
}
impl Observed {
fn new(inner: Arc<SpyHandler>) -> Self {
Self { inner }
}
}
impl DomainHandler for Observed {
fn domain_name(&self) -> &'static str {
self.inner.domain_name()
}
fn handle_command(&self, c: &str, p: Value, s: &dyn EventSender) -> Result<Value, CdpError> {
self.inner.handle_command(c, p, s)
}
fn on_session_created(&self, sid: &str) {
self.inner.on_session_created(sid);
}
fn on_session_destroyed(&self, sid: &str) {
self.inner.on_session_destroyed(sid);
}
}
struct ErrorAlways;
impl DomainHandler for ErrorAlways {
fn domain_name(&self) -> &'static str {
"ErrDomain"
}
fn handle_command(&self, _: &str, _: Value, _: &dyn EventSender) -> Result<Value, CdpError> {
Err(CdpError {
code: -32000,
message: "intentional".into(),
})
}
}
struct TargetRecorder {
entered: Mutex<bool>,
}
impl TargetRecorder {
fn new() -> Arc<Self> {
Arc::new(Self {
entered: Mutex::new(false),
})
}
fn was_entered(&self) -> bool {
*self.entered.lock().unwrap()
}
}
struct TargetObserved {
inner: Arc<TargetRecorder>,
}
impl DomainHandler for TargetObserved {
fn domain_name(&self) -> &'static str {
"Target"
}
fn handle_command(&self, _c: &str, _p: Value, _s: &dyn EventSender) -> Result<Value, CdpError> {
*self.inner.entered.lock().unwrap() = true;
Ok(json!({"handledByDomainHandler": true}))
}
}
#[test]
fn test_c7_invalid_request_error_response_chain() {
let malformed = r#"{"id":1,"method":"Page.navigate""#; assert!(parse_message(malformed).is_none());
let resp = error_response(None, ERR_INVALID_REQUEST, "Invalid Request");
assert_eq!(resp.id, None);
assert!(resp.result.is_none());
let err = resp
.error
.as_ref()
.expect("error_response must populate error");
assert_eq!(err.code, -32600);
assert!(err.message.contains("Invalid Request"));
let wire = serialize_response(&resp);
let v: Value = serde_json::from_str(&wire).unwrap();
assert_eq!(v["error"]["code"].as_i64(), Some(-32600));
assert!(v["error"]["message"]
.as_str()
.unwrap()
.contains("Invalid Request"));
}
#[test]
fn test_c7_various_malformed_json_yields_none() {
let cases = [
r#"{"id":1,"#, r#"{"id":1}"#, r#"{"#, "{",
"}",
"null",
"[]",
"42",
"\"string\"",
"",
" ",
r#"{"id":1.5}"#, r#"{"id":1e308}"#, ];
for (i, raw) in cases.iter().enumerate() {
assert!(
parse_message(raw).is_none(),
"case #{i} ({raw:?}) should parse to None to trigger -32600",
);
}
}
#[test]
fn test_c7_non_utf8_bytes_rejected() {
let mut bad: Vec<u8> = b"{\"id\":1, \"method\":\"".to_vec();
bad.extend_from_slice(&[0xFF, 0xFE, 0xC0, 0xC0]);
bad.extend_from_slice(b"\"}");
let s = std::str::from_utf8(&bad);
assert!(s.is_err(), "non-UTF-8 bytes must fail str conversion");
}
#[test]
fn test_c8_method_not_found_error_response_chain() {
let reg = DomainRegistry::<SpyHandler>::new();
reg.register(SpyHandler::new("Page")).unwrap();
assert!(reg
.dispatch_command("Network.enable", json!({}), noop())
.is_none());
let resp = error_response(
Some(7),
ERR_METHOD_NOT_FOUND,
"'Network.enable' wasn't found",
);
assert_eq!(resp.id, Some(7));
let err = resp.error.as_ref().unwrap();
assert_eq!(err.code, -32601);
assert!(err.message.contains("Network.enable"));
let wire = serialize_response(&resp);
let v: Value = serde_json::from_str(&wire).unwrap();
assert_eq!(v["error"]["code"].as_i64(), Some(-32601));
assert!(
v["error"]["message"]
.as_str()
.unwrap()
.contains("Method not found")
|| v["error"]["message"]
.as_str()
.unwrap()
.contains("wasn't found")
);
}
#[test]
fn test_c8_dispatch_none_maps_to_method_not_found_response() {
let reg = DomainRegistry::<SpyHandler>::new();
reg.register(SpyHandler::new("Page")).unwrap();
let method = "Mystery.thing";
let id = Some(42);
let dispatched = reg.dispatch_command(method, json!({}), noop());
let resp: CdpResponse = match dispatched {
Some(Ok(r)) => ok_response(id, r),
Some(Err(e)) => CdpResponse {
id,
result: None,
error: Some(e),
},
None => error_response(id, ERR_METHOD_NOT_FOUND, format!("'{method}' wasn't found")),
};
assert!(resp.error.is_some());
assert_eq!(resp.error.as_ref().unwrap().code, -32601);
assert_eq!(resp.id, Some(42));
}
#[test]
fn test_c5_no_target_handler_means_target_commands_unrouted() {
let reg = DomainRegistry::<SpyHandler>::new();
reg.register(SpyHandler::new("Page")).unwrap();
for method in [
"Target.getTargets",
"Target.createTarget",
"Target.closeTarget",
"Target.attachToTarget",
"Target.detachFromTarget",
"Target.setDiscoverTargets",
"Target.setAutoAttach",
"Target.activateTarget",
] {
assert!(
reg.dispatch_command(method, json!({}), noop()).is_none(),
"{method} must not be routed when no Target handler is registered",
);
}
}
#[test]
fn test_c5_target_handler_registered_serves_builtin_commands() {
let recorder = TargetRecorder::new();
let reg = DomainRegistry::<TargetObserved>::new();
reg.register(TargetObserved {
inner: Arc::clone(&recorder),
})
.unwrap();
let r = reg.dispatch_command("Target.getTargets", json!({}), noop());
assert!(
r.is_some(),
"Target.* must reach the registered Target handler"
);
assert!(r.unwrap().is_ok());
assert!(recorder.was_entered(), "Target handler must be invoked");
}
#[test]
fn test_c6_session_id_roundtrip_preserved() {
let raw = r#"{"id":1,"method":"Runtime.evaluate","sessionId":"flat-1"}"#;
let m = parse_message(raw).unwrap();
assert_eq!(m.session_id.as_deref(), Some("flat-1"));
}
#[test]
fn test_c6_session_id_extremes_roundtrip() {
let cases = [
"", "a", &"x".repeat(1024), "session/with/slashes",
"session:with:colons",
"session-with-dashes",
"SESSION_UPPER",
"session.with.dots",
"session@special#chars$%",
"日本語セッション", ];
for sid in cases {
let raw = json!({"id":1, "method":"Runtime.evaluate", "sessionId": sid}).to_string();
let m = parse_message(&raw).unwrap_or_else(|| panic!("parse failed for sid={sid:?}"));
assert_eq!(m.session_id.as_deref(), Some(sid), "sid={sid:?}");
}
}
#[test]
fn test_c6_dispatch_independent_of_session_id() {
let spy = Arc::new(SpyHandler::new("Runtime"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&spy))).unwrap();
for sid in ["flat-A", "flat-B", "flat-C"] {
let raw = json!({"id":1, "method":"Runtime.evaluate", "sessionId": sid}).to_string();
let m = parse_message(&raw).unwrap();
let dispatched = reg.dispatch_command(&m.method, m.params.unwrap_or_default(), noop());
assert!(dispatched.is_some(), "must route for session {sid}");
}
assert_eq!(spy.commands().len(), 3);
}
#[test]
fn test_c3_notify_session_created_invokes_callback() {
let spy = Arc::new(SpyHandler::new("Page"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&spy))).unwrap();
assert!(spy.created_ids().is_empty(), "no callback before notify");
reg.notify_session_created("Page", "sess-1");
assert_eq!(spy.created_ids(), vec!["sess-1".to_string()]);
reg.notify_session_created("Page", "sess-2");
assert_eq!(spy.created_ids().len(), 2);
}
#[test]
fn test_c3_notify_session_created_unknown_domain_no_callback_no_panic() {
let spy = Arc::new(SpyHandler::new("Page"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&spy))).unwrap();
reg.notify_session_created("UnknownDomain", "sess-x");
assert!(
spy.created_ids().is_empty(),
"unknown domain must not fire callback"
);
}
#[test]
fn test_c3_notify_session_created_only_matching_domain_fires() {
let page = Arc::new(SpyHandler::new("Page"));
let runtime = Arc::new(SpyHandler::new("Runtime"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&page))).unwrap();
reg.register(Observed::new(Arc::clone(&runtime))).unwrap();
reg.notify_session_created("Page", "s1");
assert_eq!(page.created_ids(), vec!["s1".to_string()]);
assert!(
runtime.created_ids().is_empty(),
"Runtime must not fire for Page enable"
);
}
#[test]
fn test_c4_notify_session_destroyed_invokes_each_enabled_domain() {
let page = Arc::new(SpyHandler::new("Page"));
let runtime = Arc::new(SpyHandler::new("Runtime"));
let dom = Arc::new(SpyHandler::new("DOM"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&page))).unwrap();
reg.register(Observed::new(Arc::clone(&runtime))).unwrap();
reg.register(Observed::new(Arc::clone(&dom))).unwrap();
let enabled: Vec<String> = vec!["Page".into(), "Runtime".into(), "DOM".into()];
reg.notify_session_destroyed(&enabled, "sess-dying");
assert_eq!(page.destroyed_ids(), vec!["sess-dying".to_string()]);
assert_eq!(runtime.destroyed_ids(), vec!["sess-dying".to_string()]);
assert_eq!(dom.destroyed_ids(), vec!["sess-dying".to_string()]);
}
#[test]
fn test_c4_notify_session_destroyed_skips_unenabled_domains() {
let page = Arc::new(SpyHandler::new("Page"));
let runtime = Arc::new(SpyHandler::new("Runtime"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&page))).unwrap();
reg.register(Observed::new(Arc::clone(&runtime))).unwrap();
let enabled: Vec<String> = vec!["Page".into()];
reg.notify_session_destroyed(&enabled, "s");
assert_eq!(page.destroyed_ids().len(), 1);
assert!(runtime.destroyed_ids().is_empty());
}
#[test]
fn test_c4_notify_session_destroyed_unknown_domain_in_list_no_panic() {
let page = Arc::new(SpyHandler::new("Page"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&page))).unwrap();
let enabled: Vec<String> = vec!["Page".into(), "Ghost".into(), "Phantom".into()];
reg.notify_session_destroyed(&enabled, "s");
assert_eq!(page.destroyed_ids().len(), 1);
}
#[test]
fn test_c4_notify_session_destroyed_empty_list_no_callback() {
let page = Arc::new(SpyHandler::new("Page"));
let reg = DomainRegistry::<Observed>::new();
reg.register(Observed::new(Arc::clone(&page))).unwrap();
reg.notify_session_destroyed(&[], "s");
assert!(page.destroyed_ids().is_empty());
}
#[test]
fn test_cdp_message_parse_missing_method() {
let raw = r#"{"id": 1}"#;
assert!(serde_json::from_str::<CdpMessage>(raw).is_err());
assert!(parse_message(raw).is_none());
}
#[test]
fn test_cdp_message_parse_empty_method() {
let raw = r#"{"id": 1, "method": ""}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.method, "");
}
#[test]
fn test_cdp_message_parse_null_params() {
let raw = r#"{"id": 1, "method": "Page.navigate", "params": null}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert!(msg.params.is_none());
}
#[test]
fn test_cdp_message_parse_missing_params() {
let raw = r#"{"id": 1, "method": "Page.navigate"}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert!(msg.params.is_none());
}
#[test]
fn test_cdp_message_parse_empty_object_params() {
let raw = r#"{"id": 1, "method": "Page.navigate", "params": {}}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.params, Some(json!({})));
}
#[test]
fn test_cdp_message_parse_nested_params() {
let raw = r#"{"id": 1, "method": "DOM.setAttributeValue", "params": {"nodeId": 1, "attributes": {"class": "test", "data-x": "[1,2,3]"}}}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
let params = msg.params.unwrap();
assert_eq!(params["nodeId"], 1);
assert_eq!(params["attributes"]["class"], "test");
}
#[test]
fn test_cdp_message_parse_large_array_params() {
let nums: Vec<i64> = (0..1000).collect();
let raw = json!({"id": 1, "method": "test", "params": {"data": nums}}).to_string();
let msg: CdpMessage = serde_json::from_str(&raw).unwrap();
let params = msg.params.unwrap();
assert_eq!(params["data"].as_array().unwrap().len(), 1000);
}
#[test]
fn test_cdp_message_parse_string_id_rejected() {
let raw = r#"{"id": "abc", "method": "Page.navigate"}"#;
assert!(serde_json::from_str::<CdpMessage>(raw).is_err());
assert!(parse_message(raw).is_none());
}
#[test]
fn test_cdp_message_parse_negative_id() {
let msg: CdpMessage = serde_json::from_str(r#"{"id": -999, "method": "x.y"}"#).unwrap();
assert_eq!(msg.id, Some(-999));
}
#[test]
fn test_cdp_message_parse_zero_id() {
let msg: CdpMessage = serde_json::from_str(r#"{"id": 0, "method": "x.y"}"#).unwrap();
assert_eq!(msg.id, Some(0));
}
#[test]
fn test_cdp_message_parse_large_id_max_safe_integer() {
let msg: CdpMessage =
serde_json::from_str(r#"{"id": 9007199254740991, "method": "x.y"}"#).unwrap();
assert_eq!(msg.id, Some(9007199254740991));
}
#[test]
fn test_cdp_message_parse_i64_max_id() {
let msg: CdpMessage =
serde_json::from_str(r#"{"id": 9223372036854775807, "method": "x.y"}"#).unwrap();
assert_eq!(msg.id, Some(i64::MAX));
}
#[test]
fn test_cdp_message_parse_float_id_rejected() {
assert!(serde_json::from_str::<CdpMessage>(r#"{"id": 1.5, "method": "x.y"}"#).is_err());
assert!(parse_message(r#"{"id": 1.5, "method": "x.y"}"#).is_none());
}
#[test]
fn test_cdp_message_notification_no_id() {
let msg: CdpMessage = serde_json::from_str(r#"{"method": "Page.loadEventFired"}"#).unwrap();
assert!(msg.id.is_none());
assert_eq!(msg.method, "Page.loadEventFired");
}
#[test]
fn test_cdp_message_with_session_id() {
let raw = r#"{"id": 1, "method": "Runtime.evaluate", "sessionId": "sess-abc123"}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.session_id, Some("sess-abc123".into()));
}
#[test]
fn test_cdp_message_parse_extra_fields_ignored() {
let raw = r#"{"id":1,"method":"Page.reload","extra":"x","another":123}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.id, Some(1));
assert_eq!(msg.method, "Page.reload");
}
#[test]
fn test_cdp_message_parse_params_as_array_accepted() {
let raw = r#"{"id":1,"method":"DOM.querySelectorAll","params":["div","span"]}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.params.unwrap().as_array().unwrap().len(), 2);
}
#[test]
fn test_cdp_message_parse_params_as_string_accepted() {
let raw = r#"{"id":1,"method":"X.y","params":"raw"}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.params.unwrap(), json!("raw"));
}
#[test]
fn test_cdp_message_parse_params_as_number_accepted() {
let raw = r#"{"id":1,"method":"X.y","params":42}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.params.unwrap(), json!(42));
}
#[test]
fn test_cdp_message_parse_params_as_bool_accepted() {
let raw = r#"{"id":1,"method":"X.y","params":true}"#;
let msg: CdpMessage = serde_json::from_str(raw).unwrap();
assert_eq!(msg.params.unwrap(), json!(true));
}
#[test]
fn test_cdp_message_parse_unicode_method() {
let msg: CdpMessage = serde_json::from_str(r#"{"id":1,"method":"Page.日本語テスト"}"#).unwrap();
assert_eq!(msg.method, "Page.日本語テスト");
}
#[test]
fn test_cdp_response_success_serialization() {
let resp = CdpResponse {
id: Some(1),
result: Some(json!({"value": 42})),
error: None,
};
let s = serde_json::to_string(&resp).unwrap();
assert!(s.contains("\"result\""));
assert!(!s.contains("\"error\""));
}
#[test]
fn test_cdp_response_error_serialization() {
let resp = CdpResponse {
id: Some(2),
result: None,
error: Some(CdpError {
code: -32601,
message: "not found".into(),
}),
};
let s = serde_json::to_string(&resp).unwrap();
assert!(s.contains("\"error\""));
assert!(!s.contains("\"result\""));
assert!(s.contains("-32601"));
assert!(s.contains("not found"));
}
#[test]
fn test_cdp_event_with_params() {
let ev = CdpEvent {
method: "Page.loadEventFired".into(),
params: Some(json!({"timestamp": 12345.0})),
};
let s = serde_json::to_string(&ev).unwrap();
assert!(s.contains("\"method\""));
assert!(s.contains("\"params\""));
}
#[test]
fn test_cdp_event_without_params() {
let ev = CdpEvent {
method: "DOM.documentUpdated".into(),
params: None,
};
let s = serde_json::to_string(&ev).unwrap();
assert!(!s.contains("\"params\""));
}
struct EchoHandler {
name: &'static str,
}
impl DomainHandler for EchoHandler {
fn domain_name(&self) -> &'static str {
self.name
}
fn handle_command(
&self,
cmd: &str,
params: Value,
_: &dyn EventSender,
) -> Result<Value, CdpError> {
Ok(json!({"command": cmd, "params": params}))
}
}
#[test]
fn test_dispatch_no_dot_in_method() {
let reg = DomainRegistry::<EchoHandler>::new();
reg.register(EchoHandler { name: "Page" }).unwrap();
assert!(reg.dispatch_command("Page", json!({}), noop()).is_some());
}
#[test]
fn test_dispatch_multiple_dots_in_method() {
let reg = DomainRegistry::<EchoHandler>::new();
reg.register(EchoHandler { name: "Page" }).unwrap();
let r = reg
.dispatch_command("Page.navigate.to.url", json!({}), noop())
.unwrap()
.unwrap();
assert_eq!(r["command"], "Page.navigate.to.url");
}
#[test]
fn test_dispatch_empty_method() {
let reg = DomainRegistry::<EchoHandler>::new();
reg.register(EchoHandler { name: "Page" }).unwrap();
assert!(reg.dispatch_command("", json!({}), noop()).is_none());
}
#[test]
fn test_dispatch_unregistered_domain() {
let reg = DomainRegistry::<EchoHandler>::new();
reg.register(EchoHandler { name: "Page" }).unwrap();
assert!(reg
.dispatch_command("Network.enable", json!({}), noop())
.is_none());
}
#[test]
fn test_dispatch_case_sensitive_domain() {
let reg = DomainRegistry::<EchoHandler>::new();
reg.register(EchoHandler { name: "Page" }).unwrap();
assert!(reg
.dispatch_command("page.navigate", json!({}), noop())
.is_none());
}
#[test]
fn test_registry_empty_name_handler() {
let reg = DomainRegistry::<EchoHandler>::new();
assert!(reg.register(EchoHandler { name: "" }).is_ok());
}
#[test]
fn test_registry_multiple_handlers_independent() {
let reg = DomainRegistry::<EchoHandler>::new();
reg.register(EchoHandler { name: "Page" }).unwrap();
reg.register(EchoHandler { name: "Runtime" }).unwrap();
reg.register(EchoHandler { name: "DOM" }).unwrap();
assert!(reg
.dispatch_command("Page.enable", json!({}), noop())
.is_some());
assert!(reg
.dispatch_command("Runtime.evaluate", json!({"expr": "1"}), noop())
.is_some());
assert!(reg
.dispatch_command("DOM.getDocument", json!({}), noop())
.is_some());
assert!(reg
.dispatch_command("Network.enable", json!({}), noop())
.is_none());
}
#[test]
fn test_registry_duplicate_rejected_preserves_original() {
let reg = DomainRegistry::<EchoHandler>::new();
reg.register(EchoHandler { name: "Page" }).unwrap();
let dup = reg.register(EchoHandler { name: "Page" });
assert!(dup.is_err());
assert!(dup.unwrap_err().contains("already registered"));
let r = reg
.dispatch_command("Page.enable", json!({}), noop())
.unwrap()
.unwrap();
assert_eq!(r["command"], "Page.enable");
}
#[test]
fn test_registry_dispatch_recovers_after_handler_error() {
let reg = DomainRegistry::<ErrorAlways>::new();
reg.register(ErrorAlways).unwrap();
let r1 = reg
.dispatch_command("ErrDomain.failing", json!({}), noop())
.unwrap();
assert!(r1.is_err());
assert_eq!(r1.unwrap_err().code, -32000);
let r2 = reg
.dispatch_command("ErrDomain.again", json!({}), noop())
.unwrap();
assert!(r2.is_err());
let r3 = reg.dispatch_command("Unknown.method", json!({}), noop());
assert!(
r3.is_none(),
"unknown domain still returns None after errors"
);
}
#[test]
fn test_registry_lock_not_poisoned_after_catch_unwind_style_panic() {
let reg = DomainRegistry::<ErrorAlways>::new();
reg.register(ErrorAlways).unwrap();
for _ in 0..50 {
let _ = reg.dispatch_command("ErrDomain.x", json!({}), noop());
}
assert!(reg.has_domain("ErrDomain"));
}
#[test]
fn test_concurrent_dispatch_thread_safe() {
let spy = Arc::new(SpyHandler::new("Page"));
let reg = Arc::new(DomainRegistry::<Observed>::new());
reg.register(Observed::new(Arc::clone(&spy))).unwrap();
let mut handles = Vec::new();
for i in 0..8 {
let reg = Arc::clone(®);
handles.push(thread::spawn(move || {
for _ in 0..100 {
let r = reg.dispatch_command("Page.navigate", json!({"i": i}), noop());
assert!(r.is_some(), "concurrent dispatch must succeed");
assert!(r.unwrap().is_ok());
}
}));
}
for h in handles {
h.join()
.expect("no thread should panic under concurrent dispatch");
}
assert_eq!(spy.commands().len(), 8 * 100);
}
#[test]
fn test_concurrent_register_and_dispatch() {
let reg = Arc::new(DomainRegistry::<EchoHandler>::new());
reg.register(EchoHandler { name: "Page" }).unwrap();
let mut handles = Vec::new();
for d in ["A", "B", "C", "D"] {
let reg = Arc::clone(®);
handles.push(thread::spawn(move || {
let _ = reg.register(EchoHandler { name: d });
}));
}
for _ in 0..4 {
let reg = Arc::clone(®);
handles.push(thread::spawn(move || {
let _ = reg.dispatch_command("Page.enable", json!({}), noop());
}));
}
for h in handles {
let _ = h.join();
}
assert!(reg.has_domain("Page"));
}
#[test]
fn test_server_config_builder_minimal() {
let config = cdp_server::ServerConfig::builder().build();
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 9222);
assert_eq!(config.http_timeout_seconds, 30);
assert_eq!(config.max_sessions, 100);
}
#[test]
fn test_server_config_builder_custom_host() {
let config = cdp_server::ServerConfig::builder()
.host("0.0.0.0")
.port(8080)
.build();
assert_eq!(config.host, "0.0.0.0");
assert_eq!(config.port, 8080);
}
#[test]
fn test_c8_config_http_timeout_seconds_settable() {
let config = cdp_server::ServerConfig::builder()
.http_timeout_seconds(120)
.build();
assert_eq!(config.http_timeout_seconds, 120);
}
#[test]
fn test_c8_config_http_timeout_zero_boundary() {
let config = cdp_server::ServerConfig::builder()
.http_timeout_seconds(0)
.build();
assert_eq!(config.http_timeout_seconds, 0);
}
#[test]
fn test_c8_config_max_sessions_settable() {
let config = cdp_server::ServerConfig::builder().max_sessions(1).build();
assert_eq!(config.max_sessions, 1);
}
#[test]
fn test_c8_config_all_optional_fields() {
let config = cdp_server::ServerConfig::builder()
.browser_name("Custom/1.0")
.user_agent("UA/1.0")
.v8_version("12.0")
.webkit_version("537.36")
.build();
assert_eq!(config.browser_name, "Custom/1.0");
assert_eq!(config.user_agent.as_deref(), Some("UA/1.0"));
assert_eq!(config.v8_version.as_deref(), Some("12.0"));
assert_eq!(config.webkit_version.as_deref(), Some("537.36"));
}
#[test]
fn test_cdp_error_clone() {
let e1 = CdpError {
code: -32601,
message: "test".into(),
};
let e2 = e1.clone();
assert_eq!(e1.code, e2.code);
assert_eq!(e1.message, e2.message);
}
#[test]
fn test_cdp_error_debug() {
let e = CdpError {
code: -32601,
message: "not found".into(),
};
let d = format!("{:?}", e);
assert!(d.contains("-32601"));
assert!(d.contains("not found"));
}
#[test]
fn test_error_constants_match_jsonrpc_2_0() {
assert_eq!(ERR_INVALID_REQUEST, -32600);
assert_eq!(ERR_METHOD_NOT_FOUND, -32601);
}
#[test]
fn test_ok_response_carries_result() {
let r = ok_response(Some(5), json!({"x": 1}));
assert_eq!(r.id, Some(5));
assert!(r.result.is_some());
assert!(r.error.is_none());
}
#[test]
fn test_ok_empty_returns_empty_object() {
let r = ok_empty(Some(9));
assert_eq!(r.result, Some(json!({})));
assert!(r.error.is_none());
}
#[test]
fn test_error_response_id_propagated() {
let r = error_response(Some(123), -32601, "missing");
assert_eq!(r.id, Some(123));
assert_eq!(r.error.unwrap().code, -32601);
}
#[test]
fn test_registry_as_registry_dispatch() {
let spy = Arc::new(SpyHandler::new("Page"));
let reg: Arc<DomainRegistry<Observed>> = Arc::new(DomainRegistry::new());
reg.register(Observed::new(Arc::clone(&spy))).unwrap();
let dispatch: Arc<dyn RegistryDispatch> = reg;
assert!(dispatch.has_domain("Page"));
assert!(!dispatch.has_domain("Network"));
let r = dispatch.dispatch_command("Page.navigate", json!({}), noop());
assert!(r.is_some());
dispatch.notify_session_created("Page", "s1");
dispatch.notify_session_destroyed(&["Page".to_string()], "s1");
assert_eq!(spy.created_ids(), vec!["s1".to_string()]);
assert_eq!(spy.destroyed_ids(), vec!["s1".to_string()]);
}