use std::process::Command;
use std::time::Instant;
use serde_json::Value;
mod common;
use common::TestBrowser;
const CEILING_SECS: f64 = 3.0;
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),
)
}
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(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", "verdict": "off"
}));
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");
}