Files
rust_browser/tests/js_events.rs
Zachary D. Rowitsch 336b42c277
Some checks failed
ci / fast (linux) (push) Failing after 1m41s
Fix code review issues, add test coverage, and document Test262 roadmap
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>
2026-02-20 18:26:21 -05:00

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");
}