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")); }