use super::support::{MockRdpServer, load_fixture};
fn ff_rdp_bin() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_ff-rdp"))
}
fn base_args(port: u16) -> Vec<String> {
vec![
"--host".to_owned(),
"127.0.0.1".to_owned(),
"--port".to_owned(),
port.to_string(),
"--timeout".to_owned(),
"1000".to_owned(),
"--no-daemon".to_owned(),
]
}
fn network_server() -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on_with_followups(
"watchResources",
load_fixture("watch_resources_response.json"),
vec![
load_fixture("resources_available_network.json"),
load_fixture("resources_updated_network.json"),
],
)
.on(
"unwatchResources",
load_fixture("watch_resources_response.json"),
)
}
#[test]
fn network_shows_summary_by_default() {
let server = network_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("network".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert!(
json["results"].is_object(),
"default network output should be summary (object), got: {}",
json["results"]
);
assert_eq!(json["results"]["total_requests"], 2);
assert!(json["results"]["slowest"].is_array());
assert!(json["results"]["by_cause_type"].is_object());
}
#[test]
fn network_detail_shows_requests() {
let server = network_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["--detail".to_owned(), "network".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert_eq!(json["total"], 2);
let results = json["results"].as_array().expect("results is array");
assert_eq!(results[0]["method"], "GET");
assert_eq!(results[0]["url"], "https://example.com/");
assert_eq!(results[0]["status"], 200);
assert_eq!(results[0]["is_xhr"], false);
assert_eq!(results[1]["method"], "GET");
assert_eq!(results[1]["url"], "https://example.com/favicon.ico");
assert_eq!(results[1]["status"], 404);
}
#[test]
fn network_limit_shows_detail_with_truncation() {
let server = network_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["--limit".to_owned(), "1".to_owned(), "network".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 2);
assert_eq!(json["results"].as_array().unwrap().len(), 1);
assert_eq!(json["truncated"], true);
assert!(json["hint"].as_str().unwrap().contains("--all"));
}
#[test]
fn network_all_overrides_limit() {
let server = network_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["--all".to_owned(), "network".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 2);
assert_eq!(json["results"].as_array().unwrap().len(), 2);
assert!(json.get("truncated").is_none());
}
#[test]
fn network_filter_by_url() {
let server = network_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"--detail".to_owned(),
"network".to_owned(),
"--filter".to_owned(),
"favicon".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 1);
let results = json["results"].as_array().unwrap();
assert_eq!(results[0]["url"], "https://example.com/favicon.ico");
}
#[test]
fn network_filter_by_method() {
let server = network_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"--detail".to_owned(),
"network".to_owned(),
"--method".to_owned(),
"POST".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 0);
}
#[test]
fn network_with_jq_filter() {
let server = network_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"--jq".to_owned(),
".results[] | select(.status >= 400)".to_owned(),
"network".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(json["status"], 404);
assert_eq!(json["url"], "https://example.com/favicon.ico");
}
#[test]
fn network_empty_when_no_events() {
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on(
"watchResources",
load_fixture("watch_resources_response.json"),
)
.on(
"unwatchResources",
load_fixture("watch_resources_response.json"),
);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("network".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["results"]["total_requests"], 0);
}
#[test]
fn network_falls_back_to_performance_api_when_watcher_empty() {
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on(
"watchResources",
load_fixture("watch_resources_response.json"),
)
.on(
"unwatchResources",
load_fixture("watch_resources_response.json"),
)
.on_with_followup(
"evaluateJSAsync",
load_fixture("eval_immediate_response.json"),
load_fixture("eval_result_network_perf_fallback.json"),
);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["--detail".to_owned(), "network".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert_eq!(json["total"], 2, "expected 2 entries from perf fallback");
let results = json["results"].as_array().expect("results is array");
for entry in results {
assert_eq!(
entry["source"], "performance-api",
"expected performance-api source, got: {entry}"
);
}
assert_eq!(json["meta"]["source"], "performance-api");
}
#[test]
fn network_summary_falls_back_to_performance_api() {
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on(
"watchResources",
load_fixture("watch_resources_response.json"),
)
.on(
"unwatchResources",
load_fixture("watch_resources_response.json"),
)
.on_with_followup(
"evaluateJSAsync",
load_fixture("eval_immediate_response.json"),
load_fixture("eval_result_network_perf_fallback.json"),
);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("network".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert!(json["results"].is_object(), "expected summary object");
assert_eq!(json["results"]["total_requests"], 2);
assert_eq!(json["meta"]["source"], "performance-api");
}
#[test]
fn network_prints_hint_when_both_sources_empty() {
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on(
"watchResources",
load_fixture("watch_resources_response.json"),
)
.on(
"unwatchResources",
load_fixture("watch_resources_response.json"),
)
.on_with_followup(
"evaluateJSAsync",
load_fixture("eval_immediate_response.json"),
load_fixture("eval_result_empty_array.json"),
);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("network".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("hint:"),
"expected a hint on stderr when both sources empty, got: {stderr:?}"
);
assert!(
stderr.contains("--follow") || stderr.contains("Navigate"),
"hint should mention --follow or Navigate, got: {stderr:?}"
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert_eq!(json["results"]["total_requests"], 0);
}
#[test]
fn network_follow_streams_request_and_response_events() {
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on_with_followups(
"watchResources",
load_fixture("watch_resources_response.json"),
vec![
load_fixture("resources_available_network.json"),
load_fixture("resources_updated_network.json"),
],
)
.on(
"unwatchResources",
load_fixture("watch_resources_response.json"),
)
.close_after_followups();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["network".to_owned(), "--follow".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert!(
lines.len() >= 2,
"expected multiple NDJSON lines, got: {stdout}"
);
let first: serde_json::Value = serde_json::from_str(lines[0]).expect("valid JSON on line 0");
assert_eq!(first["event"], "request");
assert_eq!(first["method"], "GET");
assert!(
first["url"].as_str().is_some_and(|u| !u.is_empty()),
"url should be present"
);
let has_response = lines.iter().any(|line| {
serde_json::from_str::<serde_json::Value>(line).is_ok_and(|v| v["event"] == "response")
});
assert!(has_response, "expected at least one response event");
}
#[test]
fn network_follow_filter_suppresses_non_matching_requests() {
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on_with_followups(
"watchResources",
load_fixture("watch_resources_response.json"),
vec![
load_fixture("resources_available_network.json"),
load_fixture("resources_updated_network.json"),
],
)
.on(
"unwatchResources",
load_fixture("watch_resources_response.json"),
)
.close_after_followups();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"network".to_owned(),
"--follow".to_owned(),
"--filter".to_owned(),
"favicon".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.trim().lines() {
let v: serde_json::Value = serde_json::from_str(line).expect("valid JSON");
assert!(
v["url"].as_str().unwrap_or("").contains("favicon"),
"unexpected URL in filtered output: {v}"
);
}
}