fix(open): graceful load-timeout + --wait-until override for SPAs (issue #10)
`open` waits for the `load` event by default. SPAs whose `load` never fires (a long-pending XHR or a stuck sub-resource holds it open) made `open` hard-fail after the lifecycle timeout — even though the DOM was ready and eval/screenshot worked immediately right after. - Graceful degradation: if the lifecycle event times out but document.readyState is interactive/complete, navigate returns success carrying a `warning` in the response (the CLI prints it to stderr; --json keeps the field) instead of erroring. Only a still-loading document is a real failure. - `open/goto/navigate` now accept `--wait-until <load|domcontentloaded| networkidle|none>` so SPAs can return as soon as the DOM is parsed. The URL parser skips the --wait-until value so it isn't mistaken for the URL. - WaitUntil::as_str() for the warning label; output.rs surfaces response warnings. Verified live: --wait-until domcontentloaded returns immediately on a page whose load never fires; default load on the same page now succeeds at the timeout with a clear stderr warning instead of failing. Adds parse tests for both arg orders + bogus value.
This commit is contained in:
+76
-1
@@ -320,7 +320,27 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
||||
// scripts before the first real navigation (see `batch`).
|
||||
// `goto` and `navigate` still require a URL since those verbs
|
||||
// imply the navigation itself.
|
||||
let first_url = rest.iter().find(|a| !a.starts_with("--"));
|
||||
// The URL is the first positional arg, skipping flags AND any value
|
||||
// consumed by `--wait-until` (so it isn't mistaken for the URL).
|
||||
let first_url = {
|
||||
let mut url = None;
|
||||
let mut skip_next = false;
|
||||
for a in &rest {
|
||||
if skip_next {
|
||||
skip_next = false;
|
||||
continue;
|
||||
}
|
||||
if *a == "--wait-until" {
|
||||
skip_next = true;
|
||||
continue;
|
||||
}
|
||||
if !a.starts_with("--") {
|
||||
url = Some(a);
|
||||
break;
|
||||
}
|
||||
}
|
||||
url
|
||||
};
|
||||
let url = match first_url {
|
||||
Some(u) => *u,
|
||||
None if cmd == "open" => {
|
||||
@@ -350,6 +370,23 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
||||
if flags.provider.is_some() {
|
||||
nav_cmd["waitUntil"] = json!("none");
|
||||
}
|
||||
// Explicit readiness override (issue #10): SPAs whose `load` event
|
||||
// never fires (a long-lived XHR/websocket holds it open) hang out the
|
||||
// load-event wait. `--wait-until domcontentloaded` returns as soon as
|
||||
// the DOM is parsed.
|
||||
if let Some(i) = rest.iter().position(|a| *a == "--wait-until") {
|
||||
let val = rest.get(i + 1).ok_or(ParseError::MissingArguments {
|
||||
context: "open --wait-until".to_string(),
|
||||
usage: "open <url> --wait-until <load|domcontentloaded|networkidle|none>",
|
||||
})?;
|
||||
if !["load", "domcontentloaded", "networkidle", "none"].contains(val) {
|
||||
return Err(ParseError::InvalidValue {
|
||||
message: format!("Unknown --wait-until value: {}", val),
|
||||
usage: "open <url> --wait-until <load|domcontentloaded|networkidle|none>",
|
||||
});
|
||||
}
|
||||
nav_cmd["waitUntil"] = json!(val);
|
||||
}
|
||||
if let Some(ref headers_json) = flags.headers {
|
||||
let headers =
|
||||
serde_json::from_str::<serde_json::Value>(headers_json).map_err(|_| {
|
||||
@@ -3643,6 +3680,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_wait_until_after_url() {
|
||||
let cmd = parse_command(
|
||||
&args("open https://x.com --wait-until domcontentloaded"),
|
||||
&default_flags(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cmd["action"], "navigate");
|
||||
assert_eq!(cmd["url"], "https://x.com");
|
||||
assert_eq!(cmd["waitUntil"], "domcontentloaded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_wait_until_before_url_not_mistaken_for_url() {
|
||||
// The --wait-until value must not be picked up as the URL.
|
||||
let cmd = parse_command(
|
||||
&args("open --wait-until domcontentloaded https://x.com"),
|
||||
&default_flags(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cmd["url"], "https://x.com");
|
||||
assert_eq!(cmd["waitUntil"], "domcontentloaded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_wait_until_rejects_bogus_value() {
|
||||
let err = parse_command(
|
||||
&args("open https://x.com --wait-until wat"),
|
||||
&default_flags(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
err.format().contains("--wait-until"),
|
||||
"got: {}",
|
||||
err.format()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_bare_value_suggests_text_locator() {
|
||||
// `find "I'm not a robot" click` — value where a locator keyword was
|
||||
|
||||
Reference in New Issue
Block a user