use bao_cdp_client::bridge::{MockServoBackend, ServoBackend};
use bao_cdp_client::dispatch_command;
use serde_json::{json, Value};
use std::sync::Arc;
fn backend() -> Arc<dyn ServoBackend> {
Arc::new(MockServoBackend::new())
}
fn assert_b_class_response_has_schema(resp: &Value, method: &str) {
if is_eval_path_method(method) {
assert!(
resp.get("result").is_some(),
"[{method}] eval-path response missing 'result' field: {resp}"
);
let result = &resp["result"];
assert!(
result.get("type").is_some(),
"[{method}] result missing 'type' field: {result}"
);
assert!(
result.get("value").is_some(),
"[{method}] result missing 'value' field: {result}"
);
} else if is_void_method(method) {
assert!(
resp.is_object(),
"[{method}] void-path response must be a JSON object: {resp}"
);
} else {
assert!(
resp.is_object(),
"[{method}] direct-path response must be a JSON object: {resp}"
);
let obj = resp.as_object().unwrap();
assert!(
!obj.is_empty(),
"[{method}] direct-path response is empty object {{}} — stub risk! got: {resp}"
);
}
}
fn assert_response_value_is_iife(resp: &Value, method: &str) {
let value = resp["result"]["value"].as_str().unwrap_or_else(|| {
panic!(
"[{method}] result.value is not a string: {}",
resp["result"]["value"]
)
});
assert!(
value.contains("(function(){"),
"[{method}] IIFE missing '(function(){{' wrapper: {value}"
);
assert!(
value.ends_with("})()") || value.ends_with("});"),
"[{method}] IIFE missing closing '}})()' or '}});': {value}"
);
}
fn is_eval_path_method(method: &str) -> bool {
const EVAL_PATH_METHODS: &[&str] = &[
"Page.title",
"Page.url",
"Page.content",
"Page.goBack",
"Page.goForward",
"Page.emulateMedia",
"Page.addScriptTag",
"Page.addStyleTag",
"Page.exposeFunction",
"Page.tap",
"Page.hover",
"Page.focus",
"Page.type",
"Page.fill",
"Page.press",
"Page.check",
"Page.uncheck",
"Page.selectOption",
"Page.setInputFiles",
"Page.opener",
"Page.requestGC",
"ElementHandle.contentFrame",
"ElementHandle.ownerFrame",
"ElementHandle.getAttribute",
"ElementHandle.innerHTML",
"ElementHandle.innerText",
"ElementHandle.textContent",
"ElementHandle.scrollIntoViewIfNeeded",
"ElementHandle.isChecked",
"ElementHandle.isDisabled",
"ElementHandle.isEditable",
"ElementHandle.isEnabled",
"ElementHandle.isHidden",
"ElementHandle.isVisible",
"JSHandle.evaluate",
"JSHandle.evaluateHandle",
"JSHandle.getProperty",
"JSHandle.jsonValue",
];
EVAL_PATH_METHODS.contains(&method)
}
fn is_void_method(method: &str) -> bool {
const VOID_METHODS: &[&str] = &[
"Page.setDefaultNavigationTimeout",
"Page.setDefaultTimeout",
"Page.setViewport",
"Page.waitForLoadState",
"Page.waitForURL",
"Page.waitForRequest",
"Page.waitForResponse",
"Page.waitForEvent",
"ElementHandle.waitForElementState",
"ElementHandle.waitForSelector",
"JSHandle.dispose",
];
VOID_METHODS.contains(&method)
}
fn is_known_direct_field_method(method: &str) -> bool {
const DIRECT_FIELD_METHODS: &[&str] = &[
"JSHandle.asElement", "JSHandle.getProperties", "Page.viewport", "Page.frames", "Page.mainFrame", "Page.screenshot", "Page.pdf", "ElementHandle.isChecked", "ElementHandle.isDisabled",
"ElementHandle.isEditable",
"ElementHandle.isEnabled",
"ElementHandle.isHidden",
"ElementHandle.isVisible",
];
DIRECT_FIELD_METHODS.contains(&method)
}
const B_CLASS_METHODS: &[&str] = &[
"Page.title",
"Page.url",
"Page.content",
"Page.viewport",
"Page.setViewport",
"Page.opener",
"Page.frames",
"Page.mainFrame",
"Page.setDefaultNavigationTimeout",
"Page.setDefaultTimeout",
"Page.waitForLoadState",
"Page.waitForURL",
"Page.waitForRequest",
"Page.waitForResponse",
"Page.waitForEvent",
"Page.goBack",
"Page.goForward",
"Page.emulateMedia",
"Page.addScriptTag",
"Page.addStyleTag",
"Page.exposeFunction",
"Page.pdf",
"Page.screenshot",
"Page.tap",
"Page.hover",
"Page.focus",
"Page.type",
"Page.fill",
"Page.press",
"Page.check",
"Page.uncheck",
"Page.selectOption",
"Page.setInputFiles",
"Page.requestGC",
"ElementHandle.contentFrame",
"ElementHandle.ownerFrame",
"ElementHandle.getAttribute",
"ElementHandle.innerHTML",
"ElementHandle.innerText",
"ElementHandle.textContent",
"ElementHandle.isChecked",
"ElementHandle.isDisabled",
"ElementHandle.isEditable",
"ElementHandle.isEnabled",
"ElementHandle.isHidden",
"ElementHandle.isVisible",
"ElementHandle.scrollIntoViewIfNeeded",
"ElementHandle.waitForElementState",
"ElementHandle.waitForSelector",
"JSHandle.asElement",
"JSHandle.dispose",
"JSHandle.evaluate",
"JSHandle.evaluateHandle",
"JSHandle.getProperties",
"JSHandle.getProperty",
"JSHandle.jsonValue",
];
#[test]
fn b_class_all_56_methods_response_has_remote_object_schema() {
let b = backend();
let mut success_count = 0;
let mut iife_count = 0;
for method in B_CLASS_METHODS {
let result = dispatch_command(&*b, method, json!({}), "1");
match result {
Ok(resp) => {
assert_b_class_response_has_schema(&resp, method);
success_count += 1;
if is_eval_path_method(method) {
assert_response_value_is_iife(&resp, method);
iife_count += 1;
}
}
Err(e) => {
if matches!(e, bao_cdp_client::bridge::BridgeError::MethodNotFound(_)) {
panic!(
"[{method}] B 类 method 未被路由 (MethodNotFound) — dispatcher 缺失 arm: {e}"
);
}
success_count += 1;
}
}
}
assert!(
success_count >= 56,
"expected >= 56 B-class methods routed with valid schema, got {success_count}"
);
eprintln!("iife_count = {iife_count} (methods without required params skip IIFE check)");
assert!(
iife_count >= 8,
"expected >= 8 IIFE-eval methods validated (Page.title/url/content/goBack/goForward/opener/requestGC + emulateMedia), got {iife_count}"
);
}
#[test]
fn stub_empty_object_fails_schema_validation() {
let stub_response = json!({});
let method = "Page.title";
let result = std::panic::catch_unwind(|| {
assert_b_class_response_has_schema(&stub_response, method);
});
assert!(
result.is_err(),
"stub empty object {{}} should NOT pass schema validation"
);
}
#[test]
fn stub_missing_value_field_fails_schema_validation() {
let stub_response = json!({ "result": { "type": "string" } });
let method = "Page.url";
let result = std::panic::catch_unwind(|| {
assert_b_class_response_has_schema(&stub_response, method);
});
assert!(
result.is_err(),
"stub missing 'value' field should NOT pass schema validation"
);
}
#[test]
fn stub_non_string_value_fails_iife_validation() {
let stub_response = json!({
"result": {
"type": "string",
"value": 12345 }
});
let method = "Page.content";
let result = std::panic::catch_unwind(|| {
assert_response_value_is_iife(&stub_response, method);
});
assert!(
result.is_err(),
"stub with non-string value should NOT pass IIFE validation"
);
}
#[test]
fn stub_malformed_iife_fails_validation() {
let stub_response = json!({
"result": {
"type": "string",
"value": "function(){ return document.title; }" }
});
let method = "Page.title";
let result = std::panic::catch_unwind(|| {
assert_response_value_is_iife(&stub_response, method);
});
assert!(
result.is_err(),
"stub with malformed IIFE (missing wrapper) should NOT pass validation"
);
}
#[test]
fn b_class_handlers_cover_all_required_entity_fields() {
assert!(
B_CLASS_METHODS.len() >= 52,
"B_CLASS_METHODS should cover at least 52 methods per SPEC, got {}",
B_CLASS_METHODS.len()
);
}
#[test]
fn page_viewport_returns_json_object_not_iife() {
let b = backend();
let resp = dispatch_command(&*b, "Page.viewport", json!({}), "1").unwrap();
assert_b_class_response_has_schema(&resp, "Page.viewport");
assert!(
resp.get("width").is_some() && resp.get("height").is_some(),
"Page.viewport should have 'width' and 'height': {resp}"
);
}
#[test]
fn page_frames_returns_frames_array() {
let b = backend();
let resp = dispatch_command(&*b, "Page.frames", json!({}), "1").unwrap();
assert_b_class_response_has_schema(&resp, "Page.frames");
assert!(
resp.get("frames").is_some(),
"Page.frames should have 'frames' field: {resp}"
);
}
#[test]
fn page_screenshot_returns_base64_data() {
let b = backend();
let result = dispatch_command(&*b, "Page.screenshot", json!({}), "1");
match result {
Ok(resp) => {
assert!(
resp.get("data").is_some() || resp.get("result").is_some(),
"Page.screenshot response should have 'data' or 'result': {resp}"
);
}
Err(e) => {
assert!(
matches!(e, bao_cdp_client::bridge::BridgeError::PageNotFound(_)),
"Page.screenshot error should be PageNotFound, got: {e}"
);
}
}
}