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(),
"--no-daemon".to_owned(),
]
}
fn a11y_summary_server() -> 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("eval_result_a11y_summary.json"),
)
}
fn a11y_server() -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on("getWalker", load_fixture("a11y_get_walker_response.json"))
.on("getDocument", load_fixture("a11y_get_root_response.json"))
.on("getRootNode", load_fixture("a11y_get_root_response.json"))
.on("children", load_fixture("a11y_children_response.json"))
}
fn a11y_contrast_server() -> 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("eval_result_contrast.json"),
)
}
#[test]
fn a11y_outputs_json_with_accessibility_tree() {
let server = a11y_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("a11y".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["results"]["role"], "document",
"root role should be document"
);
assert_eq!(json["total"], 1);
let children = json["results"]["children"]
.as_array()
.expect("results should have a children array");
assert!(!children.is_empty(), "tree should have at least one child");
assert!(
json["results"].get("actor").is_none(),
"actor IDs should be stripped from output"
);
}
#[test]
fn a11y_interactive_filters_to_interactive_elements() {
let server = a11y_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["a11y".to_owned(), "--interactive".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 results = &json["results"];
assert_eq!(json["total"], 1);
let children = results["children"]
.as_array()
.expect("filtered results should still have children");
let link_present = children.iter().any(|c| c["role"] == "link");
assert!(link_present, "interactive filter should retain link role");
let heading_present = children.iter().any(|c| c["role"] == "heading");
assert!(
!heading_present,
"interactive filter should remove heading role"
);
}
#[test]
fn a11y_with_jq_extracts_role() {
let server = a11y_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.role".to_owned(),
"a11y".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);
assert_eq!(stdout.trim(), "\"document\"");
}
#[test]
fn a11y_contrast_outputs_json_with_checks() {
let server = a11y_contrast_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["a11y".to_owned(), "contrast".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 results = json["results"]
.as_array()
.expect("contrast results should be an array");
assert!(
!results.is_empty(),
"should have at least one contrast check"
);
let first = &results[0];
assert!(
first.get("ratio").is_some(),
"check must have a ratio field"
);
assert!(
first.get("foreground").is_some(),
"check must have foreground"
);
assert!(
first.get("background").is_some(),
"check must have background"
);
assert!(
first.get("aa_normal").is_some(),
"check must have aa_normal"
);
assert!(
json["meta"]["summary"].is_object(),
"meta should contain summary"
);
assert!(
json["meta"]["summary"]["total"].is_number(),
"summary should have total"
);
}
#[test]
fn a11y_contrast_fail_only_filters_passing_checks() {
let server = a11y_contrast_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"a11y".to_owned(),
"contrast".to_owned(),
"--fail-only".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 results = json["results"]
.as_array()
.expect("contrast results should be an array");
assert!(
results.is_empty(),
"all fixture checks pass AA — fail-only should return empty array"
);
assert_eq!(
json["total"], 2,
"total reflects all checked elements even when filtered"
);
}
#[test]
fn a11y_contrast_with_jq_extracts_total() {
let server = a11y_contrast_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(),
".meta.summary.total".to_owned(),
"a11y".to_owned(),
"contrast".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);
assert_eq!(stdout.trim(), "2", "fixture has 2 contrast checks");
}
#[test]
fn a11y_summary_outputs_json_with_sections() {
let server = a11y_summary_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["a11y".to_owned(), "summary".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"]["landmarks"].is_array(),
"results must have landmarks array"
);
assert!(
json["results"]["headings"].is_array(),
"results must have headings array"
);
assert!(
json["results"]["interactive"].is_array(),
"results must have interactive array"
);
assert_eq!(
json["results"]["landmarks"].as_array().unwrap().len(),
2,
"fixture has 2 landmarks"
);
assert_eq!(
json["results"]["headings"].as_array().unwrap().len(),
2,
"fixture has 2 headings"
);
assert_eq!(
json["results"]["interactive"].as_array().unwrap().len(),
3,
"fixture has 3 interactive elements"
);
}
#[test]
fn a11y_summary_text_format_renders_sections() {
let server = a11y_summary_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"--format".to_owned(),
"text".to_owned(),
"a11y".to_owned(),
"summary".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);
assert!(
stdout.contains("LANDMARKS"),
"should contain LANDMARKS header"
);
assert!(
stdout.contains("HEADINGS"),
"should contain HEADINGS header"
);
assert!(
stdout.contains("INTERACTIVE"),
"should contain INTERACTIVE header"
);
assert!(
stdout.contains("h1 Example Domain"),
"should render h1 heading"
);
assert!(
stdout.contains("link") && stdout.contains("More information"),
"should render the link element"
);
assert!(
!stdout.trim().starts_with('"'),
"text output must not be JSON-quoted"
);
}
#[test]
fn a11y_summary_with_jq_extracts_headings() {
let server = a11y_summary_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.headings | length".to_owned(),
"a11y".to_owned(),
"summary".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);
assert_eq!(stdout.trim(), "2", "fixture has 2 headings");
}