use assert_cmd::Command;
use predicates::prelude::*;
use std::io::Write;
fn cmd() -> Command {
Command::cargo_bin("rakers").unwrap()
}
#[test]
fn help_exits_cleanly() {
cmd().arg("--help").assert().success();
}
#[test]
fn stdin_document_write() {
cmd()
.write_stdin(r#"<script>document.write("<p>hello</p>")</script>"#)
.assert()
.success()
.stdout(predicate::str::contains("<p>hello</p>"));
}
#[test]
fn static_html_passthrough() {
cmd()
.write_stdin("<html><body><h1>Static</h1></body></html>")
.assert()
.success()
.stdout(predicate::str::contains("<h1>Static</h1>"));
}
#[test]
fn console_log_goes_to_stderr() {
cmd()
.arg("--verbose")
.write_stdin(r#"<script>console.log("test message")</script>"#)
.assert()
.success()
.stderr(predicate::str::contains("[console] test message"));
}
#[test]
fn script_error_is_non_fatal() {
cmd()
.write_stdin(concat!(
r#"<script>throw new Error("oops")</script>"#,
r#"<script>document.write("<p>survived</p>")</script>"#,
))
.assert()
.success()
.stdout(predicate::str::contains("<p>survived</p>"));
}
#[test]
fn html_file_arg() {
let mut f = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
write!(
f,
r#"<html><body><script>document.write("<p>from file</p>")</script></body></html>"#
)
.unwrap();
cmd()
.arg(f.path())
.assert()
.success()
.stdout(predicate::str::contains("<p>from file</p>"));
}
#[test]
fn js_file_arg_wraps_in_html() {
let mut f = tempfile::Builder::new().suffix(".js").tempfile().unwrap();
write!(f, r#"document.write("<p>from js</p>")"#).unwrap();
cmd()
.arg(f.path())
.assert()
.success()
.stdout(predicate::str::contains("<p>from js</p>"));
}
#[test]
fn output_flag_writes_file_not_stdout() {
let out = tempfile::NamedTempFile::new().unwrap();
cmd()
.write_stdin(r#"<script>document.write("<p>written</p>")</script>"#)
.args(["-o", out.path().to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::is_empty());
let content = std::fs::read_to_string(out.path()).unwrap();
assert!(
content.contains("<p>written</p>"),
"output file missing rendered content"
);
}
#[test]
#[cfg_attr(feature = "boa", ignore = "boa overflows on large React bundles")]
fn todomvc_react_renders_ui() {
let output = cmd()
.arg("https://todomvc.com/examples/react/dist/")
.output()
.unwrap();
assert!(
output.status.success(),
"rakers exited with non-zero status"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("<h1>todos</h1>"),
"'<h1>todos</h1>' absent — React may not have rendered the TodoMVC UI"
);
assert!(
stdout.contains("class=\"new-todo\""),
"new-todo input absent — React may not have rendered the TodoMVC UI"
);
}
#[test]
#[ignore = "live network test — flaky in CI"]
#[cfg_attr(feature = "boa", ignore = "boa overflows on large React bundles")]
fn jsbench_url_renders_react_ui() {
let output = cmd().arg("https://jsbench.me").output().unwrap();
assert!(
output.status.success(),
"rakers exited with non-zero status"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.len() > 4_000,
"output too small ({} bytes) — React may not have run",
stdout.len()
);
assert!(
stdout.to_lowercase().contains("run"),
"'run' absent — React UI may not have rendered"
);
}
#[test]
fn pretty_flag_indents_output() {
cmd()
.arg("--pretty")
.write_stdin("<html><body><div><p>hello</p></div></body></html>")
.assert()
.success()
.stdout(predicate::str::contains("\n <body>"))
.stdout(predicate::str::contains("\n <div>"))
.stdout(predicate::str::contains("\n <p>"))
.stdout(predicate::str::contains("hello"));
}
#[test]
fn pretty_flag_script_content_verbatim() {
cmd()
.arg("--pretty")
.write_stdin("<html><body><script>var x = 1 < 2;</script></body></html>")
.assert()
.success()
.stdout(predicate::str::contains("var x = 1 < 2;"));
}
#[test]
fn json_flag_emits_json_object() {
let out = cmd()
.arg("--json")
.write_stdin(r#"<script>document.write("<p>hi</p>")</script>"#)
.assert()
.success()
.get_output()
.stdout
.clone();
let s = String::from_utf8(out).unwrap();
assert!(s.contains("\"raw_bytes\""), "raw_bytes field absent");
assert!(
s.contains("\"rendered_bytes\""),
"rendered_bytes field absent"
);
assert!(s.contains("\"html\""), "html field absent");
assert!(s.contains("<p>hi</p>"), "rendered content absent");
}
#[test]
fn json_and_pretty_combined() {
let out = cmd()
.args(["--json", "--pretty"])
.write_stdin("<html><body><div><p>test</p></div></body></html>")
.assert()
.success()
.get_output()
.stdout
.clone();
let s = String::from_utf8(out).unwrap();
assert!(
s.contains("\\n"),
"pretty newlines should be JSON-escaped in html field"
);
assert!(
s.contains("\"rendered_bytes\""),
"rendered_bytes field absent"
);
}
#[test]
fn diff_flag_shows_unified_diff() {
let out = cmd()
.arg("--diff")
.write_stdin(r#"<html><body><script>document.body.innerHTML="<h1>rendered</h1>"</script></body></html>"#)
.assert()
.success()
.get_output()
.stdout
.clone();
let s = String::from_utf8(out).unwrap();
assert!(s.contains("---"), "missing --- header");
assert!(s.contains("+++"), "missing +++ header");
assert!(s.contains("rendered"), "rendered content absent from diff");
}
#[test]
fn max_scripts_skips_remote_fetches() {
let out = cmd()
.args(["--max-scripts", "0", "--verbose"])
.write_stdin(concat!(
r#"<html><head><script src="https://example.com/app.js"></script></head>"#,
r#"<body><script>document.write("<p>inline</p>")</script></body></html>"#,
))
.assert()
.success()
.get_output()
.clone();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stdout.contains("<p>inline</p>"),
"inline script should still run"
);
assert!(
!stderr.contains("[fetch]"),
"remote script should not be fetched"
);
assert!(
stderr.contains("[skip]"),
"skip message should appear in stderr"
);
}
#[test]
#[cfg_attr(not(feature = "rquickjs"), ignore = "boa has no interrupt handler")]
fn timeout_kills_infinite_loop() {
cmd()
.args(["--timeout", "1"])
.write_stdin(concat!(
r#"<html><body>"#,
r#"<script>while(true){}</script>"#,
r#"<script>document.write("<p>after</p>")</script>"#,
r#"</body></html>"#,
))
.timeout(std::time::Duration::from_secs(10))
.assert()
.success()
.stdout(predicate::str::contains("<p>after</p>"));
}
#[test]
#[cfg_attr(not(feature = "rquickjs"), ignore = "boa has no interrupt handler")]
fn timeout_subsecond_kills_loop() {
cmd()
.args(["--timeout", "0.5"])
.write_stdin(concat!(
r#"<html><body>"#,
r#"<script>while(true){}</script>"#,
r#"<script>document.write("<p>sub</p>")</script>"#,
r#"</body></html>"#,
))
.timeout(std::time::Duration::from_secs(10))
.assert()
.success()
.stdout(predicate::str::contains("<p>sub</p>"));
}
#[test]
fn timeout_zero_is_rejected() {
cmd()
.args(["--timeout", "0"])
.write_stdin("<html></html>")
.assert()
.failure()
.stderr(predicate::str::contains("greater than zero"));
}
#[test]
fn no_timeout_flag_accepted() {
cmd()
.arg("--no-timeout")
.write_stdin(r#"<script>document.write("<p>ok</p>")</script>"#)
.assert()
.success()
.stdout(predicate::str::contains("<p>ok</p>"));
}
#[test]
fn timeout_and_no_timeout_conflict() {
cmd()
.args(["--timeout", "5", "--no-timeout"])
.write_stdin("<html></html>")
.assert()
.failure();
}
#[test]
fn verbose_off_suppresses_console() {
cmd()
.write_stdin(r#"<script>console.log("should be hidden")</script>"#)
.assert()
.success()
.stderr(predicate::str::contains("[console]").not());
}
#[test]
fn verbose_on_shows_console() {
cmd()
.arg("--verbose")
.write_stdin(r#"<script>console.log("should appear")</script>"#)
.assert()
.success()
.stderr(predicate::str::contains("[console] should appear"));
}
#[test]
fn selector_filters_rendered_output() {
cmd()
.args(["--selector", "h1"])
.write_stdin("<html><body><h1>Title</h1><p>Other</p></body></html>")
.assert()
.success()
.stdout(predicate::str::contains("<h1>Title</h1>"))
.stdout(predicate::str::contains("<p>Other</p>").not());
}
#[test]
fn selector_with_js_rendered_content() {
cmd()
.args(["--selector", "#app"])
.write_stdin(concat!(
r#"<html><body><div id="app"></div>"#,
r#"<script>document.getElementById('app').innerHTML='<p>rendered</p>';</script>"#,
r#"</body></html>"#,
))
.assert()
.success()
.stdout(predicate::str::contains("<p>rendered</p>"))
.stdout(predicate::str::contains("<script>").not());
}
#[test]
fn selector_empty_when_no_match() {
cmd()
.args(["--selector", "h2"])
.write_stdin("<html><body><h1>Title</h1></body></html>")
.assert()
.success()
.stdout(predicate::str::is_empty());
}
#[test]
fn invalid_selector_fails() {
cmd()
.args(["--selector", "##bad"])
.write_stdin("<html></html>")
.assert()
.failure()
.stderr(predicate::str::contains("invalid selector"));
}
#[test]
fn invalid_header_format_fails() {
cmd()
.args(["-H", "no-colon-here"])
.write_stdin("<html></html>")
.assert()
.failure()
.stderr(predicate::str::contains("invalid header"));
}