Some checks failed
ci / fast (linux) (push) Failing after 1m41s
Address 11 code review findings: fix DOM-mode vacuous error pass, add source locations to ReferenceError/TypeError, fix progress counter overcounting, harden promise registry with overflow guards, and clean up misleading test assertions. Add 16 new tests covering the fixes. Create Test262 roadmap documenting the feature gap for real conformance testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
386 lines
11 KiB
Rust
386 lines
11 KiB
Rust
use shared::NodeId;
|
|
use web_api::WebApiFacade;
|
|
|
|
/// Helper: build a WebApiFacade with a DOM: html > body > div#target("original")
|
|
/// Returns (facade, body_id, div_id).
|
|
fn setup_facade() -> (WebApiFacade, NodeId, NodeId) {
|
|
let mut facade = WebApiFacade::new();
|
|
|
|
let doc = facade.document_mut();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.append_child(html, body);
|
|
let div = doc.create_element("div");
|
|
doc.set_attribute(div, "id", "target");
|
|
doc.append_child(body, div);
|
|
let text = doc.create_text("original");
|
|
doc.append_child(div, text);
|
|
|
|
facade.bootstrap().unwrap();
|
|
(facade, body, div)
|
|
}
|
|
|
|
// --- Basic click handler registration and dispatch ---
|
|
|
|
#[test]
|
|
fn click_handler_mutates_dom() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function(event) {
|
|
el.textContent = "clicked";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let result = facade.dispatch_click(div).unwrap();
|
|
assert!(!result.default_prevented);
|
|
assert_eq!(facade.document().text_content(div), "clicked");
|
|
}
|
|
|
|
// --- Multiple handlers on same element fire in insertion order ---
|
|
|
|
#[test]
|
|
fn multiple_handlers_fire_in_order() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function() {
|
|
el.textContent = "first";
|
|
});
|
|
el.addEventListener("click", function() {
|
|
el.textContent = el.textContent + "_second";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(div).unwrap();
|
|
assert_eq!(facade.document().text_content(div), "first_second");
|
|
}
|
|
|
|
// --- preventDefault sets result flag ---
|
|
|
|
#[test]
|
|
fn prevent_default_sets_result() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function(event) {
|
|
event.preventDefault();
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let result = facade.dispatch_click(div).unwrap();
|
|
assert!(result.default_prevented);
|
|
}
|
|
|
|
// --- removeEventListener prevents handler from firing ---
|
|
|
|
#[test]
|
|
fn remove_event_listener_prevents_fire() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
var handler = function() {
|
|
el.textContent = "should not happen";
|
|
};
|
|
el.addEventListener("click", handler);
|
|
el.removeEventListener("click", handler);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(div).unwrap();
|
|
// Text should remain "original" since handler was removed
|
|
assert_eq!(facade.document().text_content(div), "original");
|
|
}
|
|
|
|
// --- Bubbling: child click reaches parent handler ---
|
|
|
|
#[test]
|
|
fn event_bubbles_to_parent() {
|
|
let mut facade = WebApiFacade::new();
|
|
let doc = facade.document_mut();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.set_attribute(body, "id", "body");
|
|
doc.append_child(html, body);
|
|
let outer = doc.create_element("div");
|
|
doc.set_attribute(outer, "id", "outer");
|
|
doc.append_child(body, outer);
|
|
let inner = doc.create_element("span");
|
|
doc.set_attribute(inner, "id", "inner");
|
|
doc.append_child(outer, inner);
|
|
let text = doc.create_text("hello");
|
|
doc.append_child(inner, text);
|
|
|
|
facade.bootstrap().unwrap();
|
|
|
|
// Register handler on outer (parent of inner)
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var outer = document.getElementById("outer");
|
|
outer.addEventListener("click", function() {
|
|
outer.textContent = "bubbled";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
// Dispatch click on inner — should bubble to outer
|
|
facade.dispatch_click(inner).unwrap();
|
|
assert_eq!(facade.document().text_content(outer), "bubbled");
|
|
}
|
|
|
|
// --- stopPropagation halts bubbling ---
|
|
|
|
#[test]
|
|
fn stop_propagation_halts_bubbling() {
|
|
let mut facade = WebApiFacade::new();
|
|
let doc = facade.document_mut();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.set_attribute(body, "id", "body");
|
|
doc.append_child(html, body);
|
|
let child = doc.create_element("div");
|
|
doc.set_attribute(child, "id", "child");
|
|
doc.append_child(body, child);
|
|
let text = doc.create_text("init");
|
|
doc.append_child(child, text);
|
|
|
|
facade.bootstrap().unwrap();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var child = document.getElementById("child");
|
|
var body = document.getElementById("body");
|
|
child.addEventListener("click", function(event) {
|
|
event.stopPropagation();
|
|
child.textContent = "child_handled";
|
|
});
|
|
body.addEventListener("click", function() {
|
|
body.textContent = "body_should_not_fire";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(child).unwrap();
|
|
assert_eq!(facade.document().text_content(child), "child_handled");
|
|
// body listener should NOT have fired due to stopPropagation
|
|
assert_ne!(facade.document().text_content(body), "body_should_not_fire");
|
|
}
|
|
|
|
// --- Handler throws exception — runtime recovers, other handlers still fire ---
|
|
|
|
#[test]
|
|
fn handler_exception_does_not_block_others() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function() {
|
|
undeclaredVariable;
|
|
});
|
|
el.addEventListener("click", function() {
|
|
el.textContent = "recovered";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(div).unwrap();
|
|
// Second handler should fire despite the first one throwing
|
|
assert_eq!(facade.document().text_content(div), "recovered");
|
|
}
|
|
|
|
// --- No listeners = no error ---
|
|
|
|
#[test]
|
|
fn no_listeners_dispatch_succeeds() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
let result = facade.dispatch_click(div).unwrap();
|
|
assert!(!result.default_prevented);
|
|
}
|
|
|
|
// --- Re-entrant dispatch is blocked ---
|
|
|
|
#[test]
|
|
fn dispatch_with_no_crash() {
|
|
// Basic test that dispatch doesn't crash with various scenarios
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function(event) {
|
|
el.textContent = "ok";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
// Multiple sequential dispatches work fine
|
|
facade.dispatch_click(div).unwrap();
|
|
assert_eq!(facade.document().text_content(div), "ok");
|
|
|
|
// Can dispatch again
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function() {
|
|
el.textContent = "second_click";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
facade.dispatch_click(div).unwrap();
|
|
assert_eq!(facade.document().text_content(div), "second_click");
|
|
}
|
|
|
|
// --- Document listener fires during bubbling ---
|
|
|
|
#[test]
|
|
fn document_listener_fires_on_bubble() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
document.addEventListener("click", function() {
|
|
el.textContent = "doc_heard";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(div).unwrap();
|
|
assert_eq!(facade.document().text_content(div), "doc_heard");
|
|
}
|
|
|
|
// --- Event properties are accessible from handler ---
|
|
|
|
#[test]
|
|
fn event_properties_accessible() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function(event) {
|
|
el.textContent = event.type;
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(div).unwrap();
|
|
assert_eq!(facade.document().text_content(div), "click");
|
|
}
|
|
|
|
// --- Handler does createElement + appendChild + reads textContent ---
|
|
|
|
#[test]
|
|
fn handler_creates_and_appends_element() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function() {
|
|
var span = document.createElement("span");
|
|
span.textContent = "dynamic";
|
|
el.appendChild(span);
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(div).unwrap();
|
|
// After appendChild, textContent includes both the original text and the appended span's text
|
|
assert_eq!(facade.document().text_content(div), "originaldynamic");
|
|
}
|
|
|
|
// --- Second handler sees DOM changes from first handler ---
|
|
|
|
#[test]
|
|
fn second_handler_sees_first_handlers_dom_changes() {
|
|
let (mut facade, _, div) = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var el = document.getElementById("target");
|
|
el.addEventListener("click", function() {
|
|
el.textContent = "modified_by_first";
|
|
});
|
|
el.addEventListener("click", function() {
|
|
el.textContent = el.textContent + "_seen_by_second";
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.dispatch_click(div).unwrap();
|
|
assert_eq!(
|
|
facade.document().text_content(div),
|
|
"modified_by_first_seen_by_second"
|
|
);
|
|
}
|
|
|
|
// --- Dispatching on an element without an id attribute does not panic ---
|
|
|
|
#[test]
|
|
fn dispatch_click_on_element_without_id_is_noop() {
|
|
let mut facade = WebApiFacade::new();
|
|
let doc = facade.document_mut();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.append_child(html, body);
|
|
// No id attribute set on this div
|
|
let div = doc.create_element("div");
|
|
doc.append_child(body, div);
|
|
let text = doc.create_text("no_id");
|
|
doc.append_child(div, text);
|
|
|
|
facade.bootstrap().unwrap();
|
|
|
|
// dispatch_click on a node with no listeners and no id must not panic
|
|
let result = facade.dispatch_click(div).unwrap();
|
|
assert!(!result.default_prevented);
|
|
assert_eq!(facade.document().text_content(div), "no_id");
|
|
}
|