use std::process::Command;
use serde_json::Value;
mod common;
fn binary() -> String {
let mut path = std::env::current_exe().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("chrome-agent");
path.to_string_lossy().into_owned()
}
fn run_cli(args: &[&str]) -> (String, i32) {
let output = Command::new(binary()).args(args).output().expect("Failed to run chrome-agent");
(
String::from_utf8_lossy(&output.stdout).to_string(),
output.status.code().unwrap_or(-1),
)
}
struct TestBrowser(String);
impl TestBrowser {
fn new(label: &str) -> Self {
Self(format!("{label}-{}", std::process::id()))
}
fn name(&self) -> &str {
&self.0
}
}
impl Drop for TestBrowser {
fn drop(&mut self) {
let _ = run_cli(&["--browser", &self.0, "close", "--purge"]);
}
}
fn open(browser: &str, fixture: &str) -> bool {
if !common::browser_ready() {
return false;
}
let (_, code) = run_cli(&["--browser", browser, "goto", &common::fixture_url(fixture)]);
if code != 0 {
return common::unavailable(&format!("goto {fixture} failed"));
}
true
}
fn json_cli(browser: &str, args: &[&str]) -> Value {
let mut full: Vec<&str> = vec!["--browser", browser, "--json"];
full.extend_from_slice(args);
let (stdout, code) = run_cli(&full);
assert_eq!(code, 0, "command should succeed: {stdout}");
serde_json::from_str(&stdout).unwrap_or_else(|e| panic!("not JSON ({e}): {stdout}"))
}
#[test]
fn a_submit_that_resets_the_form_names_the_value_it_destroyed() {
let b = TestBrowser::new("lost-reset");
if !open(b.name(), "form_value_reset_on_submit.html") {
return;
}
let filled = json_cli(b.name(), &["fill", "--selector", "#email", "hello@example.com"]);
assert_eq!(filled["value"]["verbatim"], true, "the fill has to land first: {filled}");
assert!(
filled["values_lost"].is_null(),
"a fill that landed destroyed nothing: {filled}"
);
let v = json_cli(b.name(), &["click", "--selector", "#submit"]);
let lost = v["values_lost"].as_array().unwrap_or_else(|| panic!("no values_lost: {v}"));
assert_eq!(lost.len(), 1, "{v}");
assert!(!lost[0]["uid"].as_str().unwrap_or_default().is_empty(), "{v}");
assert_eq!(lost[0]["role"], "textbox", "{v}");
assert_eq!(lost[0]["name"], "Email", "{v}");
assert_eq!(lost[0]["was"], "hello@example.com", "{v}");
assert_eq!(v["verdict"], "changed", "the page did move: {v}");
assert_eq!(v["verdict_reason"], "values_lost", "{v}");
assert!(
v["verdict_hint"].as_str().unwrap_or_default().contains("cleared itself"),
"the hint states the ambiguity rather than declaring failure: {v}"
);
let (truth, _) = run_cli(&["--browser", b.name(), "eval", "document.getElementById('email').value"]);
assert_eq!(truth.trim(), "\"\"", "the field is empty: {truth}");
}
#[test]
fn a_submit_that_keeps_the_value_reports_no_loss() {
let b = TestBrowser::new("lost-keep");
if !open(b.name(), "form_value_secret_lost_on_submit.html") {
return;
}
json_cli(b.name(), &["fill", "--selector", "#search", "kafka"]);
let v = json_cli(b.name(), &["click", "--selector", "#keep-submit"]);
assert!(v["values_lost"].is_null(), "nothing was lost: {v}");
assert_eq!(v["verdict"], "changed", "{v}");
assert_eq!(v["verdict_reason"], "tree_delta", "and the reason stays the plain one: {v}");
}
#[test]
fn a_lost_secret_is_named_but_never_printed() {
let b = TestBrowser::new("lost-secret");
if !open(b.name(), "form_value_secret_lost_on_submit.html") {
return;
}
json_cli(b.name(), &["fill", "--selector", "#card", "4111111111111111"]);
json_cli(b.name(), &["fill", "--selector", "#pw", "topsecret123"]);
json_cli(b.name(), &["fill", "--selector", "#note", "gift wrap"]);
let v = json_cli(b.name(), &["click", "--selector", "#pay-submit"]);
let lost = v["values_lost"].as_array().unwrap_or_else(|| panic!("no values_lost: {v}"));
let by_name = |name: &str| {
lost.iter()
.find(|e| e["name"] == name)
.unwrap_or_else(|| panic!("no lost value named {name}: {v}"))
.clone()
};
for secret in ["Card number", "Password"] {
let entry = by_name(secret);
assert_eq!(entry["redacted"], true, "{secret} must be redacted: {entry}");
assert!(entry["was"].is_null(), "{secret} must not carry its value: {entry}");
assert!(entry["was_length"].is_null(), "{secret} must not carry a length: {entry}");
}
assert_eq!(by_name("Note")["was"], "gift wrap", "{v}");
assert!(
!v["values_lost"].to_string().contains("4111111111111111"),
"the card number must not appear in values_lost: {v}"
);
assert!(
!v["values_lost"].to_string().contains("topsecret123"),
"the password must not appear in values_lost: {v}"
);
}
#[test]
fn pipe_reports_the_lost_value_the_way_the_cli_does() {
if !common::browser_ready() {
return;
}
let url = common::fixture_url("form_value_reset_on_submit.html");
let script = format!(
"{}\n{}\n{}\n",
serde_json::json!({"cmd": "goto", "url": url}),
serde_json::json!({"cmd": "fill", "selector": "#email", "value": "hello@example.com"}),
serde_json::json!({"cmd": "click", "selector": "#submit"}),
);
let browser = format!("lost-pipe-{}", std::process::id());
let mut child = Command::new(binary())
.args(["--browser", &browser, "pipe"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn pipe");
std::io::Write::write_all(child.stdin.as_mut().unwrap(), script.as_bytes()).unwrap();
drop(child.stdin.take());
let out = child.wait_with_output().expect("pipe output");
let stdout = String::from_utf8_lossy(&out.stdout);
let _ = run_cli(&["--browser", &browser, "close", "--purge"]);
let last: Value = stdout
.lines()
.rfind(|l| !l.trim().is_empty())
.map(|l| serde_json::from_str(l).unwrap_or_else(|e| panic!("not JSON ({e}): {l}")))
.expect("a click response");
assert_eq!(last["verdict_reason"], "values_lost", "{last}");
assert_eq!(last["values_lost"][0]["was"], "hello@example.com", "{last}");
}