Add querySelector, querySelectorAll, getElementsByTagName, and getElementsByClassName on Document, Element, and DocumentFragment. Live HTMLCollections re-evaluate on every access. Code review fixed: collection persistence across script invocations, multi-class getElementsByClassName matching, DocumentFragment query support, and added 17 integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
915 lines
29 KiB
Rust
915 lines
29 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.someUnknownMethod();
|
|
"#,
|
|
);
|
|
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.someUnknownMethod();"#);
|
|
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;
|
|
}
|
|
|
|
// --- insertBefore integration tests ---
|
|
|
|
#[test]
|
|
fn insert_before_reorders_children_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var parent = document.getElementById("target");
|
|
var a = document.createElement("span");
|
|
a.textContent = "A";
|
|
var b = document.createElement("span");
|
|
b.textContent = "B";
|
|
parent.appendChild(b);
|
|
parent.insertBefore(a, b);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let text = facade.document().text_content(div);
|
|
// Text should be "originalAB" (original text + A before B)
|
|
assert!(
|
|
text.contains("AB"),
|
|
"expected A before B in text content, got: {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn insert_before_null_appends_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var parent = document.getElementById("target");
|
|
var child = document.createElement("em");
|
|
child.textContent = "appended";
|
|
parent.insertBefore(child, null);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let children = facade.document().children(div);
|
|
let last = *children.last().unwrap();
|
|
assert_eq!(facade.document().text_content(last), "appended");
|
|
}
|
|
|
|
#[test]
|
|
fn insert_before_moves_existing_node_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var parent = document.getElementById("target");
|
|
var a = document.createElement("span");
|
|
a.textContent = "A";
|
|
var b = document.createElement("span");
|
|
b.textContent = "B";
|
|
parent.appendChild(a);
|
|
parent.appendChild(b);
|
|
// Move a after b by inserting before null (append)
|
|
parent.insertBefore(a, null);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let text = facade.document().text_content(div);
|
|
assert!(
|
|
text.ends_with("BA"),
|
|
"expected B before A after move, got: {text}"
|
|
);
|
|
}
|
|
|
|
// --- replaceChild integration tests ---
|
|
|
|
#[test]
|
|
fn replace_child_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var parent = document.getElementById("target");
|
|
var old_child = document.createElement("span");
|
|
old_child.textContent = "old";
|
|
parent.appendChild(old_child);
|
|
var new_child = document.createElement("em");
|
|
new_child.textContent = "new";
|
|
parent.replaceChild(new_child, old_child);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let text = facade.document().text_content(div);
|
|
assert!(text.contains("new"), "expected 'new' in text, got: {text}");
|
|
assert!(!text.contains("old"), "expected 'old' removed, got: {text}");
|
|
}
|
|
|
|
#[test]
|
|
fn replace_child_returns_old_child_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
let val = facade
|
|
.execute_script(
|
|
r#"
|
|
var parent = document.getElementById("target");
|
|
var old_child = document.createElement("span");
|
|
parent.appendChild(old_child);
|
|
var new_child = document.createElement("em");
|
|
var returned = parent.replaceChild(new_child, old_child);
|
|
typeof returned;
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::String("object".into()));
|
|
}
|
|
|
|
// --- createDocumentFragment integration tests ---
|
|
|
|
#[test]
|
|
fn create_fragment_add_children_append_to_dom_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var frag = document.createDocumentFragment();
|
|
var a = document.createElement("span");
|
|
a.textContent = "one";
|
|
var b = document.createElement("span");
|
|
b.textContent = "two";
|
|
var c = document.createElement("span");
|
|
c.textContent = "three";
|
|
frag.appendChild(a);
|
|
frag.appendChild(b);
|
|
frag.appendChild(c);
|
|
var parent = document.getElementById("target");
|
|
parent.appendChild(frag);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let text = facade.document().text_content(div);
|
|
assert!(
|
|
text.contains("onetwothree"),
|
|
"expected fragment children transferred, got: {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fragment_insert_before_transfers_children_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var parent = document.getElementById("target");
|
|
var existing = document.createElement("span");
|
|
existing.textContent = "existing";
|
|
parent.appendChild(existing);
|
|
|
|
var frag = document.createDocumentFragment();
|
|
var a = document.createElement("em");
|
|
a.textContent = "A";
|
|
var b = document.createElement("em");
|
|
b.textContent = "B";
|
|
frag.appendChild(a);
|
|
frag.appendChild(b);
|
|
|
|
parent.insertBefore(frag, existing);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let text = facade.document().text_content(div);
|
|
// Should be "original" + "A" + "B" + "existing"
|
|
assert!(
|
|
text.contains("ABexisting"),
|
|
"expected fragment children before existing, got: {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn move_node_to_different_parent_via_append_child() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var parent1 = document.getElementById("target");
|
|
var parent2 = document.createElement("div");
|
|
var child = document.createElement("span");
|
|
child.textContent = "movable";
|
|
parent1.appendChild(child);
|
|
// Moving child from parent1 to parent2
|
|
parent2.appendChild(child);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let text = facade.document().text_content(div);
|
|
assert!(
|
|
!text.contains("movable"),
|
|
"expected child moved away, got: {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn replace_child_with_fragment_via_script() {
|
|
let (mut facade, _, _) = setup_facade();
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var parent = document.getElementById("target");
|
|
var old_child = document.createElement("span");
|
|
old_child.textContent = "old";
|
|
parent.appendChild(old_child);
|
|
|
|
var frag = document.createDocumentFragment();
|
|
var a = document.createElement("em");
|
|
a.textContent = "A";
|
|
var b = document.createElement("em");
|
|
b.textContent = "B";
|
|
frag.appendChild(a);
|
|
frag.appendChild(b);
|
|
|
|
parent.replaceChild(frag, old_child);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let div = facade.document().get_element_by_id("target").unwrap();
|
|
let text = facade.document().text_content(div);
|
|
assert!(
|
|
text.contains("AB"),
|
|
"expected fragment children to replace old child, got: {text}"
|
|
);
|
|
assert!(
|
|
!text.contains("old"),
|
|
"expected old child removed, got: {text}"
|
|
);
|
|
}
|
|
|
|
// --- 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()
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// querySelector / querySelectorAll integration tests (Story 2.5)
|
|
// =========================================================================
|
|
|
|
/// Helper: build a facade with a richer DOM for query testing.
|
|
/// Structure: html > body > div#container > (span.item + p.item.active + p.item)
|
|
fn setup_query_facade() -> web_api::WebApiFacade {
|
|
let mut facade = web_api::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 container = doc.create_element("div");
|
|
doc.set_attribute(container, "id", "container");
|
|
doc.append_child(body, container);
|
|
let span = doc.create_element("span");
|
|
doc.set_attribute(span, "class", "item");
|
|
doc.append_child(container, span);
|
|
let p1 = doc.create_element("p");
|
|
doc.set_attribute(p1, "class", "item active");
|
|
doc.append_child(container, p1);
|
|
let p2 = doc.create_element("p");
|
|
doc.set_attribute(p2, "class", "item");
|
|
doc.append_child(container, p2);
|
|
|
|
facade.bootstrap().unwrap();
|
|
facade
|
|
}
|
|
|
|
#[test]
|
|
fn query_selector_returns_first_match_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"typeof document.querySelector("p");"#)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::String("object".into()));
|
|
}
|
|
|
|
#[test]
|
|
fn query_selector_no_match_returns_null_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"document.querySelector("table");"#)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::Null);
|
|
}
|
|
|
|
#[test]
|
|
fn query_selector_all_returns_correct_count_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"document.querySelectorAll("p").length;"#)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::Number(2.0));
|
|
}
|
|
|
|
#[test]
|
|
fn query_selector_all_empty_result_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"document.querySelectorAll("table").length;"#)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::Number(0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn query_selector_invalid_selector_throws_syntax_error_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let result = facade.execute_script(r#"document.querySelector("[[[bad");"#);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(
|
|
err.contains("SyntaxError"),
|
|
"expected SyntaxError, got: {err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn query_selector_combinator_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"document.querySelectorAll("div > p").length;"#)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::Number(2.0));
|
|
}
|
|
|
|
#[test]
|
|
fn element_scoped_query_selector_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(
|
|
r#"
|
|
var c = document.getElementById("container");
|
|
c.querySelectorAll(".item").length;
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::Number(3.0));
|
|
}
|
|
|
|
// =========================================================================
|
|
// getElementsByTagName / getElementsByClassName integration tests (Story 2.5)
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn get_elements_by_tag_name_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"document.getElementsByTagName("p").length;"#)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::Number(2.0));
|
|
}
|
|
|
|
#[test]
|
|
fn get_elements_by_class_name_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"document.getElementsByClassName("item").length;"#)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::Number(3.0));
|
|
}
|
|
|
|
#[test]
|
|
fn get_elements_by_tag_name_wildcard_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(r#"document.getElementsByTagName("*").length;"#)
|
|
.unwrap();
|
|
// html, body, div#container, span.item, p.item.active, p.item = 6
|
|
assert_eq!(val, js::JsValue::Number(6.0));
|
|
}
|
|
|
|
#[test]
|
|
fn live_collection_reflects_dom_add_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(
|
|
r#"
|
|
var coll = document.getElementsByTagName("p");
|
|
var before = coll.length;
|
|
var newP = document.createElement("p");
|
|
document.getElementById("container").appendChild(newP);
|
|
var after = coll.length;
|
|
after - before;
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
val,
|
|
js::JsValue::Number(1.0),
|
|
"live collection should reflect 1 new p element"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn live_collection_reflects_dom_remove_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(
|
|
r#"
|
|
var coll = document.getElementsByTagName("p");
|
|
var before = coll.length;
|
|
var container = document.getElementById("container");
|
|
var firstP = document.querySelector("p");
|
|
container.removeChild(firstP);
|
|
var after = coll.length;
|
|
before - after;
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
val,
|
|
js::JsValue::Number(1.0),
|
|
"live collection should decrease by 1 after removing a p"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn live_collection_item_method_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
let val = facade
|
|
.execute_script(
|
|
r#"
|
|
var coll = document.getElementsByTagName("p");
|
|
typeof coll.item(0);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(val, js::JsValue::String("object".into()));
|
|
}
|
|
|
|
#[test]
|
|
fn live_collection_persists_across_scripts() {
|
|
let mut facade = setup_query_facade();
|
|
// Script 1: create collection and read initial length
|
|
facade
|
|
.execute_script(
|
|
r#"
|
|
var coll = document.getElementsByTagName("p");
|
|
var initialLen = coll.length;
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
// Script 2: add a p and re-check collection length
|
|
let val = facade
|
|
.execute_script(
|
|
r#"
|
|
var newP = document.createElement("p");
|
|
document.getElementById("container").appendChild(newP);
|
|
coll.length;
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
val,
|
|
js::JsValue::Number(3.0),
|
|
"live collection should persist and reflect 3 p elements across scripts"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn get_elements_by_class_name_multi_class_via_js() {
|
|
let mut facade = setup_query_facade();
|
|
// Only p.item.active has both classes
|
|
let val = facade
|
|
.execute_script(r#"document.getElementsByClassName("item active").length;"#)
|
|
.unwrap();
|
|
assert_eq!(
|
|
val,
|
|
js::JsValue::Number(1.0),
|
|
"multi-class query should match only elements with ALL classes"
|
|
);
|
|
}
|