Add DOMContentLoaded, load, and readystatechange events with correct readyState transitions (loading→interactive→complete). Includes Window as a first-class event target, body onload spec quirk, and idempotency guards to prevent double-firing. Code review hardened the API surface by enforcing forward-only state transitions, eliminating a redundant wrapper function, and requesting a redraw after load handler DOM mutations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
142 lines
4.3 KiB
Rust
142 lines
4.3 KiB
Rust
use web_api::WebApiFacade;
|
|
|
|
/// Helper: build a WebApiFacade with a basic DOM (html > body).
|
|
fn setup_facade() -> WebApiFacade {
|
|
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);
|
|
facade.bootstrap().unwrap();
|
|
facade
|
|
}
|
|
|
|
// --- 5.1: DOMContentLoaded fires after scripts ---
|
|
|
|
#[test]
|
|
fn dom_content_loaded_fires_after_scripts() {
|
|
let mut facade = setup_facade();
|
|
|
|
// Script registers listeners and records state
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var state = [];
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
state.push("dcl:" + document.readyState);
|
|
});
|
|
state.push("script:" + document.readyState);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
// At this point, DOMContentLoaded hasn't fired yet
|
|
let before = facade.execute_script("state.length;").unwrap();
|
|
assert_eq!(before, js::JsValue::Number(1.0));
|
|
|
|
// Fire lifecycle events
|
|
facade.fire_dom_content_loaded();
|
|
|
|
// Verify state = ["script:loading", "dcl:interactive"]
|
|
let len = facade.execute_script("state.length;").unwrap();
|
|
assert_eq!(len, js::JsValue::Number(2.0));
|
|
|
|
let s0 = facade.execute_script("state[0];").unwrap();
|
|
assert_eq!(s0, js::JsValue::String("script:loading".into()));
|
|
|
|
let s1 = facade.execute_script("state[1];").unwrap();
|
|
assert_eq!(s1, js::JsValue::String("dcl:interactive".into()));
|
|
}
|
|
|
|
// --- 5.2: load fires after DOMContentLoaded ---
|
|
|
|
#[test]
|
|
fn load_fires_after_dom_content_loaded() {
|
|
let mut facade = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var order = [];
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
order.push("dcl:" + document.readyState);
|
|
});
|
|
window.addEventListener("load", function() {
|
|
order.push("load:" + document.readyState);
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.fire_dom_content_loaded();
|
|
facade.fire_load_event();
|
|
|
|
let len = facade.execute_script("order.length;").unwrap();
|
|
assert_eq!(len, js::JsValue::Number(2.0));
|
|
|
|
let s0 = facade.execute_script("order[0];").unwrap();
|
|
assert_eq!(s0, js::JsValue::String("dcl:interactive".into()));
|
|
|
|
let s1 = facade.execute_script("order[1];").unwrap();
|
|
assert_eq!(s1, js::JsValue::String("load:complete".into()));
|
|
}
|
|
|
|
// --- 5.3: readystatechange fires on each transition ---
|
|
|
|
#[test]
|
|
fn readystatechange_fires_on_each_transition() {
|
|
let mut facade = setup_facade();
|
|
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var changes = [];
|
|
document.addEventListener("readystatechange", function() {
|
|
changes.push(document.readyState);
|
|
});
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
facade.fire_dom_content_loaded();
|
|
facade.fire_load_event();
|
|
|
|
let len = facade.execute_script("changes.length;").unwrap();
|
|
assert_eq!(len, js::JsValue::Number(2.0));
|
|
|
|
let s0 = facade.execute_script("changes[0];").unwrap();
|
|
assert_eq!(s0, js::JsValue::String("interactive".into()));
|
|
|
|
let s1 = facade.execute_script("changes[1];").unwrap();
|
|
assert_eq!(s1, js::JsValue::String("complete".into()));
|
|
}
|
|
|
|
// --- 5.4: inline <body onload="..."> fires as window load event ---
|
|
|
|
#[test]
|
|
fn body_onload_fires_as_window_load_event() {
|
|
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, "onload", "window.bodyLoadFired = true;");
|
|
doc.append_child(html, body);
|
|
|
|
facade.bootstrap().unwrap();
|
|
facade.install_inline_event_handlers();
|
|
|
|
// Before lifecycle events, bodyLoadFired should not be set
|
|
let before = facade.execute_script("window.bodyLoadFired;").unwrap();
|
|
assert_eq!(before, js::JsValue::Undefined);
|
|
|
|
facade.fire_dom_content_loaded();
|
|
facade.fire_load_event();
|
|
|
|
let after = facade.execute_script("window.bodyLoadFired;").unwrap();
|
|
assert_eq!(after, js::JsValue::Boolean(true));
|
|
}
|