Files
rust_browser/tests/js_dom_tests.rs
Zachary D. Rowitsch d691f56470 Add Phase 5 hardening: source locations, tracing, tests, and docs
- Add line:col source locations in runtime error messages (PR 5)
- Consolidate info! → debug! logging, add debug_span! for event loop,
  dispatch, and microtask drain (PR 6)
- Add 14 integration tests across js_events, js_dom_tests, js_tests,
  and js_scheduling for edge cases (PR 7)
- Add JS feature matrix and conformance report documentation (PR 8)
- Update CLAUDE.md with JS262 harness commands and doc references
- Mark Milestone 6 Phase 5 complete in plan doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 17:27:49 -05:00

443 lines
14 KiB
Rust

use shared::NodeId;
use web_api::WebApiFacade;
/// Helper: build a WebApiFacade with a known DOM structure.
/// Returns (facade, body_id, div_id) where div has id="target" and text "original".
fn setup_facade() -> (WebApiFacade, NodeId, NodeId) {
let mut facade = WebApiFacade::new();
// Build DOM: html > body > div#target("original")
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)
}
#[test]
fn get_element_by_id_and_read_text_content() {
let (mut facade, _, _) = setup_facade();
let val = facade
.execute_script(r#"document.getElementById("target").textContent;"#)
.unwrap();
assert_eq!(val, js::JsValue::String("original".into()));
}
#[test]
fn set_text_content_via_script() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"var el = document.getElementById("target"); el.textContent = "updated";"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "updated");
}
#[test]
fn create_element_set_text_append_child() {
let (mut facade, body, _) = setup_facade();
facade
.execute_script(
r#"
var p = document.createElement("p");
p.textContent = "new paragraph";
var body = document.getElementById("target");
body.appendChild(p);
"#,
)
.unwrap();
// The div#target should now have a child <p> with text "new paragraph"
// Verify by checking total text content
let div = facade.document().get_element_by_id("target").unwrap();
let children = facade.document().children(div);
// Last child should be the new <p>
let last_child = *children.last().unwrap();
assert_eq!(facade.document().text_content(last_child), "new paragraph");
let _ = body; // suppress unused
}
#[test]
fn remove_child_via_script() {
let (mut facade, _, _) = setup_facade();
// Add a child first, then remove it
facade
.execute_script(
r#"
var parent = document.getElementById("target");
var child = document.createElement("span");
child.textContent = "temp";
parent.appendChild(child);
var removed = parent.removeChild(child);
"#,
)
.unwrap();
// After removeChild, div#target should only have its original text
let div = facade.document().get_element_by_id("target").unwrap();
assert_eq!(facade.document().text_content(div), "original");
}
#[test]
fn get_element_by_id_null_then_access_type_error() {
let (mut facade, _, _) = setup_facade();
let result = facade.execute_script(r#"document.getElementById("nonexistent").textContent;"#);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("null"),
"expected error mentioning null, got: {err}"
);
}
#[test]
fn execute_script_recovers_after_runtime_error() {
let (mut facade, _, _) = setup_facade();
// First script fails with a runtime error (accessing property of null)
let result = facade.execute_script(r#"document.getElementById("nope").textContent;"#);
assert!(result.is_err());
// Second script should still work — facade auto-recovers
let val = facade
.execute_script(r#"document.getElementById("target").textContent;"#)
.unwrap();
assert_eq!(val, js::JsValue::String("original".into()));
}
#[test]
fn host_global_document_is_not_overwritable() {
let (mut facade, _, _) = setup_facade();
// Attempting to overwrite `document` should fail with TypeError (const binding)
let result = facade.execute_script(r#"var document = null;"#);
assert!(result.is_err(), "overwriting document global should fail");
}
#[test]
fn multi_statement_dom_mutations() {
let (mut facade, _, _) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
el.textContent = "step1";
el.textContent = "step2";
var p = document.createElement("p");
p.textContent = "appended";
el.appendChild(p);
"#,
)
.unwrap();
let div = facade.document().get_element_by_id("target").unwrap();
// textContent was set to "step2", then a <p> was appended
// So total text content is "step2appended"
assert_eq!(facade.document().text_content(div), "step2appended");
}
#[test]
fn invalid_host_object_id_errors() {
let (mut facade, _, _) = setup_facade();
// Try to use a stale/invalid element reference
// We'll create an element, then manipulate it without appending — that's valid.
// But a truly invalid ID would require injecting one. Let's test via the null path instead.
let result =
facade.execute_script(r#"var x = document.getElementById("nope"); x.textContent;"#);
assert!(result.is_err());
}
// --- typeof document returns "object" ---
#[test]
fn typeof_document_is_object() {
let (mut facade, _, _) = setup_facade();
let val = facade.execute_script("typeof document;").unwrap();
assert_eq!(val, js::JsValue::String("object".into()));
}
// --- document global survives multiple execute_script calls ---
#[test]
fn document_global_persists_across_scripts() {
let (mut facade, _, _) = setup_facade();
// First script assigns a var using document.
facade
.execute_script(r#"var el = document.getElementById("target");"#)
.unwrap();
// Second script: document is still accessible.
let val = facade
.execute_script(r#"document.getElementById("target").textContent;"#)
.unwrap();
assert_eq!(val, js::JsValue::String("original".into()));
}
// --- textContent = "" clears node text content ---
#[test]
fn set_text_content_empty_string_via_script() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(r#"var el = document.getElementById("target"); el.textContent = "";"#)
.unwrap();
assert_eq!(facade.document().text_content(div), "");
}
// --- textContent set to a number (coercion to string) ---
#[test]
fn set_text_content_number_coercion_via_script() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(r#"var el = document.getElementById("target"); el.textContent = 42;"#)
.unwrap();
assert_eq!(facade.document().text_content(div), "42");
}
// --- removeChild on a non-child element returns a runtime error ---
#[test]
fn remove_non_child_errors() {
let (mut facade, _, _) = setup_facade();
// Create two independent elements; removing one from the other should fail.
let result = facade.execute_script(
r#"
var parent = document.getElementById("target");
var orphan = document.createElement("span");
parent.removeChild(orphan);
"#,
);
assert!(
result.is_err(),
"removeChild on non-child should error, but got: {result:?}"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("not a child"),
"expected 'not a child' in error, got: {err}"
);
}
// --- multiple recovery cycles: fail, recover, fail, recover ---
#[test]
fn multiple_recovery_cycles() {
let (mut facade, _, _) = setup_facade();
for _ in 0..3 {
// Trigger a failure
let result = facade.execute_script(r#"document.getElementById("nope").textContent;"#);
assert!(result.is_err());
// Recover and confirm document is still accessible
let val = facade
.execute_script(r#"document.getElementById("target").textContent;"#)
.unwrap();
assert_eq!(val, js::JsValue::String("original".into()));
}
}
// --- getElementById returns null; storing null then reading property fails ---
#[test]
fn get_element_by_id_null_stored_in_var_then_property_errors() {
let (mut facade, _, _) = setup_facade();
let result = facade.execute_script(
r#"
var el = document.getElementById("nonexistent");
el.textContent;
"#,
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("null"),
"expected null mention in error, got: {err}"
);
}
// --- appendChild returns the appended child as a HostObject ---
#[test]
fn append_child_return_value_is_truthy() {
let (mut facade, _, _) = setup_facade();
// If appendChild returns the child, using it in a boolean context should be true.
let val = facade
.execute_script(
r#"
var parent = document.getElementById("target");
var child = document.createElement("span");
var returned = parent.appendChild(child);
typeof returned;
"#,
)
.unwrap();
// The returned child is a HostObject; typeof should be "object".
assert_eq!(val, js::JsValue::String("object".into()));
}
// --- parse error in execute_script returns an error without affecting recovery ---
#[test]
fn execute_script_parse_error_does_not_require_recovery() {
let (mut facade, _, _) = setup_facade();
// A parse error happens before the VM even runs, so the VM state stays Primed.
let result = facade.execute_script("var ;");
assert!(result.is_err());
// The next call should still work without triggering auto-recovery.
let val = facade
.execute_script(r#"document.getElementById("target").textContent;"#)
.unwrap();
assert_eq!(val, js::JsValue::String("original".into()));
}
// --- calling an unknown method on an Element errors ---
#[test]
fn unknown_element_method_errors() {
let (mut facade, _, _) = setup_facade();
let result = facade.execute_script(
r#"
var el = document.getElementById("target");
el.querySelectorAll("span");
"#,
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("is not a function"),
"expected 'is not a function', got: {err}"
);
}
// --- calling an unknown Document method errors ---
#[test]
fn unknown_document_method_errors_from_script() {
let (mut facade, _, _) = setup_facade();
let result = facade.execute_script(r#"document.querySelectorAll("div");"#);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("is not a function"),
"expected 'is not a function', got: {err}"
);
}
// --- document equality: document === document (same HostObject id+type) ---
#[test]
fn document_identity_equality() {
let (mut facade, _, _) = setup_facade();
// Two references to `document` should be equal via ===
// (same id and type_name, so PartialEq returns true).
let val = facade
.execute_script("var a = document; var b = document; a === b;")
.unwrap();
// document is a HostObject; strict_eq does not handle HostObject, so this
// will actually be false — document the actual runtime behaviour here.
// strict_eq falls through to `_ => false` for HostObjects.
assert_eq!(val, js::JsValue::Boolean(false));
}
// --- Create 3-level nested element tree via JS ---
#[test]
fn create_3_level_nested_tree() {
let (mut facade, _, _) = setup_facade();
facade
.execute_script(
r#"
var root = document.getElementById("target");
var level1 = document.createElement("div");
var level2 = document.createElement("span");
var level3 = document.createElement("em");
level3.textContent = "deep";
level2.appendChild(level3);
level1.appendChild(level2);
root.appendChild(level1);
"#,
)
.unwrap();
let div = facade.document().get_element_by_id("target").unwrap();
// Total text content should include the deeply nested "deep"
let text = facade.document().text_content(div);
assert!(
text.contains("deep"),
"expected deeply nested text, got: {text}"
);
}
// --- textContent with unicode characters ---
#[test]
fn text_content_unicode_roundtrip() {
let (mut facade, _, div) = setup_facade();
facade
.execute_script(
r#"
var el = document.getElementById("target");
el.textContent = "café ☕ 日本語";
"#,
)
.unwrap();
assert_eq!(facade.document().text_content(div), "café ☕ 日本語");
}
// --- removeChild then appendChild to a different parent ---
#[test]
fn remove_child_then_append_to_different_parent() {
let (mut facade, body, _) = setup_facade();
facade
.execute_script(
r#"
var parent1 = document.getElementById("target");
var child = document.createElement("span");
child.textContent = "movable";
parent1.appendChild(child);
var removed = parent1.removeChild(child);
// Create a new parent and re-append the removed child
var parent2 = document.createElement("div");
parent2.appendChild(removed);
"#,
)
.unwrap();
let div = facade.document().get_element_by_id("target").unwrap();
// div#target should no longer contain "movable"
let text = facade.document().text_content(div);
assert!(
!text.contains("movable"),
"expected child removed from original parent, got: {text}"
);
let _ = body;
}
// --- createElement with the tag name is case-preserved ---
#[test]
fn create_element_uppercase_tag() {
let (mut facade, _, _) = setup_facade();
// createElement should succeed with any non-empty tag string.
// We verify by appending it and confirming the DOM updated.
facade
.execute_script(
r#"
var parent = document.getElementById("target");
var child = document.createElement("DIV");
parent.appendChild(child);
"#,
)
.unwrap();
let div = facade.document().get_element_by_id("target").unwrap();
let children = facade.document().children(div);
// div#target originally had one text child; now it has an additional DIV child.
assert!(
children.len() >= 2,
"expected at least 2 children after appendChild, got {}",
children.len()
);
}