use super::support::{MockRdpServer, load_fixture};
const LONGSTRING_PERF_JSON: &str = r#"{"entries":[{"name":"https://example.com/app.js","initiatorType":"script","duration":42.5,"transferSize":12345,"encodedBodySize":12000,"decodedBodySize":36000,"startTime":100.0,"responseEnd":142.5,"nextHopProtocol":"h2"}],"hostname":"example.com"}"#;
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 perf_server(fixture: &str) -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on_with_followup(
"evaluateJSAsync",
load_fixture("eval_immediate_response.json"),
load_fixture(fixture),
)
}
#[test]
fn perf_shows_resources() {
let server = perf_server("eval_result_perf_resource.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("perf".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]["url"], "https://example.com/app.js");
assert_eq!(results[0]["initiator_type"], "script");
assert_eq!(results[0]["duration_ms"], 42.5);
assert_eq!(results[0]["transfer_size"], 12345);
assert_eq!(results[0]["decoded_size"], 36000);
assert_eq!(results[0]["protocol"], "h2");
assert_eq!(results[0]["from_cache"], false);
assert_eq!(results[0]["resource_type"], "js");
assert_eq!(results[0]["third_party"], false);
assert_eq!(results[1]["url"], "https://example.com/favicon.ico");
assert_eq!(results[1]["initiator_type"], "img");
assert_eq!(results[1]["resource_type"], "image");
assert_eq!(results[1]["third_party"], false);
}
#[test]
fn perf_resource_explicit_type_flag() {
let server = perf_server("eval_result_perf_resource.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"perf".to_owned(),
"--type".to_owned(),
"resource".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);
}
#[test]
fn perf_filter_by_url() {
let server = perf_server("eval_result_perf_resource.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"perf".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);
assert_eq!(json["results"][0]["url"], "https://example.com/favicon.ico");
}
#[test]
fn perf_with_jq_filter() {
let server = perf_server("eval_result_perf_resource.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"perf".to_owned(),
"--jq".to_owned(),
".results[] | select(.initiator_type == \"script\") | .url".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);
assert_eq!(stdout.trim(), r#""https://example.com/app.js""#);
}
#[test]
fn perf_jq_array_iteration_works() {
let server = perf_server("eval_result_perf_resource.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"perf".to_owned(),
"--jq".to_owned(),
".results[].url".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(),
".results[].url should work: stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], r#""https://example.com/app.js""#);
assert_eq!(lines[1], r#""https://example.com/favicon.ico""#);
}
#[test]
fn perf_navigation_type() {
let server = perf_server("eval_result_perf_navigation.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"perf".to_owned(),
"--type".to_owned(),
"navigation".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 json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 1);
let nav = &json["results"][0];
assert_eq!(nav["url"], "https://example.com/");
assert_eq!(nav["dns_ms"], 20.0);
assert_eq!(nav["ttfb_ms"], 340.0);
assert_eq!(nav["tls_ms"], 25.0);
}
#[test]
fn perf_paint_type() {
let server = perf_server("eval_result_perf_paint.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["perf".to_owned(), "--type".to_owned(), "paint".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"][0]["name"], "first-paint");
assert_eq!(json["results"][1]["name"], "first-contentful-paint");
}
#[test]
fn perf_lcp_alias() {
let server = perf_server("eval_result_perf_lcp.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["perf".to_owned(), "--type".to_owned(), "lcp".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"][1]["url"], "https://example.com/banner.jpg");
assert_eq!(json["results"][1]["start_time_ms"], 1850.0);
assert_eq!(json["results"][1]["size"], 120_000);
}
#[test]
fn perf_cls_alias() {
let server = perf_server("eval_result_perf_cls.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["perf".to_owned(), "--type".to_owned(), "cls".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"], 3);
assert_eq!(json["results"][2]["had_recent_input"], true);
}
#[test]
fn perf_longtask_type() {
let server = perf_server("eval_result_perf_longtask.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"perf".to_owned(),
"--type".to_owned(),
"longtask".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"][0]["duration_ms"], 120.0);
assert_eq!(json["results"][1]["duration_ms"], 80.0);
}
#[test]
fn perf_vitals_computes_cwv() {
let server = perf_server("eval_result_perf_vitals.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["perf".to_owned(), "vitals".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");
let r = &json["results"];
assert_eq!(r["lcp_ms"], 1850.0);
assert_eq!(r["lcp_rating"], "good");
assert_eq!(r["fcp_ms"], 980.0);
assert_eq!(r["fcp_rating"], "good");
assert_eq!(r["ttfb_ms"], 340.0);
assert_eq!(r["ttfb_rating"], "good");
assert_eq!(r["cls"], 0.05);
assert_eq!(r["cls_rating"], "good");
assert_eq!(r["tbt_ms"], 120.0);
assert_eq!(r["tbt_rating"], "good");
}
#[test]
fn perf_audit_returns_structured_report() {
let server = perf_server("eval_result_perf_audit.json");
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["perf".to_owned(), "audit".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");
let r = &json["results"];
assert_eq!(r["vitals"]["lcp_ms"], 1850.0);
assert_eq!(r["vitals"]["fcp_ms"], 980.0);
assert_eq!(r["vitals"]["ttfb_ms"], 340.0);
assert_eq!(r["resource_summary"]["count"], 2);
assert!(!r["resource_by_type"].as_array().unwrap().is_empty());
assert_eq!(r["third_party_summary"]["count"], 1);
assert_eq!(r["dom_stats"]["node_count"], 250);
assert_eq!(r["dom_stats"]["render_blocking_count"], 5);
assert_eq!(r["dom_stats"]["images_without_lazy"], 2);
assert!(!r["slowest_resources"].as_array().unwrap().is_empty());
assert!(r["navigation"].is_object());
}
#[test]
fn perf_exception_exits_nonzero() {
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on_with_followup(
"evaluateJSAsync",
load_fixture("eval_immediate_response.json"),
load_fixture("eval_result_cached_exception.json"),
);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("perf".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 non-zero exit for exception"
);
assert_eq!(output.status.code(), Some(1));
}
#[test]
fn perf_handles_long_string() {
let longstring_result = load_fixture("eval_result_cached_longstring.json");
let substring_response = serde_json::json!({
"from": "server1.conn0.longstr1",
"substring": LONGSTRING_PERF_JSON
});
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on_with_followup(
"evaluateJSAsync",
load_fixture("eval_immediate_response.json"),
longstring_result,
)
.on("substring", substring_response);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("perf".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"], 1);
assert_eq!(json["results"][0]["url"], "https://example.com/app.js");
assert_eq!(json["results"][0]["initiator_type"], "script");
assert_eq!(json["results"][0]["duration_ms"], 42.5);
}