diff --git a/crates/browser/tests/wpt.rs b/crates/browser/tests/wpt.rs new file mode 100644 index 0000000..0386d85 --- /dev/null +++ b/crates/browser/tests/wpt.rs @@ -0,0 +1,837 @@ +//! WPT (Web Platform Tests) test harness. +//! +//! Discovers `.html` test files under `tests/wpt/`, parses them, executes +//! inline scripts with a testharness.js shim, and reports pass/fail/skip +//! counts grouped by test directory. +//! +//! Run with: `cargo test -p we-browser --test wpt -- --nocapture` + +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +/// Workspace root relative to the crate directory. +const WORKSPACE_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../"); + +/// Maximum instructions per test (prevents infinite loops). +const INSTRUCTION_LIMIT: u64 = 2_000_000; + +/// Timeout for individual test files (in instruction count). +/// Tests exceeding this are marked as timeout. +const TEST_TIMEOUT_INSTRUCTIONS: u64 = 2_000_000; + +/// Minimal testharness.js shim that implements the WPT test API. +/// +/// This provides `test()`, `async_test()`, `promise_test()`, `assert_*()`, +/// and the `Event` constructor needed to run WPT-style HTML tests. +const TESTHARNESS_PREAMBLE: &str = r#" +// --- WPT testharness.js shim --- + +// Global results array: each entry is { name, status, message }. +// status: 0 = PASS, 1 = FAIL, 2 = TIMEOUT, 3 = NOTRUN +var __wpt_results__ = []; +var __wpt_test_count__ = 0; + +function test(func, name) { + if (name === undefined) { + name = "test " + __wpt_test_count__; + } + __wpt_test_count__ = __wpt_test_count__ + 1; + try { + func(); + __wpt_results__.push({ name: name, status: 0, message: "" }); + } catch (e) { + var msg = ""; + if (typeof e === "string") { + msg = e; + } else if (e && e.message) { + msg = e.message; + } else { + msg = String(e); + } + __wpt_results__.push({ name: name, status: 1, message: msg }); + } +} + +// Async test support — simplified for synchronous execution model. +function async_test(nameOrFunc, name) { + if (typeof nameOrFunc === "function") { + // async_test(func, name) — run immediately + test(nameOrFunc, name); + return { step: function(f) { f(); }, done: function() {} }; + } + // async_test(name) — returns test handle + var testName = nameOrFunc; + if (testName === undefined) { + testName = "async_test " + __wpt_test_count__; + } + __wpt_test_count__ = __wpt_test_count__ + 1; + var completed = false; + var t = { + step: function(func) { + if (completed) { return; } + try { + func(); + } catch (e) { + completed = true; + var msg = typeof e === "string" ? e : (e && e.message ? e.message : String(e)); + __wpt_results__.push({ name: testName, status: 1, message: msg }); + } + }, + step_func: function(func) { + var self = this; + return function() { + self.step(func); + }; + }, + done: function() { + if (!completed) { + completed = true; + __wpt_results__.push({ name: testName, status: 0, message: "" }); + } + } + }; + return t; +} + +// Promise test support — simplified for synchronous execution model. +function promise_test(func, name) { + if (name === undefined) { + name = "promise_test " + __wpt_test_count__; + } + __wpt_test_count__ = __wpt_test_count__ + 1; + try { + var result = func(); + // If result is a promise, we can't actually await it in sync mode. + // Mark as pass if no error thrown. + __wpt_results__.push({ name: name, status: 0, message: "" }); + } catch (e) { + var msg = typeof e === "string" ? e : (e && e.message ? e.message : String(e)); + __wpt_results__.push({ name: name, status: 1, message: msg }); + } +} + +// --- Assertion functions --- + +function assert_equals(actual, expected, description) { + if (actual === expected) { return; } + // Handle NaN + if (actual !== actual && expected !== expected) { return; } + var msg = "assert_equals: "; + if (description) { msg = description + ": "; } + msg = msg + "expected " + String(expected) + " but got " + String(actual); + throw new Error(msg); +} + +function assert_not_equals(actual, unexpected, description) { + if (actual !== unexpected) { return; } + var msg = "assert_not_equals: "; + if (description) { msg = description + ": "; } + msg = msg + "got disallowed value " + String(unexpected); + throw new Error(msg); +} + +function assert_true(actual, description) { + if (actual === true) { return; } + var msg = "assert_true: "; + if (description) { msg = description + ": "; } + msg = msg + "expected true but got " + String(actual); + throw new Error(msg); +} + +function assert_false(actual, description) { + if (actual === false) { return; } + var msg = "assert_false: "; + if (description) { msg = description + ": "; } + msg = msg + "expected false but got " + String(actual); + throw new Error(msg); +} + +function assert_throws_js(constructor, func, description) { + var threw = false; + try { + func(); + } catch (e) { + threw = true; + } + if (!threw) { + var msg = "assert_throws_js: "; + if (description) { msg = description + ": "; } + msg = msg + "function did not throw"; + throw new Error(msg); + } +} + +function assert_throws_dom(name, func, description) { + var threw = false; + try { + func(); + } catch (e) { + threw = true; + } + if (!threw) { + var msg = "assert_throws_dom: "; + if (description) { msg = description + ": "; } + msg = msg + "function did not throw"; + throw new Error(msg); + } +} + +function assert_array_equals(actual, expected, description) { + if (actual.length !== expected.length) { + var msg = "assert_array_equals: "; + if (description) { msg = description + ": "; } + msg = msg + "lengths differ: " + actual.length + " vs " + expected.length; + throw new Error(msg); + } + for (var i = 0; i < actual.length; i = i + 1) { + if (actual[i] !== expected[i]) { + var msg2 = "assert_array_equals: "; + if (description) { msg2 = description + ": "; } + msg2 = msg2 + "element " + i + " differs: " + actual[i] + " vs " + expected[i]; + throw new Error(msg2); + } + } +} + +function assert_class_string(object, expected, description) { + // Simplified: just check that the object exists + if (object === null || object === undefined) { + var msg = "assert_class_string: "; + if (description) { msg = description + ": "; } + msg = msg + "object is " + String(object); + throw new Error(msg); + } +} + +function assert_readonly(object, name, description) { + // Simplified: just verify the property exists + if (!(name in object)) { + var msg = "assert_readonly: "; + if (description) { msg = description + ": "; } + msg = msg + "property " + name + " not found"; + throw new Error(msg); + } +} + +function assert_unreached(description) { + var msg = "assert_unreached: "; + if (description) { msg = msg + description; } + throw new Error(msg); +} + +// Event constructor shim (for tests that create events). +if (typeof Event === "undefined") { + function Event(type, options) { + this.type = type; + this.bubbles = false; + this.cancelable = false; + if (options) { + if (options.bubbles) { this.bubbles = options.bubbles; } + if (options.cancelable) { this.cancelable = options.cancelable; } + } + this.defaultPrevented = false; + this.target = null; + this.currentTarget = null; + } +} + +// setup() is a no-op in our harness. +function setup(func_or_properties, maybe_properties) {} + +// done() for manual tests — no-op in our synchronous model. +function done() {} + +// format_value helper used by some tests. +function format_value(value) { + return String(value); +} + +// After all scripts run, dump results to console for the harness to read. +// This is invoked by the test runner after script execution. +"#; + +/// A captured console that stores output. +struct CapturedConsole { + messages: RefCell>, +} + +impl CapturedConsole { + fn new() -> Self { + Self { + messages: RefCell::new(Vec::new()), + } + } +} + +impl we_js::vm::ConsoleOutput for CapturedConsole { + fn log(&self, message: &str) { + self.messages.borrow_mut().push(message.to_string()); + } + fn error(&self, _message: &str) {} + fn warn(&self, _message: &str) {} +} + +/// Wrapper to make Rc implement ConsoleOutput. +struct RcConsole(Rc); + +impl we_js::vm::ConsoleOutput for RcConsole { + fn log(&self, message: &str) { + self.0.log(message); + } + fn error(&self, message: &str) { + self.0.error(message); + } + fn warn(&self, message: &str) { + self.0.warn(message); + } +} + +/// Result of a single subtest within an HTML test file. +#[derive(Debug)] +struct SubtestResult { + name: String, + status: SubtestStatus, + message: String, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum SubtestStatus { + Pass, + Fail, + Timeout, + NotRun, +} + +/// Result of running an entire HTML test file. +#[derive(Debug)] +enum TestFileResult { + /// Test file executed, subtests collected. + Executed(Vec), + /// Test file was skipped. + Skip(String), + /// Test file caused a panic. + Panic, +} + +/// Category statistics for reporting. +struct CategoryStats { + pass: usize, + fail: usize, + skip: usize, + timeout: usize, + error: usize, +} + +impl CategoryStats { + fn new() -> Self { + Self { + pass: 0, + fail: 0, + skip: 0, + timeout: 0, + error: 0, + } + } + + fn total(&self) -> usize { + self.pass + self.fail + self.skip + self.timeout + self.error + } + + fn executed(&self) -> usize { + self.pass + self.fail + } + + fn pass_rate(&self) -> f64 { + let executed = self.executed(); + if executed == 0 { + 0.0 + } else { + (self.pass as f64 / executed as f64) * 100.0 + } + } +} + +/// Recursively collect `.html` test files under a directory. +fn collect_test_files(dir: &Path, files: &mut Vec) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let path = entry.path(); + if path.is_dir() { + collect_test_files(&path, files); + } else if path + .extension() + .map_or(false, |e| e == "html" || e == "htm") + { + files.push(path); + } + } +} + +/// Check if an HTML file is a testharness.js test (contains a reference +/// to testharness.js or uses test()/async_test()/promise_test()). +fn is_testharness_test(html: &str) -> bool { + html.contains("testharness.js") + || html.contains("test(function") + || html.contains("test(()") // arrow function variant + || html.contains("async_test(") + || html.contains("promise_test(") +} + +/// Extract inline script content from all ` + +
+ diff --git a/tests/wpt/dom/collections/getElementsByTagName.html b/tests/wpt/dom/collections/getElementsByTagName.html new file mode 100644 index 0000000..66fcf72 --- /dev/null +++ b/tests/wpt/dom/collections/getElementsByTagName.html @@ -0,0 +1,28 @@ + + +getElementsByTagName + + +
+

