use std::process::Command;
use std::time::Instant;
use serde_json::Value;
mod common;
use common::TestBrowser;
const CEILING_SECS: f64 = 3.0;
fn run_cli(args: &[&str]) -> (String, i32) {
let output = Command::new(common::binary())
.args(args)
.output()
.expect("Failed to run chrome-agent");
(
String::from_utf8_lossy(&output.stdout).to_string(),
output.status.code().unwrap_or(-1),
)
}
fn timed(args: &[&str]) -> (Value, f64) {
let started = Instant::now();
let (stdout, code) = run_cli(args);
let secs = started.elapsed().as_secs_f64();
assert_eq!(code, 0, "{args:?} failed: {stdout}");
(serde_json::from_str(&stdout).expect("JSON response"), secs)
}
struct PipeSession {
child: std::process::Child,
responses: std::io::Lines<std::io::BufReader<std::process::ChildStdout>>,
}
impl PipeSession {
fn start(browser: &str) -> Self {
use std::io::BufRead as _;
use std::process::Stdio;
let mut child = Command::new(common::binary())
.args(["--browser", browser, "pipe"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn pipe");
let stdout = child.stdout.take().expect("pipe stdout");
Self {
child,
responses: std::io::BufReader::new(stdout).lines(),
}
}
fn send(&mut self, cmd: &Value) -> Value {
use std::io::Write as _;
let stdin = self.child.stdin.as_mut().expect("pipe stdin");
writeln!(stdin, "{cmd}").expect("write command");
stdin.flush().expect("flush command");
let line = self
.responses
.next()
.expect("a response per command")
.expect("readable");
serde_json::from_str(&line).unwrap_or_else(|e| panic!("bad pipe line {line}: {e}"))
}
}
impl Drop for PipeSession {
fn drop(&mut self) {
drop(self.child.stdin.take());
let _ = self.child.wait();
}
}
fn uid_for(browser: &str, needles: &[&str]) -> String {
let (stdout, code) = run_cli(&["--browser", browser, "--json", "inspect"]);
assert_eq!(code, 0, "{stdout}");
let snapshot: Value = serde_json::from_str(&stdout).expect("JSON inspect");
let text = snapshot["snapshot"]
.as_str()
.unwrap_or_default()
.to_string();
text.lines()
.find(|line| needles.iter().all(|n| line.contains(n)))
.and_then(|line| line.trim_start().strip_prefix("uid="))
.and_then(|rest| rest.split_whitespace().next())
.unwrap_or_else(|| panic!("no uid matching {needles:?} in:\n{text}"))
.to_string()
}
fn open(browser: &str, fixture: &str) -> bool {
if !common::browser_ready() {
return false;
}
let url = common::fixture_url(fixture);
let (_, code) = run_cli(&["--browser", browser, "goto", &url]);
if code != 0 {
return common::unavailable(&format!("goto {fixture} failed"));
}
let (_, code) = run_cli(&["--browser", browser, "--json", "inspect"]);
if code != 0 {
return common::unavailable(&format!("inspect {fixture} failed"));
}
true
}
#[test]
fn a_click_that_only_navigates_a_subframe_does_not_wait_for_a_load() {
let b = TestBrowser::new("slow-subframe");
if !open(b.name(), "click_spawns_subframe.html") {
return;
}
let _ = run_cli(&["--browser", b.name(), "--json", "eval", "1"]);
let (response, secs) = timed(&[
"--browser",
b.name(),
"--verdict",
"off",
"--json",
"click",
"--selector",
"#go",
]);
assert!(
secs < CEILING_SECS,
"a click that spawns a tracking iframe waited {secs:.2}s for a load event that cannot \
fire: {response}"
);
assert!(
response["waited_ms"].is_null(),
"nothing was waited for, so nothing may be reported: {response}"
);
let (state, _) = timed(&[
"--browser",
b.name(),
"--json",
"eval",
"document.getElementById('state').textContent",
]);
assert_eq!(state["result"], "tracked", "the handler ran: {state}");
let (url, _) = timed(&["--browser", b.name(), "--json", "eval", "location.pathname"]);
assert!(
url["result"]
.as_str()
.unwrap_or_default()
.ends_with("click_spawns_subframe.html"),
"the top frame never navigated: {url}"
);
}
#[test]
fn a_click_that_navigates_the_page_still_waits_and_says_how_long() {
let b = TestBrowser::new("slow-navigates");
if !open(b.name(), "click_navigates_away.html") {
return;
}
let _ = run_cli(&["--browser", b.name(), "--json", "eval", "1"]);
let (response, secs) = timed(&[
"--browser",
b.name(),
"--verdict",
"off",
"--json",
"click",
"--selector",
"#leave",
]);
assert!(
secs < CEILING_SECS,
"a local navigation took {secs:.2}s: {response}"
);
assert!(
response["waited_ms"].is_u64(),
"the action waited for a load, and a wait a caller paid for is a wait it may read: \
{response}"
);
let (url, _) = timed(&["--browser", b.name(), "--json", "eval", "location.pathname"]);
assert!(
url["result"]
.as_str()
.unwrap_or_default()
.ends_with("click_spawns_subframe.html"),
"the click did navigate the top frame: {url}"
);
}
#[test]
fn a_wait_belongs_to_the_action_that_paid_it_and_to_no_other() {
let b = TestBrowser::new("slow-pipe");
if !common::browser_ready() {
return;
}
let navigates = common::fixture_url("click_navigates_away.html");
let mut session = PipeSession::start(b.name());
session.send(&serde_json::json!({"cmd": "goto", "url": navigates}));
session.send(&serde_json::json!({"cmd": "inspect"}));
let navigated = session.send(&serde_json::json!({"cmd": "click", "selector": "#leave"}));
assert!(
navigated["waited_ms"].is_u64(),
"the navigation was waited for: {navigated}"
);
session.send(&serde_json::json!({"cmd": "inspect"}));
let tracked = session.send(&serde_json::json!({"cmd": "click", "selector": "#go"}));
assert!(
tracked["waited_ms"].is_null(),
"the previous command's wait is not this command's: {tracked}"
);
}
#[test]
fn a_pointer_action_on_a_backgrounded_page_is_not_charged_five_seconds() {
let b = TestBrowser::new("slow-background");
if !open(b.name(), "click_overlay.html") {
return;
}
let second = common::fixture_url("click_navigates_away.html");
let (out, code) = run_cli(&["--browser", b.name(), "--page", "second", "goto", &second]);
assert_eq!(code, 0, "a second page could not be opened: {out}");
let (hidden, _) = timed(&[
"--browser",
b.name(),
"--json",
"eval",
"document.visibilityState",
]);
assert_eq!(
hidden["result"], "hidden",
"the page under test must be backgrounded: {hidden}"
);
let uid = uid_for(b.name(), &["button"]);
let (response, secs) = timed(&[
"--browser",
b.name(),
"--verdict",
"off",
"--json",
"hover",
&uid,
]);
assert!(
secs < CEILING_SECS,
"a hover on a backgrounded page took {secs:.2}s — the pointer path did not bring it \
forward: {response}"
);
let (_, keys) = timed(&[
"--browser",
b.name(),
"--verdict",
"off",
"--json",
"press",
"Escape",
]);
assert!(keys < CEILING_SECS, "press took {keys:.2}s");
}