Files
rust_browser/tests/js_lifecycle.rs
Zachary D. Rowitsch 626a1c517c Implement document lifecycle events with code review fixes (§8.4, §7.1)
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>
2026-03-15 00:38:46 -04:00

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