first

+

second

+ other +
+
+ diff --git a/tests/wpt/dom/events/Event-dispatch-basic.html b/tests/wpt/dom/events/Event-dispatch-basic.html new file mode 100644 index 0000000..c33a75b --- /dev/null +++ b/tests/wpt/dom/events/Event-dispatch-basic.html @@ -0,0 +1,36 @@ + + +Event dispatch basics + + +
+ diff --git a/tests/wpt/dom/events/Event-propagation.html b/tests/wpt/dom/events/Event-propagation.html new file mode 100644 index 0000000..be100d1 --- /dev/null +++ b/tests/wpt/dom/events/Event-propagation.html @@ -0,0 +1,30 @@ + + +Event propagation (bubbling) + + +
text
+
+ diff --git a/tests/wpt/dom/nodes/Document-createElement.html b/tests/wpt/dom/nodes/Document-createElement.html new file mode 100644 index 0000000..cf86807 --- /dev/null +++ b/tests/wpt/dom/nodes/Document-createElement.html @@ -0,0 +1,27 @@ + + +Document.createElement + + +
+ diff --git a/tests/wpt/dom/nodes/Document-getElementById.html b/tests/wpt/dom/nodes/Document-getElementById.html new file mode 100644 index 0000000..165ba10 --- /dev/null +++ b/tests/wpt/dom/nodes/Document-getElementById.html @@ -0,0 +1,25 @@ + + +Document.getElementById + + +
content
+other +
+ diff --git a/tests/wpt/dom/nodes/Element-getAttribute.html b/tests/wpt/dom/nodes/Element-getAttribute.html new file mode 100644 index 0000000..58f6cac --- /dev/null +++ b/tests/wpt/dom/nodes/Element-getAttribute.html @@ -0,0 +1,36 @@ + + +Element.getAttribute and setAttribute + + +
+ diff --git a/tests/wpt/dom/nodes/Element-innerHTML.html b/tests/wpt/dom/nodes/Element-innerHTML.html new file mode 100644 index 0000000..e4c773c --- /dev/null +++ b/tests/wpt/dom/nodes/Element-innerHTML.html @@ -0,0 +1,20 @@ + + +Element.innerHTML + + +
+ diff --git a/tests/wpt/dom/nodes/Node-appendChild.html b/tests/wpt/dom/nodes/Node-appendChild.html new file mode 100644 index 0000000..21ac891 --- /dev/null +++ b/tests/wpt/dom/nodes/Node-appendChild.html @@ -0,0 +1,28 @@ + + +Node.appendChild + + +
+ diff --git a/tests/wpt/dom/nodes/Node-cloneNode.html b/tests/wpt/dom/nodes/Node-cloneNode.html new file mode 100644 index 0000000..9f6e507 --- /dev/null +++ b/tests/wpt/dom/nodes/Node-cloneNode.html @@ -0,0 +1,21 @@ + + +Node.cloneNode + + +
+ diff --git a/tests/wpt/dom/nodes/Node-insertBefore.html b/tests/wpt/dom/nodes/Node-insertBefore.html new file mode 100644 index 0000000..26f1277 --- /dev/null +++ b/tests/wpt/dom/nodes/Node-insertBefore.html @@ -0,0 +1,23 @@ + + +Node.insertBefore + + +
+ diff --git a/tests/wpt/dom/nodes/Node-removeChild.html b/tests/wpt/dom/nodes/Node-removeChild.html new file mode 100644 index 0000000..ec3197e --- /dev/null +++ b/tests/wpt/dom/nodes/Node-removeChild.html @@ -0,0 +1,23 @@ + + +Node.removeChild + + +
+ diff --git a/tests/wpt/dom/nodes/Node-replaceChild.html b/tests/wpt/dom/nodes/Node-replaceChild.html new file mode 100644 index 0000000..f7dcdd8 --- /dev/null +++ b/tests/wpt/dom/nodes/Node-replaceChild.html @@ -0,0 +1,25 @@ + + +Node.replaceChild + + +
+ diff --git a/tests/wpt/dom/nodes/Node-textContent.html b/tests/wpt/dom/nodes/Node-textContent.html new file mode 100644 index 0000000..8890506 --- /dev/null +++ b/tests/wpt/dom/nodes/Node-textContent.html @@ -0,0 +1,29 @@ + + +Node.textContent + + +
+ diff --git a/tests/wpt/html/dom/document-properties.html b/tests/wpt/html/dom/document-properties.html new file mode 100644 index 0000000..fc82e34 --- /dev/null +++ b/tests/wpt/html/dom/document-properties.html @@ -0,0 +1,31 @@ + + +Document properties + + +
+ diff --git a/tests/wpt/html/dom/document-querySelector.html b/tests/wpt/html/dom/document-querySelector.html new file mode 100644 index 0000000..946d500 --- /dev/null +++ b/tests/wpt/html/dom/document-querySelector.html @@ -0,0 +1,37 @@ + + +Document.querySelector and querySelectorAll + + +
one
+
two
+

three

+
+