Files
claw/tests/browser_ws_protocol_test.rs
木炎 bdf8e12246 feat: align browser callback runtime and export flows
Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:44:53 +08:00

196 lines
6.6 KiB
Rust

use serde_json::{json, Value};
use sgclaw::browser::ws_protocol::{decode_callback_frame, encode_v1_action};
use sgclaw::pipe::Action;
#[test]
fn encodes_navigate_frame_exactly_as_browser_array() {
let request = encode_v1_action(
&Action::Navigate,
&json!({ "url": "https://www.baidu.com" }),
"https://www.zhihu.com/hot",
Some("req42"),
)
.unwrap();
assert_eq!(
request.payload,
r#"["https://www.zhihu.com/hot","sgHideBrowserCallAfterLoaded","https://www.baidu.com","callBackJsToCpp(\"https://www.zhihu.com/hot@_@https://www.baidu.com@_@sgclaw_cb_req42@_@sgHideBrowserCallAfterLoaded@_@\")"]"#
);
let callback = request.callback.unwrap();
assert_eq!(callback.request_id, "req42");
assert_eq!(callback.callback_name, "sgclaw_cb_req42");
assert_eq!(callback.source_url, "https://www.zhihu.com/hot");
assert_eq!(callback.target_url, "https://www.baidu.com");
assert_eq!(callback.action_url, "sgHideBrowserCallAfterLoaded");
}
#[test]
fn encodes_get_text_frame_with_documented_callback_action_url() {
let request = encode_v1_action(
&Action::GetText,
&json!({
"target_url": "https://www.zhihu.com/hot",
"selector": "#content"
}),
"https://www.zhihu.com/hot",
Some("req42"),
)
.unwrap();
let payload: Value = serde_json::from_str(&request.payload).unwrap();
assert_eq!(
payload,
json!([
"https://www.zhihu.com/hot",
"sgBrowserExcuteJsCodeByArea",
"https://www.zhihu.com/hot",
"(function(){const el=document.querySelector(\"#content\");if(!el){throw new Error(\"selector not found: #content\");}const text=el.innerText ?? el.textContent ?? \"\";callBackJsToCpp(\"https://www.zhihu.com/hot@_@https://www.zhihu.com/hot@_@sgclaw_cb_req42@_@sgBrowserExcuteJsCodeByArea@_@\"+String(text));})();",
"hide"
])
);
let callback = request.callback.unwrap();
assert_eq!(callback.request_id, "req42");
assert_eq!(callback.callback_name, "sgclaw_cb_req42");
assert_eq!(callback.source_url, "https://www.zhihu.com/hot");
assert_eq!(callback.target_url, "https://www.zhihu.com/hot");
assert_eq!(callback.action_url, "sgBrowserExcuteJsCodeByArea");
}
#[test]
fn decodes_callback_payload_from_browser_frame() {
let callback = decode_callback_frame(
r#"["https://www.zhihu.com/hot","callBackJsToCpp","https://www.zhihu.com/hot@_@https://www.zhihu.com/hot@_@sgclaw_cb_req42@_@sgBrowserExcuteJsCodeByArea@_@天气"]"#,
)
.unwrap();
assert_eq!(callback.source_url, "https://www.zhihu.com/hot");
assert_eq!(callback.target_url, "https://www.zhihu.com/hot");
assert_eq!(callback.callback_name, "sgclaw_cb_req42");
assert_eq!(callback.action_url, "sgBrowserExcuteJsCodeByArea");
assert_eq!(callback.response_text, "天气");
}
#[test]
fn rejects_malformed_callback_frames_and_missing_request_ids() {
let malformed = decode_callback_frame(
r#"["https://www.zhihu.com/hot","callBackJsToCpp","https://www.zhihu.com/hot@_@too-short"]"#,
)
.unwrap_err();
assert!(malformed.to_string().contains("malformed callback payload"));
let wrong_function = decode_callback_frame(
r#"["https://www.zhihu.com/hot","sgBrowerserOpenPage","0"]"#,
)
.unwrap_err();
assert!(wrong_function
.to_string()
.contains("callback frame must target callBackJsToCpp"));
let missing_request_id = encode_v1_action(
&Action::Eval,
&json!({
"target_url": "https://www.zhihu.com/hot",
"script": "2 + 2"
}),
"https://www.zhihu.com/hot",
None,
)
.unwrap_err();
assert!(missing_request_id
.to_string()
.contains("request_id is required"));
}
#[test]
fn eval_uses_documented_js_opcode_for_callback_action_url() {
let request = encode_v1_action(
&Action::Eval,
&json!({
"target_url": "https://www.zhihu.com/hot",
"script": "2 + 2"
}),
"https://www.zhihu.com/hot",
Some("req-eval"),
)
.unwrap();
let callback = request.callback.unwrap();
assert_eq!(callback.callback_name, "sgclaw_cb_req-eval");
assert_eq!(callback.action_url, "sgBrowserExcuteJsCodeByArea");
let payload: Value = serde_json::from_str(&request.payload).unwrap();
let js = payload[3].as_str().unwrap();
assert!(js.contains("callBackJsToCpp(\"https://www.zhihu.com/hot@_@https://www.zhihu.com/hot@_@sgclaw_cb_req-eval@_@sgBrowserExcuteJsCodeByArea@_@\"+String(result))"));
}
#[test]
fn covers_supported_v1_action_mapping_and_rejects_unsupported_actions() {
let cases = vec![
(
Action::Navigate,
json!({ "url": "https://www.baidu.com" }),
Some("req-nav"),
"sgHideBrowserCallAfterLoaded",
true,
),
(
Action::Click,
json!({
"target_url": "https://www.zhihu.com/hot",
"selector": "#submit"
}),
None,
"sgBrowserExcuteJsCodeByArea",
false,
),
(
Action::Type,
json!({
"target_url": "https://www.zhihu.com/hot",
"selector": "#kw",
"text": "天气"
}),
None,
"sgBrowserExcuteJsCodeByArea",
false,
),
(
Action::GetText,
json!({
"target_url": "https://www.zhihu.com/hot",
"selector": "#content"
}),
Some("req-get-text"),
"sgBrowserExcuteJsCodeByArea",
true,
),
(
Action::Eval,
json!({
"target_url": "https://www.zhihu.com/hot",
"script": "2 + 2"
}),
Some("req-eval"),
"sgBrowserExcuteJsCodeByArea",
true,
),
];
for (action, params, request_id, browser_function, expects_callback) in cases {
let request = encode_v1_action(&action, &params, "https://www.zhihu.com/hot", request_id)
.unwrap();
let payload: Value = serde_json::from_str(&request.payload).unwrap();
assert_eq!(payload[1], json!(browser_function), "action={action:?}");
assert_eq!(request.callback.is_some(), expects_callback, "action={action:?}");
}
let unsupported = encode_v1_action(
&Action::GetHtml,
&json!({ "selector": "body" }),
"https://www.zhihu.com/hot",
None,
)
.unwrap_err();
assert!(unsupported.to_string().contains("unsupported browser ws action"));
}