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>
This commit is contained in:
195
tests/browser_ws_protocol_test.rs
Normal file
195
tests/browser_ws_protocol_test.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
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, ¶ms, "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"));
|
||||
}
|
||||
Reference in New Issue
Block a user