Merge remote-tracking branch 'origin/main' into maintainer/pr-517-tests
This commit is contained in:
commit
037e321a79
5 files changed
+183
-19
No files matched your search
@@ -580,6 +580,10 @@ impl DomTree {
|
|||||||
// Fast path: a bare "#id" selector resolves through the id index in O(1)
|
// Fast path: a bare "#id" selector resolves through the id index in O(1)
|
||||||
// instead of scanning every descendant. The index holds the first element
|
// instead of scanning every descendant. The index holds the first element
|
||||||
// in tree order per id, which is exactly what the full scan would return.
|
// in tree order per id, which is exactly what the full scan would return.
|
||||||
|
// In quirks mode `#id` matches ASCII-case-insensitively, but the id
|
||||||
|
// index is keyed on the exact-case id, so skip the fast path and let the
|
||||||
|
// selector engine (below) do the case-insensitive match.
|
||||||
|
if !self.is_quirks() {
|
||||||
if let Some(id) = simple_id_selector(selector) {
|
if let Some(id) = simple_id_selector(selector) {
|
||||||
match self.get_element_by_id(id) {
|
match self.get_element_by_id(id) {
|
||||||
// querySelector matches strict descendants of root only, so the
|
// querySelector matches strict descendants of root only, so the
|
||||||
@@ -592,13 +596,14 @@ impl DomTree {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let selector_list = parse_selector(selector)?;
|
let selector_list = parse_selector(selector)?;
|
||||||
let mut caches = selectors::context::SelectorCaches::default();
|
let mut caches = selectors::context::SelectorCaches::default();
|
||||||
let mut context = MatchingContext::new(
|
let mut context = MatchingContext::new(
|
||||||
MatchingMode::Normal,
|
MatchingMode::Normal,
|
||||||
None,
|
None,
|
||||||
&mut caches,
|
&mut caches,
|
||||||
QuirksMode::NoQuirks,
|
self.selector_quirks_mode(),
|
||||||
NeedsSelectorFlags::No,
|
NeedsSelectorFlags::No,
|
||||||
MatchingForInvalidation::No,
|
MatchingForInvalidation::No,
|
||||||
);
|
);
|
||||||
@@ -619,6 +624,16 @@ impl DomTree {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map the document's quirks flag onto the selector crate's QuirksMode. In
|
||||||
|
// quirks mode the crate matches class/id selectors ASCII-case-insensitively.
|
||||||
|
fn selector_quirks_mode(&self) -> QuirksMode {
|
||||||
|
if self.is_quirks() {
|
||||||
|
QuirksMode::Quirks
|
||||||
|
} else {
|
||||||
|
QuirksMode::NoQuirks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn query_selector_all_from(&self, root: NodeId, selector: &str) -> Result<Vec<NodeId>, String> {
|
pub fn query_selector_all_from(&self, root: NodeId, selector: &str) -> Result<Vec<NodeId>, String> {
|
||||||
let selector_list = parse_selector(selector)?;
|
let selector_list = parse_selector(selector)?;
|
||||||
let mut caches = selectors::context::SelectorCaches::default();
|
let mut caches = selectors::context::SelectorCaches::default();
|
||||||
@@ -626,7 +641,7 @@ impl DomTree {
|
|||||||
MatchingMode::Normal,
|
MatchingMode::Normal,
|
||||||
None,
|
None,
|
||||||
&mut caches,
|
&mut caches,
|
||||||
QuirksMode::NoQuirks,
|
self.selector_quirks_mode(),
|
||||||
NeedsSelectorFlags::No,
|
NeedsSelectorFlags::No,
|
||||||
MatchingForInvalidation::No,
|
MatchingForInvalidation::No,
|
||||||
);
|
);
|
||||||
@@ -729,6 +744,34 @@ mod tests {
|
|||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quirks_mode_matches_class_and_id_case_insensitively() {
|
||||||
|
// No doctype => quirks mode; class/id match ASCII case-insensitively.
|
||||||
|
let tree = parse_html(r#"<div class="Foo" id="Bar">x</div>"#);
|
||||||
|
assert!(
|
||||||
|
tree.query_selector(".foo").unwrap().is_some(),
|
||||||
|
".foo should match class=\"Foo\" in quirks mode"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tree.query_selector("#bar").unwrap().is_some(),
|
||||||
|
"#bar should match id=\"Bar\" in quirks mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standards_mode_matches_class_and_id_case_sensitively() {
|
||||||
|
// With a doctype => no-quirks; class/id remain case-sensitive.
|
||||||
|
let tree = parse_html(r#"<!DOCTYPE html><div class="Foo" id="Bar">x</div>"#);
|
||||||
|
assert!(
|
||||||
|
tree.query_selector(".foo").unwrap().is_none(),
|
||||||
|
".foo must NOT match class=\"Foo\" in standards mode"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tree.query_selector("#bar").unwrap().is_none(),
|
||||||
|
"#bar must NOT match id=\"Bar\" in standards mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_query_selector_complex() {
|
fn test_query_selector_complex() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ pub(crate) struct DomTreeInner {
|
|||||||
pub(crate) free_list: Vec<u32>,
|
pub(crate) free_list: Vec<u32>,
|
||||||
pub(crate) document: NodeId,
|
pub(crate) document: NodeId,
|
||||||
pub(crate) id_index: HashMap<String, NodeId>,
|
pub(crate) id_index: HashMap<String, NodeId>,
|
||||||
|
// Whether the document was parsed in (full) quirks mode. In quirks mode CSS
|
||||||
|
// class and id selectors match ASCII-case-insensitively.
|
||||||
|
pub(crate) quirks: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DomTree {
|
impl DomTree {
|
||||||
@@ -162,6 +165,7 @@ impl DomTree {
|
|||||||
free_list: Vec::new(),
|
free_list: Vec::new(),
|
||||||
document: NodeId(0),
|
document: NodeId(0),
|
||||||
id_index: HashMap::new(),
|
id_index: HashMap::new(),
|
||||||
|
quirks: false,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +174,17 @@ impl DomTree {
|
|||||||
self.inner.borrow().document
|
self.inner.borrow().document
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record whether the document was parsed in (full) quirks mode.
|
||||||
|
pub fn set_quirks(&self, quirks: bool) {
|
||||||
|
self.inner.borrow_mut().quirks = quirks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the document is in (full) quirks mode, in which CSS class and id
|
||||||
|
/// selectors match ASCII-case-insensitively.
|
||||||
|
pub fn is_quirks(&self) -> bool {
|
||||||
|
self.inner.borrow().quirks
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn borrow_inner(&self) -> std::cell::Ref<'_, DomTreeInner> {
|
pub(crate) fn borrow_inner(&self) -> std::cell::Ref<'_, DomTreeInner> {
|
||||||
self.inner.borrow()
|
self.inner.borrow()
|
||||||
}
|
}
|
||||||
@@ -469,13 +484,21 @@ impl DomTree {
|
|||||||
inner.id_index.remove(&id_str);
|
inner.id_index.remove(&id_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only free slots that are currently live. Freeing an out-of-range id
|
||||||
|
// would panic on direct indexing, and freeing an already-freed slot
|
||||||
|
// would push it onto the free list a second time — later handing the
|
||||||
|
// same NodeId to two live nodes (aliasing).
|
||||||
for desc_id in descendants {
|
for desc_id in descendants {
|
||||||
|
if matches!(inner.nodes.get(desc_id.index()), Some(Some(_))) {
|
||||||
inner.nodes[desc_id.index()] = None;
|
inner.nodes[desc_id.index()] = None;
|
||||||
inner.free_list.push(desc_id.0);
|
inner.free_list.push(desc_id.0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if matches!(inner.nodes.get(node_id.index()), Some(Some(_))) {
|
||||||
inner.nodes[node_id.index()] = None;
|
inner.nodes[node_id.index()] = None;
|
||||||
inner.free_list.push(node_id.0);
|
inner.free_list.push(node_id.0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn children(&self, node_id: NodeId) -> Vec<NodeId> {
|
pub fn children(&self, node_id: NodeId) -> Vec<NodeId> {
|
||||||
let inner = self.inner.borrow();
|
let inner = self.inner.borrow();
|
||||||
@@ -932,6 +955,30 @@ mod tests {
|
|||||||
assert!(node.is_document());
|
assert!(node.is_document());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_out_of_range_id_is_a_noop() {
|
||||||
|
let tree = DomTree::new();
|
||||||
|
// Direct indexing into `nodes` panicked out-of-bounds for an id past the
|
||||||
|
// end of the slot vector; it must be a no-op instead.
|
||||||
|
tree.remove(NodeId::new(9999));
|
||||||
|
assert_eq!(tree.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_twice_does_not_alias_slots() {
|
||||||
|
let tree = DomTree::new();
|
||||||
|
let doc = tree.document();
|
||||||
|
let a = tree.new_node(NodeData::Text { contents: "a".into() });
|
||||||
|
tree.append_child(doc, a);
|
||||||
|
tree.remove(a);
|
||||||
|
// Removing the already-freed node again must not push its slot onto the
|
||||||
|
// free list a second time, or two later allocations alias one slot.
|
||||||
|
tree.remove(a);
|
||||||
|
let x = tree.new_node(NodeData::Text { contents: "x".into() });
|
||||||
|
let y = tree.new_node(NodeData::Text { contents: "y".into() });
|
||||||
|
assert_ne!(x, y, "double-free aliased two live nodes onto the same slot");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_append_child() {
|
fn test_append_child() {
|
||||||
let tree = DomTree::new();
|
let tree = DomTree::new();
|
||||||
|
|||||||
@@ -218,7 +218,10 @@ impl TreeSink for DomTree {
|
|||||||
x == y
|
x == y
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_quirks_mode(&self, _mode: QuirksMode) {
|
fn set_quirks_mode(&self, mode: QuirksMode) {
|
||||||
|
// Only full quirks mode makes CSS class/id selectors case-insensitive;
|
||||||
|
// limited-quirks behaves like no-quirks for selector matching.
|
||||||
|
self.set_quirks(mode == QuirksMode::Quirks);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_mathml_annotation_xml_integration_point(&self, target: &NodeId) -> bool {
|
fn is_mathml_annotation_xml_integration_point(&self, target: &NodeId) -> bool {
|
||||||
|
|||||||
Vendored
+18
-16
@@ -4193,13 +4193,13 @@ if (typeof Response === 'undefined') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!Element.prototype.replaceWith) {
|
if (!Element.prototype.replaceWith) {
|
||||||
|
// _convertNodes turns any non-node argument (numbers, booleans, null, …) into
|
||||||
|
// a Text node via String(n), matching the spec and append()/prepend(); the
|
||||||
|
// old `typeof n === 'string'` check corrupted insert_before for other types.
|
||||||
Element.prototype.replaceWith = function(...nodes) {
|
Element.prototype.replaceWith = function(...nodes) {
|
||||||
const parent = this.parentNode;
|
const parent = this.parentNode;
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
for (const n of nodes) {
|
for (const n of _convertNodes(nodes)) parent.insertBefore(n, this);
|
||||||
if (typeof n === 'string') parent.insertBefore(document.createTextNode(n), this);
|
|
||||||
else parent.insertBefore(n, this);
|
|
||||||
}
|
|
||||||
parent.removeChild(this);
|
parent.removeChild(this);
|
||||||
};
|
};
|
||||||
_markNative(Element.prototype.replaceWith);
|
_markNative(Element.prototype.replaceWith);
|
||||||
@@ -4208,10 +4208,7 @@ if (!Element.prototype.before) {
|
|||||||
Element.prototype.before = function(...nodes) {
|
Element.prototype.before = function(...nodes) {
|
||||||
const parent = this.parentNode;
|
const parent = this.parentNode;
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
for (const n of nodes) {
|
for (const n of _convertNodes(nodes)) parent.insertBefore(n, this);
|
||||||
if (typeof n === 'string') parent.insertBefore(document.createTextNode(n), this);
|
|
||||||
else parent.insertBefore(n, this);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
_markNative(Element.prototype.before);
|
_markNative(Element.prototype.before);
|
||||||
}
|
}
|
||||||
@@ -4220,10 +4217,7 @@ if (!Element.prototype.after) {
|
|||||||
const parent = this.parentNode;
|
const parent = this.parentNode;
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
const ref = this.nextSibling;
|
const ref = this.nextSibling;
|
||||||
for (const n of nodes) {
|
for (const n of _convertNodes(nodes)) parent.insertBefore(n, ref);
|
||||||
if (typeof n === 'string') parent.insertBefore(document.createTextNode(n), ref);
|
|
||||||
else parent.insertBefore(n, ref);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
_markNative(Element.prototype.after);
|
_markNative(Element.prototype.after);
|
||||||
}
|
}
|
||||||
@@ -5465,11 +5459,16 @@ globalThis.XMLSerializer = class XMLSerializer {
|
|||||||
};
|
};
|
||||||
globalThis.performance = globalThis.performance || {
|
globalThis.performance = globalThis.performance || {
|
||||||
now: (function() {
|
now: (function() {
|
||||||
var _lastMs = -1, _sub = 0;
|
// Monotonically non-decreasing: return the wall-clock offset, but never a
|
||||||
|
// value below the last one. Equal readings are allowed, and avoiding a
|
||||||
|
// synthetic per-call increment keeps tight loops from advancing the clock
|
||||||
|
// faster than real elapsed time.
|
||||||
|
var _last = -Infinity;
|
||||||
return function() {
|
return function() {
|
||||||
var ms = Date.now() - (globalThis.performance.timeOrigin || 0);
|
var ms = Date.now() - (globalThis.performance.timeOrigin || 0);
|
||||||
if (ms !== _lastMs) { _lastMs = ms; _sub = 0; } else { _sub += 0.1; }
|
if (ms < _last) return _last;
|
||||||
return ms + _sub;
|
_last = ms;
|
||||||
|
return _last;
|
||||||
};
|
};
|
||||||
})(),
|
})(),
|
||||||
mark(){}, measure(){},
|
mark(){}, measure(){},
|
||||||
@@ -5718,7 +5717,10 @@ globalThis.atob = globalThis.atob || ((s) => { const c="ABCDEFGHIJKLMNOPQRSTUVWX
|
|||||||
const stack = [{state: null, url: undefined}]; // initial entry; url=undefined means "use document URL"
|
const stack = [{state: null, url: undefined}]; // initial entry; url=undefined means "use document URL"
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
const resolveOrFallback = (url) => {
|
const resolveOrFallback = (url) => {
|
||||||
if (url === null || url === undefined) return undefined;
|
// A missing url (pushState/replaceState called with < 3 args) keeps the
|
||||||
|
// current document URL per the HTML spec — capture it so the entry does not
|
||||||
|
// reset location back to the original document URL.
|
||||||
|
if (url === null || url === undefined) return __currentUrl();
|
||||||
try { return new URL(String(url), __currentUrl()).href; } catch (e) { return String(url); }
|
try { return new URL(String(url), __currentUrl()).href; } catch (e) { return String(url); }
|
||||||
};
|
};
|
||||||
const applyVirtual = () => {
|
const applyVirtual = () => {
|
||||||
|
|||||||
@@ -1346,6 +1346,75 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn performance_now_is_monotonic_under_bursty_calls() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
// Hammer performance.now() so many calls land in the same millisecond and
|
||||||
|
// the wall clock rolls over repeatedly; the value must never go backwards.
|
||||||
|
let violations = rt
|
||||||
|
.evaluate(
|
||||||
|
"(function(){var prev=-Infinity, bad=0; for(var i=0;i<500000;i++){var t=performance.now(); if(t<prev) bad++; prev=t;} return bad;})()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(violations.as_f64(), Some(0.0), "performance.now() went backwards");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn performance_now_does_not_outrun_elapsed_time() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
let lead = rt
|
||||||
|
.evaluate(
|
||||||
|
"(function(){for(var i=0;i<500000;i++)performance.now(); return performance.now()-(Date.now()-performance.timeOrigin);})()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
lead.as_f64().unwrap() <= 1.0,
|
||||||
|
"performance.now() advanced ahead of elapsed time: {lead}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn childnode_helpers_coerce_non_string_primitives_to_text() {
|
||||||
|
let mut rt = setup_runtime(r#"<html><body><div id="p"><span id="t">x</span></div></body></html>"#);
|
||||||
|
let before = rt
|
||||||
|
.evaluate("(function(){var t=document.getElementById('t'); t.before(5); return t.previousSibling ? t.previousSibling.textContent : 'NULL';})()")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(before, serde_json::json!("5"));
|
||||||
|
let after = rt
|
||||||
|
.evaluate("(function(){var t=document.getElementById('t'); t.after(true); return t.nextSibling ? t.nextSibling.textContent : 'NULL';})()")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(after, serde_json::json!("true"));
|
||||||
|
let replaced = rt
|
||||||
|
.evaluate("(function(){var t=document.getElementById('t'); t.replaceWith(42); return document.getElementById('p').textContent;})()")
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
replaced.as_str().unwrap().contains("42"),
|
||||||
|
"replaceWith(42) should leave text '42': {replaced}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replace_state_without_url_preserves_current_location() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
let path = rt
|
||||||
|
.evaluate(
|
||||||
|
"(function(){history.pushState({}, '', '/dashboard'); history.replaceState({scroll:1}); return location.pathname;})()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(path, serde_json::json!("/dashboard"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push_state_without_url_preserves_current_location() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
let path = rt
|
||||||
|
.evaluate(
|
||||||
|
"(function(){history.pushState({}, '', '/a'); history.pushState({b:1}); return location.pathname;})()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(path, serde_json::json!("/a"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_document_title() {
|
fn test_document_title() {
|
||||||
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");
|
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");
|
||||||
|
|||||||
Reference in new issue
Block a user