mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-31 13:19:05 +00:00
LibWeb: Don't crash if ElementByIdMap already has an element
Let's simply reinsert the element respecting it's new position in the DOM tree, instead of crashing. Fixes regression in WPT tests caused by introducion of cache for getElementById().
This commit is contained in:
parent
9eca5febd1
commit
634f0c2469
Notes:
github-actions[bot]
2025-03-27 14:45:52 +00:00
Author: https://github.com/kalenikaliaksandr
Commit: 634f0c2469
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4118
Reviewed-by: https://github.com/gmta ✅
3 changed files with 1278 additions and 1 deletions
|
@ -17,7 +17,9 @@ void ElementByIdMap::add(FlyString const& element_id, Element& element)
|
|||
return !element.has_value();
|
||||
});
|
||||
|
||||
VERIFY(!elements_with_id.contains_slow(element));
|
||||
elements_with_id.remove_first_matching([&](auto const& another_element) {
|
||||
return &element == another_element.ptr();
|
||||
});
|
||||
elements_with_id.insert_before_matching(element, [&](auto& another_element) {
|
||||
return element.is_before(*another_element);
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,158 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<script src="../../../html/resources/common.js"></script>
|
||||
<script src="../../../resources/testharness.js"></script>
|
||||
<script src="../../../resources/testharnessreport.js"></script>
|
||||
<script src="../../../resources/testdriver.js"></script>
|
||||
<script src="../../../resources/testdriver-vendor.js"></script>
|
||||
<script src="../../../resources/testdriver-actions.js"></script>
|
||||
<script src="../../../wai-aria/scripts/aria-utils.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="host-container"></div>
|
||||
<script>
|
||||
const Behavior = Object.freeze({
|
||||
ReflectsHost: 'ReflectsHost',
|
||||
ReflectsHostInArray: 'ReflectsHostInArray',
|
||||
IsNull: 'IsNull',
|
||||
ReflectsHostID: 'ReflectsHostID',
|
||||
ReflectsHostIDInDOMTokenList: 'ReflectsHostIDInDOMTokenList',
|
||||
});
|
||||
|
||||
function test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, attribute, reflected_property, expected_behavior) {
|
||||
// There's nothing to test if the referencing element type doesn't have the reflecting
|
||||
// property.
|
||||
if (!(reflected_property in document.createElement(referencing_element_type))) {
|
||||
return;
|
||||
}
|
||||
|
||||
test(function () {
|
||||
const referencing_element = document.createElement(referencing_element_type);
|
||||
document.body.appendChild(referencing_element);
|
||||
referencing_element.setAttribute(attribute, "host-id");
|
||||
const host_container = document.querySelector("#host-container");
|
||||
const host = element_creation_method(host_container, referenced_element_type);
|
||||
if (expected_behavior === Behavior.ReflectsHost) {
|
||||
assert_equals(referencing_element[reflected_property], host);
|
||||
} else if (expected_behavior === Behavior.ReflectsHostInArray) {
|
||||
assert_array_equals(referencing_element[reflected_property], [host]);
|
||||
} else if (expected_behavior === Behavior.IsNull) {
|
||||
assert_equals(referencing_element[reflected_property], null);
|
||||
} else if (expected_behavior === Behavior.ReflectsHostID) {
|
||||
assert_equals(referencing_element[reflected_property], "host-id");
|
||||
} else if (expected_behavior === Behavior.ReflectsHostIDInDOMTokenList) {
|
||||
assert_true(referencing_element[reflected_property] instanceof DOMTokenList);
|
||||
assert_array_equals(Array.from(referencing_element[reflected_property]), ["host-id"]);
|
||||
}
|
||||
referencing_element.remove();
|
||||
host_container.setHTMLUnsafe("");
|
||||
}, `${referencing_element_type}.${reflected_property} has reflection behavior ${expected_behavior} when pointing to ${referenced_element_type} with reference target${element_creation_method.method_name}`);
|
||||
}
|
||||
|
||||
function appendTestDeclaratively(host_container, referenced_element_type) {
|
||||
host_container.setHTMLUnsafe(`
|
||||
<div id="host-id">
|
||||
<template shadowrootmode="open" shadowrootreferencetarget="target">
|
||||
<${referenced_element_type} id="target"></${referenced_element_type}>
|
||||
</template>
|
||||
</div>`);
|
||||
const host = host_container.firstElementChild;
|
||||
return host;
|
||||
}
|
||||
function appendTestWithOptions(host_container, referenced_element_type) {
|
||||
host_container.setHTMLUnsafe('<div id="host-id"></div>');
|
||||
const host = host_container.firstElementChild;
|
||||
host.attachShadow({ mode: 'open', referenceTarget: 'target' });
|
||||
host.shadowRoot.innerHTML = `<${referenced_element_type} id="target"></${referenced_element_type}>`;
|
||||
return host;
|
||||
}
|
||||
const element_creation_methods = [
|
||||
appendTestDeclaratively,
|
||||
appendTestWithOptions,
|
||||
];
|
||||
element_creation_methods[0].method_name = '';
|
||||
element_creation_methods[1].method_name = ' via options';
|
||||
// We want to test types of elements that are associated with properties that can reflect other
|
||||
// elements and can therefore interact with reference target in interesting ways.
|
||||
// The HTML5_LABELABLE_ELEMENTS are defined in https://html.spec.whatwg.org/#category-label,
|
||||
// while non_labelable_element_types is a manually curated list of other elements with
|
||||
// reflecting properties (plus div as representative of more "normal" elements).
|
||||
// We'll test all permutations of these element types being both the referencing element
|
||||
// pointing into the reference target shadow host, and being the referenced element inside
|
||||
// the shadow.
|
||||
const non_labelable_element_types = ["div", "object", "label", "fieldset", "legend", "option", "datalist", "form"];
|
||||
const element_types = HTML5_LABELABLE_ELEMENTS.concat(non_labelable_element_types);
|
||||
|
||||
for(let element_creation_method of element_creation_methods) {
|
||||
for(let referencing_element_type of element_types) {
|
||||
for(let referenced_element_type of element_types) {
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-controls", "ariaControlsElements", Behavior.ReflectsHostInArray);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-activedescendant", "ariaActiveDescendantElement", Behavior.ReflectsHost);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-describedby", "ariaDescribedByElements", Behavior.ReflectsHostInArray);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-details", "ariaDetailsElements", Behavior.ReflectsHostInArray);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-errormessage", "ariaErrorMessageElements", Behavior.ReflectsHostInArray);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-flowto", "ariaFlowToElements", Behavior.ReflectsHostInArray);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-labelledby", "ariaLabelledByElements", Behavior.ReflectsHostInArray);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "aria-owns", "ariaOwnsElements", Behavior.ReflectsHostInArray);
|
||||
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "anchor", "anchorElement", Behavior.ReflectsHost);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "commandfor", "commandForElement", Behavior.ReflectsHost);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "popovertarget", "popoverTargetElement", Behavior.ReflectsHost);
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "interesttarget", "interestTargetElement", Behavior.ReflectsHost);
|
||||
|
||||
const expected_htmlFor_property_behavior = (referencing_element_type == "output") ? Behavior.ReflectsHostIDInDOMTokenList : Behavior.ReflectsHostID;
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "for", "htmlFor", expected_htmlFor_property_behavior);
|
||||
|
||||
// The form property of <label>, <legend>, and <option> reflects the form property of the associated labelable element,
|
||||
// the associated <fieldset>, and the associated <select>, respectively. Here since we don't have those associated elements,
|
||||
// the form property would return null.
|
||||
const expected_form_property_behavior = (referenced_element_type == 'form' &&
|
||||
referencing_element_type != "label" &&
|
||||
referencing_element_type != "legend" &&
|
||||
referencing_element_type != "option") ? Behavior.ReflectsHost : Behavior.IsNull;
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "form", "form", expected_form_property_behavior);
|
||||
|
||||
const expected_list_property_behavior = (referenced_element_type == 'datalist') ? Behavior.ReflectsHost : Behavior.IsNull;
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "list", "list", expected_list_property_behavior);
|
||||
|
||||
const expected_control_property_behavior = HTML5_LABELABLE_ELEMENTS.includes(referenced_element_type) ? Behavior.ReflectsHost : Behavior.IsNull;
|
||||
test_property_reflection(element_creation_method, referencing_element_type, referenced_element_type, "for", "control", expected_control_property_behavior);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the corresponding properties return null when the reference target has invalid ID.
|
||||
function appendTestDeclarativelyWithInvalidID(host_container, referenced_element_type) {
|
||||
host_container.setHTMLUnsafe(`
|
||||
<div id="host-id">
|
||||
<template shadowrootmode="open" shadowrootreferencetarget="invalid-id">
|
||||
<${referenced_element_type} id="target"></${referenced_element_type}>
|
||||
</template>
|
||||
</div>`);
|
||||
const host = host_container.firstElementChild;
|
||||
return host;
|
||||
}
|
||||
for(let referencing_element_type of element_types) {
|
||||
for(let referenced_element_type of element_types) {
|
||||
test_property_reflection(appendTestDeclarativelyWithInvalidID, referencing_element_type, referenced_element_type, "form", "form", Behavior.IsNull);
|
||||
test_property_reflection(appendTestDeclarativelyWithInvalidID, referencing_element_type, referenced_element_type, "list", "list", Behavior.IsNull);
|
||||
test_property_reflection(appendTestDeclarativelyWithInvalidID, referencing_element_type, referenced_element_type, "for", "control", Behavior.IsNull);
|
||||
}
|
||||
}
|
||||
|
||||
test(function () {
|
||||
const referencing_element = document.createElement('label');
|
||||
document.body.appendChild(referencing_element);
|
||||
referencing_element.setAttribute('for', "host-id");
|
||||
const host_container = document.querySelector("#host-container");
|
||||
const host = appendTestDeclaratively(host_container, 'input');
|
||||
const referenced_element = host.shadowRoot.getElementById('target');
|
||||
assert_array_equals(Array.from(referenced_element['labels']), [referencing_element]);
|
||||
referencing_element.remove();
|
||||
host_container.setHTMLUnsafe("");
|
||||
}, `The .labels property of the referenced input element should point to the referencing label element`);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue