Files
2026-04-14 16:19:46 +08:00

620 lines
16 KiB
Rust

//! Browser network observation E2E tests.
//!
//! Covers the planned `browser network requests` and
//! `browser network request <id>` commands for ACT-882.
use std::fs;
use crate::harness::{
SessionGuard, assert_error_envelope, assert_failure, assert_meta, assert_success,
headless_json, parse_json, skip, start_session, unique_session, url_network_load,
url_network_xhr, wait_page_ready,
};
fn wait_requests_done(session_id: &str, tab_id: &str) {
let out = headless_json(
&[
"browser",
"wait",
"condition",
"window.__ab_requests_done === true",
"--session",
session_id,
"--tab",
tab_id,
"--timeout",
"5000",
],
10,
);
assert_success(&out, "wait requests done");
}
fn clear_requests(session_id: &str, tab_id: &str) {
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
session_id,
"--tab",
tab_id,
"--clear",
],
15,
);
assert_success(&out, "clear network requests");
}
fn request_items(value: &serde_json::Value) -> &[serde_json::Value] {
value["data"]["requests"]
.as_array()
.map(Vec::as_slice)
.unwrap_or(&[])
}
fn start_session_with_max_tracked_requests(
url: &str,
max_tracked_requests: usize,
) -> (String, String) {
let (sid, profile) = unique_session("net");
let max = max_tracked_requests.to_string();
let argv = vec![
"browser".to_string(),
"start".to_string(),
"--mode".to_string(),
"local".to_string(),
"--headless".to_string(),
"--set-session-id".to_string(),
sid.clone(),
"--profile".to_string(),
profile,
"--open-url".to_string(),
url.to_string(),
"--max-tracked-requests".to_string(),
max,
];
let args: Vec<&str> = argv.iter().map(String::as_str).collect();
let out = headless_json(&args, 30);
assert_success(
&out,
&format!("start session {sid} with custom max tracked requests"),
);
let v = parse_json(&out);
let actual_sid = v["data"]["session"]["session_id"]
.as_str()
.unwrap()
.to_string();
let tid = v["data"]["tab"]["tab_id"].as_str().unwrap().to_string();
wait_page_ready(&actual_sid, &tid);
(actual_sid, tid)
}
fn issue_bulk_requests(session_id: &str, tab_id: &str, count: usize, prefix: &str) {
let api_prefix = url_network_xhr().replace("/network-xhr", "/api/data?source=");
let expression = format!(
"await Promise.all(Array.from({{ length: {count} }}, (_, i) => fetch(`{api_prefix}{prefix}-${{i}}`).then(r => r.text())))"
);
let argv = vec![
"browser".to_string(),
"eval".to_string(),
expression,
"--session".to_string(),
session_id.to_string(),
"--tab".to_string(),
tab_id.to_string(),
];
let args: Vec<&str> = argv.iter().map(String::as_str).collect();
let out = headless_json(&args, 30);
assert_success(&out, "issue bulk requests");
}
#[test]
fn network_requests_lists_page_load_requests() {
if skip() {
return;
}
let (sid, tid) = start_session(&url_network_load());
let _guard = SessionGuard::new(&sid);
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
],
15,
);
assert_success(&out, "network requests page load");
let v = parse_json(&out);
let requests = request_items(&v);
assert_eq!(v["command"], "browser network requests");
assert_eq!(v["ok"], true);
assert!(v["error"].is_null());
assert_eq!(v["context"]["session_id"], sid);
assert_eq!(v["context"]["tab_id"], tid);
assert!(requests.len() >= 3, "expected document + css + js requests");
assert!(
requests
.iter()
.any(|req| req["resource_type"] == "Document"),
"document request should be present"
);
assert!(
requests
.iter()
.any(|req| req["resource_type"] == "Stylesheet"),
"stylesheet request should be present"
);
assert!(
requests.iter().any(|req| req["resource_type"] == "Script"),
"script request should be present"
);
assert_eq!(
v["data"]["total"].as_u64().unwrap_or(0),
requests.len() as u64
);
assert_meta(&v);
}
#[test]
fn network_requests_filter_by_type() {
if skip() {
return;
}
let (sid, tid) = start_session(&url_network_xhr());
let _guard = SessionGuard::new(&sid);
wait_requests_done(&sid, &tid);
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
"--type",
"xhr,fetch",
],
15,
);
assert_success(&out, "network requests filter type");
let v = parse_json(&out);
let requests = request_items(&v);
assert_eq!(v["command"], "browser network requests");
assert!(!requests.is_empty(), "xhr/fetch requests should be present");
assert!(
requests
.iter()
.all(|req| { matches!(req["resource_type"].as_str(), Some("XHR") | Some("Fetch")) })
);
assert_eq!(
v["data"]["filtered"].as_u64().unwrap_or(0),
requests.len() as u64
);
assert_meta(&v);
}
#[test]
fn network_requests_clear() {
if skip() {
return;
}
let (sid, tid) = start_session(&url_network_xhr());
let _guard = SessionGuard::new(&sid);
wait_requests_done(&sid, &tid);
let clear_out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
"--clear",
],
15,
);
assert_success(&clear_out, "network requests clear");
let clear_v = parse_json(&clear_out);
assert_eq!(clear_v["command"], "browser network requests");
assert_eq!(clear_v["data"]["cleared"], true);
let list_out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
],
15,
);
assert_success(&list_out, "network requests list after clear");
let list_v = parse_json(&list_out);
let requests = request_items(&list_v);
assert_eq!(requests.len(), 0);
assert_eq!(list_v["data"]["total"], 0);
assert_eq!(list_v["data"]["filtered"], 0);
}
#[test]
fn network_request_detail() {
if skip() {
return;
}
let (sid, tid) = start_session(&url_network_xhr());
let _guard = SessionGuard::new(&sid);
wait_requests_done(&sid, &tid);
let list_out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
"--filter",
"source=fetch",
],
15,
);
assert_success(&list_out, "network requests list for detail");
let list_v = parse_json(&list_out);
let request_id = list_v["data"]["requests"][0]["request_id"]
.as_str()
.expect("request id")
.to_string();
let detail_out = headless_json(
&[
"browser",
"network",
"request",
&request_id,
"--session",
&sid,
"--tab",
&tid,
],
15,
);
assert_success(&detail_out, "network request detail");
let v = parse_json(&detail_out);
assert_eq!(v["command"], "browser network request");
assert_eq!(v["ok"], true);
assert!(v["error"].is_null());
assert_eq!(v["context"]["session_id"], sid);
assert_eq!(v["context"]["tab_id"], tid);
assert_eq!(v["data"]["request"]["request_id"], request_id);
assert!(
v["data"]["request"]["url"]
.as_str()
.is_some_and(|url| url.contains("/api/data?source=fetch"))
);
assert_eq!(v["data"]["request"]["status"], 200);
assert_eq!(
v["data"]["request"]["response_headers"]["x-ab-fixture"],
"api-data"
);
assert!(
v["data"]["request"]["response_body"]
.as_str()
.is_some_and(|body| body.contains("\"source\":\"fetch\""))
);
assert_meta(&v);
}
#[test]
fn network_requests_empty_on_fresh_tab() {
if skip() {
return;
}
let (sid, tid) = start_session("about:blank");
let _guard = SessionGuard::new(&sid);
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
],
15,
);
assert_success(&out, "network requests fresh tab");
let v = parse_json(&out);
let requests = request_items(&v);
assert_eq!(v["command"], "browser network requests");
assert!(
requests.len() <= 1,
"fresh about:blank tab should be empty or minimal"
);
assert!(v["data"]["total"].as_u64().unwrap_or(0) <= 1);
assert_meta(&v);
}
#[test]
fn start_with_custom_buffer_size() {
if skip() {
return;
}
let (sid, tid) = start_session_with_max_tracked_requests(&url_network_xhr(), 10);
let _guard = SessionGuard::new(&sid);
wait_requests_done(&sid, &tid);
clear_requests(&sid, &tid);
issue_bulk_requests(&sid, &tid, 12, "buffer");
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
],
15,
);
assert_success(&out, "network requests custom buffer size");
let v = parse_json(&out);
let requests = request_items(&v);
assert_eq!(v["command"], "browser network requests");
assert!(
requests.len() <= 10,
"buffer should keep at most 10 newest requests"
);
let urls: Vec<&str> = requests
.iter()
.filter_map(|req| req["url"].as_str())
.collect();
assert!(
!urls.iter().any(|url| url.contains("buffer-0"))
&& !urls.iter().any(|url| url.contains("buffer-1")),
"oldest requests should be evicted: {urls:?}"
);
assert!(
urls.iter().any(|url| url.contains("buffer-11")),
"newest request should be retained: {urls:?}"
);
}
#[test]
fn start_default_buffer_size_shown() {
if skip() {
return;
}
let (sid, _tid) = start_session("about:blank");
let _guard = SessionGuard::new(&sid);
let out = headless_json(&["browser", "list-sessions"], 10);
assert_success(&out, "list-sessions with default buffer size");
let v = parse_json(&out);
assert_eq!(v["command"], "browser list-sessions");
let sessions = v["data"]["sessions"].as_array().expect("sessions array");
let session = sessions
.iter()
.find(|s| s["session_id"].as_str() == Some(sid.as_str()))
.expect("current session present in list-sessions");
assert_eq!(session["max_tracked_requests"], 500);
}
#[test]
fn start_invalid_buffer_size_rejected() {
if skip() {
return;
}
let (sid, profile) = unique_session("net-invalid");
let out = headless_json(
&[
"browser",
"start",
"--mode",
"local",
"--headless",
"--set-session-id",
&sid,
"--profile",
&profile,
"--open-url",
"about:blank",
"--max-tracked-requests",
"0",
],
30,
);
assert_failure(&out, "start invalid max-tracked-requests");
let v = parse_json(&out);
assert_error_envelope(&v, "INVALID_ARGUMENT");
}
#[test]
fn network_requests_dump_writes_file() {
if skip() {
return;
}
let (sid, tid) = start_session(&url_network_xhr());
let _guard = SessionGuard::new(&sid);
wait_requests_done(&sid, &tid);
clear_requests(&sid, &tid);
issue_bulk_requests(&sid, &tid, 3, "dump");
let temp = tempfile::tempdir().expect("create temp dir");
let dump_dir = temp.path().join("requests-dump");
let dump_dir_str = dump_dir.to_string_lossy().to_string();
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
"--dump",
"--out",
&dump_dir_str,
],
30,
);
assert_success(&out, "network requests dump writes file");
let v = parse_json(&out);
let requests_path = dump_dir.join("requests.json");
assert!(requests_path.exists(), "requests.json should be created");
let dumped: serde_json::Value =
serde_json::from_slice(&fs::read(&requests_path).expect("read requests.json"))
.expect("requests.json must be valid JSON");
let requests = dumped["requests"].as_array().expect("requests array");
assert_eq!(requests.len(), 3);
assert!(
requests
.iter()
.all(|req| req["url"].is_string() && req["method"].is_string())
);
assert_eq!(
v["data"]["dump"]["path"],
requests_path.to_string_lossy().to_string()
);
assert_eq!(v["data"]["dump"]["count"], 3);
}
#[test]
fn network_requests_dump_includes_body() {
if skip() {
return;
}
let (sid, tid) = start_session(&url_network_xhr());
let _guard = SessionGuard::new(&sid);
wait_requests_done(&sid, &tid);
clear_requests(&sid, &tid);
issue_bulk_requests(&sid, &tid, 1, "dump-body");
let temp = tempfile::tempdir().expect("create temp dir");
let dump_dir = temp.path().join("requests-dump-body");
let dump_dir_str = dump_dir.to_string_lossy().to_string();
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
"--dump",
"--out",
&dump_dir_str,
],
30,
);
assert_success(&out, "network requests dump includes body");
let requests_path = dump_dir.join("requests.json");
let dumped: serde_json::Value =
serde_json::from_slice(&fs::read(&requests_path).expect("read requests.json"))
.expect("requests.json must be valid JSON");
let request = dumped["requests"]
.as_array()
.and_then(|requests| {
requests.iter().find(|req| {
req["url"]
.as_str()
.is_some_and(|url| url.contains("dump-body-0"))
})
})
.expect("matching dumped request");
assert!(
!request["response_body"].is_null() || !request["body_error"].is_null(),
"dumped request should include response_body or body_error"
);
}
#[test]
fn network_requests_dump_with_filter() {
if skip() {
return;
}
let (sid, tid) = start_session(&url_network_xhr());
let _guard = SessionGuard::new(&sid);
wait_requests_done(&sid, &tid);
clear_requests(&sid, &tid);
issue_bulk_requests(&sid, &tid, 2, "match");
issue_bulk_requests(&sid, &tid, 1, "skip");
let temp = tempfile::tempdir().expect("create temp dir");
let dump_dir = temp.path().join("requests-dump-filter");
let dump_dir_str = dump_dir.to_string_lossy().to_string();
let out = headless_json(
&[
"browser",
"network",
"requests",
"--session",
&sid,
"--tab",
&tid,
"--filter",
"match",
"--dump",
"--out",
&dump_dir_str,
],
30,
);
assert_success(&out, "network requests dump with filter");
let requests_path = dump_dir.join("requests.json");
let dumped: serde_json::Value =
serde_json::from_slice(&fs::read(&requests_path).expect("read requests.json"))
.expect("requests.json must be valid JSON");
let requests = dumped["requests"].as_array().expect("requests array");
assert_eq!(requests.len(), 2);
assert!(
requests
.iter()
.all(|req| req["url"].as_str().is_some_and(|url| url.contains("match")))
);
}