use std::io::Write;
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::Duration;
use bao_cdp_client::transport::{Transport, TransportKind, WebSocketTransport};
use bao_cdp_client::CdpError;
use serde_json::{json, Value};
fn apply_mask(payload: &mut [u8], mask: &[u8; 4]) {
for (i, b) in payload.iter_mut().enumerate() {
*b ^= mask[i % 4];
}
}
fn encode_text_unmasked(payload: &str) -> Vec<u8> {
let opcode = 0x1u8;
let fin = 0x80u8;
let payload_bytes = payload.as_bytes();
let len = payload_bytes.len();
let mut buf = Vec::with_capacity(payload_bytes.len() + 14);
buf.push(fin | opcode);
if len < 126 {
buf.push(len as u8);
} else if len <= u16::MAX as usize {
buf.push(126u8);
buf.extend_from_slice(&(len as u16).to_be_bytes());
} else {
buf.push(127u8);
buf.extend_from_slice(&(len as u64).to_be_bytes());
}
buf.extend_from_slice(payload_bytes);
buf
}
fn read_one_frame(stream: &mut TcpStream) -> Option<Vec<u8>> {
let mut decoder = bao_cdp::ws_codec::FrameDecoder::new();
let header = match decoder.decode_frame(stream) {
Ok(Some(h)) => h,
_ => return None,
};
let payload = if header.mask {
let mask = decoder.take_mask();
let mut p = decoder.take_payload(&header);
apply_mask(&mut p, &mask);
p
} else {
decoder.take_payload(&header)
};
Some(payload)
}
struct MiniCdpServer {
addr: String,
handle: Option<thread::JoinHandle<()>>,
}
impl MiniCdpServer {
fn new<F>(handler: F) -> Self
where
F: FnOnce(TcpStream) + Send + 'static,
{
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let handle = thread::spawn(move || {
if let Ok((stream, _)) = listener.accept() {
handler(stream);
}
});
Self {
addr,
handle: Some(handle),
}
}
fn url(&self) -> String {
format!("ws://{}/devtools/page/test", self.addr)
}
}
impl Drop for MiniCdpServer {
fn drop(&mut self) {
if let Some(h) = self.handle.take() {
h.join().ok();
}
}
}
#[test]
fn e2e_external_ws_handshake_full_chain() {
use bao_cdp_client::Browser;
let server = MiniCdpServer::new(|mut stream| {
let _ = bao_cdp::ws_handshake::server_handshake(&mut stream);
thread::sleep(Duration::from_millis(100));
});
let browser = Browser::connect(&server.url()).expect("route ws://");
assert!(browser.is_websocket());
assert_eq!(browser.transport_kind(), TransportKind::WebSocket);
let mut transport = browser.build_websocket_transport().expect("ws handshake");
assert_eq!(transport.kind(), TransportKind::WebSocket);
let _ = transport.close();
}
#[test]
fn e2e_external_ws_handshake_refused_returns_error() {
use bao_cdp_client::Browser;
let url = "ws://127.0.0.1:1/x";
let browser = Browser::connect(url).expect("route succeeds");
assert!(browser.is_websocket());
let err = browser.build_websocket_transport().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("ConnectionFailed") || msg.contains("ws connect") || msg.contains("refused"),
"got: {msg}"
);
}
#[test]
fn e2e_external_command_round_trip_simple() {
let server = MiniCdpServer::new(|mut stream| {
if bao_cdp::ws_handshake::server_handshake(&mut stream).is_err() {
return;
}
let payload = match read_one_frame(&mut stream) {
Some(p) => p,
None => return,
};
let v: Value = match serde_json::from_slice(&payload) {
Ok(v) => v,
Err(_) => return,
};
let response = json!({
"id": v["id"],
"result": {"echoedMethod": v["method"]},
});
let frame = encode_text_unmasked(&serde_json::to_string(&response).unwrap());
let _ = stream.write_all(&frame);
let _ = stream.flush();
thread::sleep(Duration::from_millis(50));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
let r = t
.send_command("Page.navigate", json!({"url":"https://example.com"}), None)
.expect("round-trip");
assert_eq!(r["echoedMethod"], "Page.navigate");
}
#[test]
fn e2e_external_command_round_trip_multiple() {
let server = MiniCdpServer::new(|mut stream| {
if bao_cdp::ws_handshake::server_handshake(&mut stream).is_err() {
return;
}
for i in 0..5 {
let payload = match read_one_frame(&mut stream) {
Some(p) => p,
None => return,
};
let v: Value = serde_json::from_slice(&payload).unwrap();
let response = json!({
"id": v["id"],
"result": {"seq": i, "method": v["method"]},
});
let frame = encode_text_unmasked(&serde_json::to_string(&response).unwrap());
let _ = stream.write_all(&frame);
let _ = stream.flush();
}
thread::sleep(Duration::from_millis(50));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
for i in 0..5 {
let r = t
.send_command(&format!("Page.cmd{i}"), json!({}), None)
.unwrap_or_else(|e| panic!("cmd {i} failed: {e:?}"));
assert_eq!(r["seq"], i, "response seq must match loop index");
}
}
#[test]
fn e2e_external_command_with_session_id_passes_through() {
let server = MiniCdpServer::new(|mut stream| {
if bao_cdp::ws_handshake::server_handshake(&mut stream).is_err() {
return;
}
let payload = read_one_frame(&mut stream).expect("frame");
let v: Value = serde_json::from_slice(&payload).unwrap();
let session = v.get("sessionId").cloned().unwrap_or(Value::Null);
let response = json!({"id": v["id"], "result": {"echoedSession": session}});
let frame = encode_text_unmasked(&serde_json::to_string(&response).unwrap());
let _ = stream.write_all(&frame);
let _ = stream.flush();
thread::sleep(Duration::from_millis(50));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
let r = t
.send_command(
"Target.sendMessageToTarget",
json!({"msg":"hi"}),
Some("TARGET-99"),
)
.unwrap();
assert_eq!(r["echoedSession"], "TARGET-99");
}
#[test]
fn e2e_external_event_subscription_single_push() {
let server = MiniCdpServer::new(|mut stream| {
if bao_cdp::ws_handshake::server_handshake(&mut stream).is_err() {
return;
}
let event = json!({
"method": "Page.frameNavigated",
"params": {"url": "https://example.com"},
});
let frame = encode_text_unmasked(&serde_json::to_string(&event).unwrap());
let _ = stream.write_all(&frame);
let _ = stream.flush();
thread::sleep(Duration::from_millis(200));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
t.set_event_timeout(Duration::from_secs(2));
let ev = t.recv_event().unwrap().expect("expected event");
assert_eq!(ev.method, "Page.frameNavigated");
assert_eq!(ev.params["url"], "https://example.com");
}
#[test]
fn e2e_external_event_subscription_with_session_id() {
let server = MiniCdpServer::new(|mut stream| {
if bao_cdp::ws_handshake::server_handshake(&mut stream).is_err() {
return;
}
let event = json!({
"method": "Network.requestWillBeSent",
"params": {"requestId": "REQ-1"},
"sessionId": "TARGET-SUB",
});
let frame = encode_text_unmasked(&serde_json::to_string(&event).unwrap());
let _ = stream.write_all(&frame);
let _ = stream.flush();
thread::sleep(Duration::from_millis(200));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
t.set_event_timeout(Duration::from_secs(2));
let ev = t.recv_event().unwrap().expect("expected event");
assert_eq!(ev.method, "Network.requestWillBeSent");
assert_eq!(ev.session_id.as_deref(), Some("TARGET-SUB"));
}
#[test]
fn e2e_external_event_subscription_sequence() {
let server = MiniCdpServer::new(|mut stream| {
if bao_cdp::ws_handshake::server_handshake(&mut stream).is_err() {
return;
}
let events = vec![
json!({"method":"Page.frameStartedLoading","params":{"frameId":"F"}}),
json!({"method":"Page.frameNavigated","params":{"frameId":"F","url":"https://x"}}),
json!({"method":"Page.frameStoppedLoading","params":{"frameId":"F"}}),
];
for ev in events {
let frame = encode_text_unmasked(&serde_json::to_string(&ev).unwrap());
let _ = stream.write_all(&frame);
let _ = stream.flush();
thread::sleep(Duration::from_millis(20));
}
thread::sleep(Duration::from_millis(200));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
t.set_event_timeout(Duration::from_secs(2));
let mut methods = Vec::new();
while let Ok(Some(ev)) = t.recv_event() {
methods.push(ev.method);
}
assert_eq!(methods.len(), 3, "expected 3 events, got {}", methods.len());
assert_eq!(methods[0], "Page.frameStartedLoading");
assert_eq!(methods[1], "Page.frameNavigated");
assert_eq!(methods[2], "Page.frameStoppedLoading");
}
#[test]
fn e2e_external_jsonrpc_error_returned_as_protocol_error() {
let server = MiniCdpServer::new(|mut stream| {
if bao_cdp::ws_handshake::server_handshake(&mut stream).is_err() {
return;
}
let payload = read_one_frame(&mut stream).expect("frame");
let v: Value = serde_json::from_slice(&payload).unwrap();
let response = json!({
"id": v["id"],
"error": {"code": -32601, "message": "method not found"},
});
let frame = encode_text_unmasked(&serde_json::to_string(&response).unwrap());
let _ = stream.write_all(&frame);
let _ = stream.flush();
thread::sleep(Duration::from_millis(50));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
let err = t.send_command("Bogus.method", json!({}), None).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("CDP protocol error") || msg.contains("method not found"),
"got: {msg}"
);
}
#[test]
fn e2e_external_command_timeout_returns_timeout_error() {
let server = MiniCdpServer::new(|mut stream| {
let _ = bao_cdp::ws_handshake::server_handshake(&mut stream);
thread::sleep(Duration::from_millis(500));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
t.set_command_timeout(Duration::from_millis(50));
let err = t.send_command("X", json!({}), None).unwrap_err();
match err {
CdpError::Timeout(_) | CdpError::IoError(_) | CdpError::ConnectionClosed => {}
other => panic!("expected timeout/io/closed, got {:?}", other),
}
}
#[test]
fn e2e_external_close_then_send_returns_connection_closed() {
let server = MiniCdpServer::new(|mut stream| {
let _ = bao_cdp::ws_handshake::server_handshake(&mut stream);
thread::sleep(Duration::from_millis(100));
});
let mut t = WebSocketTransport::connect(&server.url()).unwrap();
t.close().unwrap();
let err = t.send_command("X", json!({}), None).unwrap_err();
assert!(matches!(err, CdpError::ConnectionClosed));
}
#[test]
fn e2e_real_chrome_navigation_and_screenshot() {
let url = match std::env::var("BAO_TEST_CHROME_URL") {
Ok(v) => v,
Err(_) => {
eprintln!("[skip] 环境不可用: BAO_TEST_CHROME_URL not set (real chrome E2E)");
return;
}
};
let mut t = WebSocketTransport::connect(&url).expect("ws connect to real chrome");
t.set_command_timeout(Duration::from_secs(10));
let r = t
.send_command("Target.createTarget", json!({"url":"about:blank"}), None)
.expect("createTarget");
let target_id = r["targetId"]
.as_str()
.expect("targetId in createTarget response")
.to_string();
let r = t
.send_command(
"Target.attachToTarget",
json!({"targetId": target_id, "flatten": true}),
None,
)
.expect("attachToTarget");
let session_id = r["sessionId"]
.as_str()
.expect("sessionId in attachToTarget response")
.to_string();
let nav = t
.send_command(
"Page.navigate",
json!({"url":"https://example.com"}),
Some(&session_id),
)
.expect("navigate");
assert!(nav["frameId"].is_string(), "navigate returns frameId");
let deadline = std::time::Instant::now() + Duration::from_secs(5);
let mut ready = false;
while std::time::Instant::now() < deadline {
let tree = match t.send_command("Page.getFrameTree", json!({}), Some(&session_id)) {
Ok(v) => v,
Err(_) => {
thread::sleep(Duration::from_millis(100));
continue;
}
};
let main_url = tree["frameTree"]["frame"]["url"].as_str().unwrap_or("");
if main_url.contains("example.com") {
ready = true;
break;
}
thread::sleep(Duration::from_millis(100));
}
let _ = ready;
let r = t
.send_command(
"Page.captureScreenshot",
json!({"format":"png"}),
Some(&session_id),
)
.expect("screenshot");
assert!(
r["data"].is_string(),
"screenshot must return base64 data, got: {r}"
);
let data = r["data"].as_str().expect("data as str");
assert!(!data.is_empty(), "screenshot base64 must be non-empty");
let _ = t.send_command("Target.closeTarget", json!({"targetId": target_id}), None);
}
#[test]
fn e2e_real_chrome_runtime_evaluate() {
let url = match std::env::var("BAO_TEST_CHROME_URL") {
Ok(v) => v,
Err(_) => {
eprintln!("[skip] 环境不可用: BAO_TEST_CHROME_URL not set (real chrome E2E)");
return;
}
};
let mut t = WebSocketTransport::connect(&url).expect("ws connect");
t.set_command_timeout(Duration::from_secs(10));
let r = t
.send_command("Target.createTarget", json!({"url":"about:blank"}), None)
.expect("createTarget");
let target_id = r["targetId"]
.as_str()
.expect("targetId in createTarget response")
.to_string();
let r = t
.send_command(
"Target.attachToTarget",
json!({"targetId": target_id, "flatten": true}),
None,
)
.expect("attachToTarget");
let session_id = r["sessionId"]
.as_str()
.expect("sessionId in attachToTarget response")
.to_string();
let r = t
.send_command(
"Runtime.evaluate",
json!({"expression": "1 + 1"}),
Some(&session_id),
)
.expect("evaluate");
assert_eq!(r["result"]["value"], 2, "1+1 must equal 2, got: {r}");
let _ = t.send_command("Target.closeTarget", json!({"targetId": target_id}), None);
}