diff --git a/wpt/runner/src/main.rs b/wpt/runner/src/main.rs index 80be76ed0..3fd4888a7 100644 --- a/wpt/runner/src/main.rs +++ b/wpt/runner/src/main.rs @@ -40,6 +40,7 @@ mod test_runners; mod net_provider; mod panic_backtrace; mod report; +mod script_detection; use net_provider::WptNetProvider; diff --git a/wpt/runner/src/script_detection.rs b/wpt/runner/src/script_detection.rs new file mode 100644 index 000000000..f6ba77432 --- /dev/null +++ b/wpt/runner/src/script_detection.rs @@ -0,0 +1,210 @@ +//! Detection of tests that depend on JavaScript to render correctly. +//! +//! Blitz does not execute JavaScript, so ref tests whose rendering depends on script +//! (e.g. tests that mutate the DOM from a `"#).unwrap()); +static SRC_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"(?i)src\s*=\s*["']?([^"'\s>]+)"#).unwrap()); +static EVENT_HANDLER_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"(?i)\son[a-z]+\s*=\s*("[^"]*"|'[^']*')"#).unwrap()); +static COMMENT_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"(?s)//[^\n]*|/\*.*?\*/"#).unwrap()); +static STRING_RE: LazyLock = + LazyLock::new(|| Regex::new(r#""([^"\\]*)"|'([^'\\]*)'"#).unwrap()); +static IDENT_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"[A-Za-z_$][A-Za-z0-9_$]*"#).unwrap()); + +/// External scripts that only provide wait/screenshot helpers and have no +/// rendering side-effects of their own. +const TRIVIAL_SCRIPT_SRCS: &[&str] = &["/reftest-wait.js", "/rendering-utils.js"]; + +/// Identifiers allowed in a trivial "wait then screenshot" inline script. +const TRIVIAL_IDENTS: &[&str] = &[ + // Keywords / declarations + "async", + "await", + "const", + "function", + "let", + "new", + "return", + "var", + // Waiting vocabulary + "Promise", + "addEventListener", + "document", + "fonts", + "load", + "onload", + "ready", + "requestAnimationFrame", + "resolve", + "then", + "finally", + "window", + // reftest-wait helpers (defined by /common/reftest-wait.js and + // /common/rendering-utils.js) + "takeScreenshot", + "waitForAtLeastOneFrame", + // Removing the `reftest-wait` class + "body", + "classList", + "documentElement", + "remove", +]; + +/// String literals allowed in a trivial inline script. +const TRIVIAL_STRINGS: &[&str] = &["", "reftest-wait", "load", "DOMContentLoaded"]; + +fn src_is_trivial(src: &str) -> bool { + TRIVIAL_SCRIPT_SRCS + .iter() + .any(|suffix| src.ends_with(suffix)) +} + +fn inline_script_is_trivial(body: &str) -> bool { + let body = COMMENT_RE.replace_all(body, ""); + for caps in STRING_RE.captures_iter(&body) { + let string = caps + .get(1) + .or_else(|| caps.get(2)) + .map_or("", |m| m.as_str()); + if !TRIVIAL_STRINGS.contains(&string) { + return false; + } + } + let without_strings = STRING_RE.replace_all(&body, "\"\""); + IDENT_RE + .find_iter(&without_strings) + .all(|ident| TRIVIAL_IDENTS.contains(&ident.as_str())) +} + +/// Returns true if the document depends on JavaScript to render correctly +/// (i.e. contains any script that is not a trivial "wait then screenshot" script). +pub fn uses_nontrivial_script(html: &str) -> bool { + for caps in SCRIPT_RE.captures_iter(html) { + let attrs = caps.get(1).unwrap().as_str(); + let body = caps.get(2).unwrap().as_str(); + if let Some(src) = SRC_RE.captures(attrs).and_then(|c| c.get(1)) { + if !src_is_trivial(src.as_str()) { + return true; + } + } else if !body.trim().is_empty() && !inline_script_is_trivial(body) { + return true; + } + } + + // Content outside of well-formed elements: an unclosed + // + "#; + assert!(uses_nontrivial_script(html)); + } + + #[test] + fn font_wait_screenshot() { + let html = r#" + + + + "#; + assert!(!uses_nontrivial_script(html)); + } + + #[test] + fn remove_reftest_wait_on_font_ready() { + let html = r#" + + "#; + assert!(!uses_nontrivial_script(html)); + } + + #[test] + fn onload_raf_screenshot() { + let html = r#" + + + "#; + assert!(!uses_nontrivial_script(html)); + } + + #[test] + fn external_helper_script() { + let html = r#""#; + assert!(uses_nontrivial_script(html)); + } + + #[test] + fn nontrivial_event_handler_attribute() { + let html = r#""#; + assert!(uses_nontrivial_script(html)); + } + + #[test] + fn nontrivial_string_literal() { + let html = r#""#; + assert!(uses_nontrivial_script(html)); + } + + #[test] + fn unclosed_script_tag() { + assert!(uses_nontrivial_script("