mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 04:09:13 +00:00
LibWeb: Implement 'State-preserving atomic move integration'
This was recently added to both the HTML and DOM specifications, introducing the new moveBefore DOM API, as well as the new internal 'removing steps'. See: *432e8fb
*eaf2ac7
This commit is contained in:
parent
a47c4dbc63
commit
31a3bc3681
Notes:
github-actions[bot]
2025-04-26 14:46:43 +00:00
Author: https://github.com/shannonbooth
Commit: 31a3bc3681
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3855
Reviewed-by: https://github.com/ADKaster
Reviewed-by: https://github.com/kalenikaliaksandr
39 changed files with 1383 additions and 12 deletions
|
@ -8,6 +8,7 @@
|
||||||
#include <AK/AnyOf.h>
|
#include <AK/AnyOf.h>
|
||||||
#include <AK/Debug.h>
|
#include <AK/Debug.h>
|
||||||
#include <AK/StringBuilder.h>
|
#include <AK/StringBuilder.h>
|
||||||
|
#include <LibJS/Runtime/NativeFunction.h>
|
||||||
#include <LibUnicode/CharacterTypes.h>
|
#include <LibUnicode/CharacterTypes.h>
|
||||||
#include <LibUnicode/Locale.h>
|
#include <LibUnicode/Locale.h>
|
||||||
#include <LibWeb/Animations/Animation.h>
|
#include <LibWeb/Animations/Animation.h>
|
||||||
|
@ -1284,6 +1285,11 @@ void Element::removed_from(Node* old_parent, Node& old_root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Element::moved_from(GC::Ptr<Node> old_parent)
|
||||||
|
{
|
||||||
|
Base::moved_from(old_parent);
|
||||||
|
}
|
||||||
|
|
||||||
void Element::children_changed(ChildrenChangedMetadata const* metadata)
|
void Element::children_changed(ChildrenChangedMetadata const* metadata)
|
||||||
{
|
{
|
||||||
Node::children_changed(metadata);
|
Node::children_changed(metadata);
|
||||||
|
@ -2531,22 +2537,54 @@ void Element::enqueue_a_custom_element_upgrade_reaction(HTML::CustomElementDefin
|
||||||
enqueue_an_element_on_the_appropriate_element_queue();
|
enqueue_an_element_on_the_appropriate_element_queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-a-custom-element-callback-reaction
|
||||||
void Element::enqueue_a_custom_element_callback_reaction(FlyString const& callback_name, GC::RootVector<JS::Value> arguments)
|
void Element::enqueue_a_custom_element_callback_reaction(FlyString const& callback_name, GC::RootVector<JS::Value> arguments)
|
||||||
{
|
{
|
||||||
// 1. Let definition be element's custom element definition.
|
// 1. Let definition be element's custom element definition.
|
||||||
auto& definition = m_custom_element_definition;
|
auto& definition = m_custom_element_definition;
|
||||||
|
|
||||||
// 2. Let callback be the value of the entry in definition's lifecycle callbacks with key callbackName.
|
// 2. Let callback be the value of the entry in definition's lifecycle callbacks with key callbackName.
|
||||||
auto callback_iterator = definition->lifecycle_callbacks().find(callback_name);
|
GC::Ptr<Web::WebIDL::CallbackType> callback;
|
||||||
|
if (auto callback_iterator = definition->lifecycle_callbacks().find(callback_name); callback_iterator != definition->lifecycle_callbacks().end())
|
||||||
|
callback = callback_iterator->value;
|
||||||
|
|
||||||
|
// 3. If callbackName is "connectedMoveCallback" and callback is null:
|
||||||
|
if (callback_name == HTML::CustomElementReactionNames::connectedMoveCallback && !callback) {
|
||||||
|
// 1. Let disconnectedCallback be the value of the entry in definition's lifecycle callbacks with key "disconnectedCallback".
|
||||||
|
GC::Ptr<WebIDL::CallbackType> disconnected_callback;
|
||||||
|
if (auto it = definition->lifecycle_callbacks().find(HTML::CustomElementReactionNames::disconnectedCallback); it != definition->lifecycle_callbacks().end())
|
||||||
|
disconnected_callback = it->value;
|
||||||
|
|
||||||
|
// 2. Let connectedCallback be the value of the entry in definition's lifecycle callbacks with key "connectedCallback".
|
||||||
|
GC::Ptr<WebIDL::CallbackType> connected_callback;
|
||||||
|
if (auto it = definition->lifecycle_callbacks().find(HTML::CustomElementReactionNames::connectedCallback); it != definition->lifecycle_callbacks().end())
|
||||||
|
connected_callback = it->value;
|
||||||
|
|
||||||
|
// 3. If connectedCallback and disconnectedCallback are null, then return.
|
||||||
|
if (!connected_callback && !disconnected_callback)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 4. Set callback to the following steps:
|
||||||
|
auto steps = JS::NativeFunction::create(realm(), [this, disconnected_callback, connected_callback](JS::VM&) {
|
||||||
|
GC::RootVector<JS::Value> no_arguments { heap() };
|
||||||
|
|
||||||
|
// 1. If disconnectedCallback is not null, then call disconnectedCallback with no arguments.
|
||||||
|
if (disconnected_callback)
|
||||||
|
(void)WebIDL::invoke_callback(*disconnected_callback, this, WebIDL::ExceptionBehavior::Report, no_arguments);
|
||||||
|
|
||||||
|
// 2. If connectedCallback is not null, then call connectedCallback with no arguments.
|
||||||
|
if (connected_callback)
|
||||||
|
(void)WebIDL::invoke_callback(*connected_callback, this, WebIDL::ExceptionBehavior::Report, no_arguments);
|
||||||
|
|
||||||
|
return JS::js_undefined(); }, 0, FlyString {}, &realm());
|
||||||
|
callback = realm().heap().allocate<WebIDL::CallbackType>(steps, realm());
|
||||||
|
}
|
||||||
|
|
||||||
// 3. If callback is null, then return.
|
// 3. If callback is null, then return.
|
||||||
if (callback_iterator == definition->lifecycle_callbacks().end())
|
if (!callback)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!callback_iterator->value)
|
// 5. If callbackName is "attributeChangedCallback":
|
||||||
return;
|
|
||||||
|
|
||||||
// 4. If callbackName is "attributeChangedCallback", then:
|
|
||||||
if (callback_name == HTML::CustomElementReactionNames::attributeChangedCallback) {
|
if (callback_name == HTML::CustomElementReactionNames::attributeChangedCallback) {
|
||||||
// 1. Let attributeName be the first element of args.
|
// 1. Let attributeName be the first element of args.
|
||||||
VERIFY(!arguments.is_empty());
|
VERIFY(!arguments.is_empty());
|
||||||
|
@ -2559,10 +2597,10 @@ void Element::enqueue_a_custom_element_callback_reaction(FlyString const& callba
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Add a new callback reaction to element's custom element reaction queue, with callback function callback and arguments args.
|
// 6. Add a new callback reaction to element's custom element reaction queue, with callback function callback and arguments args.
|
||||||
ensure_custom_element_reaction_queue().append(CustomElementCallbackReaction { .callback = callback_iterator->value, .arguments = move(arguments) });
|
ensure_custom_element_reaction_queue().append(CustomElementCallbackReaction { .callback = callback, .arguments = move(arguments) });
|
||||||
|
|
||||||
// 6. Enqueue an element on the appropriate element queue given element.
|
// 7. Enqueue an element on the appropriate element queue given element.
|
||||||
enqueue_an_element_on_the_appropriate_element_queue();
|
enqueue_an_element_on_the_appropriate_element_queue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -483,6 +483,8 @@ protected:
|
||||||
|
|
||||||
virtual void inserted() override;
|
virtual void inserted() override;
|
||||||
virtual void removed_from(Node* old_parent, Node& old_root) override;
|
virtual void removed_from(Node* old_parent, Node& old_root) override;
|
||||||
|
virtual void moved_from(GC::Ptr<Node> old_parent) override;
|
||||||
|
|
||||||
virtual void children_changed(ChildrenChangedMetadata const*) override;
|
virtual void children_changed(ChildrenChangedMetadata const*) override;
|
||||||
virtual i32 default_tab_index_value() const;
|
virtual i32 default_tab_index_value() const;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
|
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
|
||||||
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
|
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
|
||||||
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
||||||
|
* Copyright (c) 2025, Shannon Booth <shannon@serenityos.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -1175,6 +1176,190 @@ WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_node(Document* document, bool sub
|
||||||
return GC::Ref { *copy };
|
return GC::Ref { *copy };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://dom.spec.whatwg.org/#move
|
||||||
|
WebIDL::ExceptionOr<void> Node::move_node(Node& new_parent, Node* child)
|
||||||
|
{
|
||||||
|
// 1. If newParent’s shadow-including root is not the same as node’s shadow-including root, then throw a "HierarchyRequestError" DOMException.
|
||||||
|
if (&new_parent.shadow_including_root() != &shadow_including_root())
|
||||||
|
return WebIDL::HierarchyRequestError::create(realm(), "New parent is not in the same shadow tree"_string);
|
||||||
|
|
||||||
|
// NOTE: This has the side effect of ensuring that a move is only performed if newParent’s connected is node’s connected.
|
||||||
|
|
||||||
|
// 2. If node is a host-including inclusive ancestor of newParent, then throw a "HierarchyRequestError" DOMException.
|
||||||
|
if (is_host_including_inclusive_ancestor_of(new_parent))
|
||||||
|
return WebIDL::HierarchyRequestError::create(realm(), "New parent is an ancestor of this node"_string);
|
||||||
|
|
||||||
|
// 3. If child is non-null and its parent is not newParent, then throw a "NotFoundError" DOMException.
|
||||||
|
if (child && child->parent() != &new_parent)
|
||||||
|
return WebIDL::NotFoundError::create(realm(), "Child does not belong to the new parent"_string);
|
||||||
|
|
||||||
|
// 4. If node is not an Element or a CharacterData node, then throw a "HierarchyRequestError" DOMException.
|
||||||
|
if (!is<Element>(*this) && !is<CharacterData>(*this))
|
||||||
|
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
|
||||||
|
|
||||||
|
// 5. If node is a Text node and newParent is a document, then throw a "HierarchyRequestError" DOMException.
|
||||||
|
if (is<Text>(*this) && is<Document>(new_parent))
|
||||||
|
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
|
||||||
|
|
||||||
|
// 6. If newParent is a document, node is an Element node, and either newParent has an element child, child is a doctype,
|
||||||
|
// or child is non-null and a doctype is following child then throw a "HierarchyRequestError" DOMException.
|
||||||
|
if (is<Document>(new_parent) && is<Element>(*this)) {
|
||||||
|
if (new_parent.has_child_of_type<Element>() || is<DocumentType>(child) || (child && child->has_following_node_of_type_in_tree_order<DocumentType>()))
|
||||||
|
return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion"_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Let oldParent be node’s parent.
|
||||||
|
auto* old_parent = this->parent();
|
||||||
|
|
||||||
|
// 8. Assert: oldParent is non-null.
|
||||||
|
VERIFY(old_parent);
|
||||||
|
|
||||||
|
// 9. Run the live range pre-remove steps, given node.
|
||||||
|
live_range_pre_remove();
|
||||||
|
|
||||||
|
// 10. For each NodeIterator object iterator whose root’s node document is node’s node document, run the NodeIterator pre-remove steps given node and iterator.
|
||||||
|
document().for_each_node_iterator([&](NodeIterator& node_iterator) {
|
||||||
|
node_iterator.run_pre_removing_steps(*this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 11. Let oldPreviousSibling be node’s previous sibling.
|
||||||
|
auto* old_previous_sibling = previous_sibling();
|
||||||
|
|
||||||
|
// 12. Let oldNextSibling be node’s next sibling.
|
||||||
|
auto* old_next_sibling = next_sibling();
|
||||||
|
|
||||||
|
if (old_parent->is_connected()) {
|
||||||
|
// Since the tree structure is about to change, we need to invalidate both style and layout.
|
||||||
|
// In the future, we should find a way to only invalidate the parts that actually need it.
|
||||||
|
old_parent->invalidate_style(StyleInvalidationReason::NodeRemove);
|
||||||
|
|
||||||
|
// NOTE: If we didn't have a layout node before, rebuilding the layout tree isn't gonna give us one
|
||||||
|
// after we've been removed from the DOM.
|
||||||
|
if (layout_node())
|
||||||
|
old_parent->set_needs_layout_tree_update(true, SetNeedsLayoutTreeUpdateReason::NodeRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 13. Remove node from oldParent’s children.
|
||||||
|
old_parent->remove_child_impl(*this);
|
||||||
|
|
||||||
|
// 14. If node is assigned, then run assign slottables for node’s assigned slot.
|
||||||
|
if (auto assigned_slot = assigned_slot_for_node(*this))
|
||||||
|
assign_slottables(*assigned_slot);
|
||||||
|
|
||||||
|
// 15. If oldParent’s root is a shadow root, and oldParent is a slot whose assigned nodes is empty, then run signal a slot change for oldParent.
|
||||||
|
auto& old_parent_root = old_parent->root();
|
||||||
|
if (old_parent_root.is_shadow_root() && is<HTML::HTMLSlotElement>(*old_parent)) {
|
||||||
|
auto& old_parent_slot = static_cast<HTML::HTMLSlotElement&>(*old_parent);
|
||||||
|
if (old_parent_slot.assigned_nodes_internal().is_empty())
|
||||||
|
signal_a_slot_change(old_parent_slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16. If node has an inclusive descendant that is a slot:
|
||||||
|
auto has_descendent_slot = false;
|
||||||
|
|
||||||
|
for_each_in_inclusive_subtree_of_type<HTML::HTMLSlotElement>([&](auto const&) {
|
||||||
|
has_descendent_slot = true;
|
||||||
|
return TraversalDecision::Break;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (has_descendent_slot) {
|
||||||
|
// 1. Run assign slottables for a tree with oldParent’s root.
|
||||||
|
assign_slottables_for_a_tree(old_parent_root);
|
||||||
|
|
||||||
|
// 2. Run assign slottables for a tree with node.
|
||||||
|
assign_slottables_for_a_tree(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 17. If child is non-null:
|
||||||
|
if (child) {
|
||||||
|
// 1. For each live range whose start node is newParent and start offset is greater than child’s index, increase its start offset by 1.
|
||||||
|
for (auto& range : Range::live_ranges()) {
|
||||||
|
if (range->start_container() == &new_parent && range->start_offset() > child->index())
|
||||||
|
range->increase_start_offset({}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. For each live range whose end node is newParent and end offset is greater than child’s index, increase its end offset by 1.
|
||||||
|
for (auto& range : Range::live_ranges()) {
|
||||||
|
if (range->end_container() == &new_parent && range->end_offset() > child->index())
|
||||||
|
range->increase_end_offset({}, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 18. Let newPreviousSibling be child’s previous sibling if child is non-null, and newParent’s last child otherwise.
|
||||||
|
auto* new_previous_sibling = child ? child->previous_sibling() : new_parent.last_child();
|
||||||
|
|
||||||
|
// 19. If child is null, then append node to newParent’s children.
|
||||||
|
if (!child) {
|
||||||
|
new_parent.append_child_impl(*this);
|
||||||
|
}
|
||||||
|
// 20. Otherwise, insert node into newParent’s children before child’s index.
|
||||||
|
else {
|
||||||
|
new_parent.insert_before_impl(*this, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_parent.invalidate_style(StyleInvalidationReason::NodeInsertBefore);
|
||||||
|
if (is_connected()) {
|
||||||
|
new_parent.set_needs_layout_tree_update(true, SetNeedsLayoutTreeUpdateReason::NodeInsertBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 21. If newParent is a shadow host whose shadow root’s slot assignment is "named" and node is a slottable, then assign a slot for node.
|
||||||
|
if (is<Element>(new_parent) && is<Element>(*this)) {
|
||||||
|
auto& this_element = static_cast<Element&>(*this);
|
||||||
|
auto& new_parent_element = static_cast<Element&>(new_parent);
|
||||||
|
|
||||||
|
auto is_named_shadow_host = new_parent_element.is_shadow_host()
|
||||||
|
&& new_parent_element.shadow_root()->slot_assignment() == Bindings::SlotAssignmentMode::Named;
|
||||||
|
|
||||||
|
if (is_named_shadow_host && this_element.is_slottable())
|
||||||
|
assign_a_slot(this_element.as_slottable());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 22. If newParent’s root is a shadow root, and newParent is a slot whose assigned nodes is empty, then run signal a slot change for newParent.
|
||||||
|
if (new_parent.root().is_shadow_root() && is<HTML::HTMLSlotElement>(new_parent)) {
|
||||||
|
auto& new_parent_slot = static_cast<HTML::HTMLSlotElement&>(new_parent);
|
||||||
|
if (new_parent_slot.assigned_nodes_internal().is_empty())
|
||||||
|
signal_a_slot_change(new_parent_slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 23. Run assign slottables for a tree with node’s root.
|
||||||
|
assign_slottables_for_a_tree(root());
|
||||||
|
|
||||||
|
// 24. For each shadow-including inclusive descendant inclusiveDescendant of node, in shadow-including tree order:
|
||||||
|
for_each_shadow_including_inclusive_descendant([this, &new_parent, old_parent](Node& inclusive_descendant) {
|
||||||
|
// 1. If inclusiveDescendant is node, then run the moving steps with inclusiveDescendant and oldParent. Otherwise, run the moving
|
||||||
|
// steps with inclusiveDescendant and null.
|
||||||
|
if (&inclusive_descendant == this)
|
||||||
|
inclusive_descendant.moved_from(*old_parent);
|
||||||
|
else
|
||||||
|
inclusive_descendant.moved_from(nullptr);
|
||||||
|
|
||||||
|
// NOTE: Because the move algorithm is a separate primitive from insert and remove, it does not invoke the traditional insertion steps or
|
||||||
|
// removing steps for inclusiveDescendant.
|
||||||
|
|
||||||
|
// 2. If inclusiveDescendant is custom and newParent is connected, then enqueue a custom element callback reaction with inclusiveDescendant,
|
||||||
|
// callback name "connectedMoveCallback", and « ».
|
||||||
|
if (is<DOM::Element>(inclusive_descendant)) {
|
||||||
|
auto& element = static_cast<DOM::Element&>(inclusive_descendant);
|
||||||
|
|
||||||
|
if (element.is_custom() && new_parent.is_connected()) {
|
||||||
|
GC::RootVector<JS::Value> empty_arguments { vm().heap() };
|
||||||
|
element.enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::connectedMoveCallback, move(empty_arguments));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TraversalDecision::Continue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 25. Queue a tree mutation record for oldParent with « », « node », oldPreviousSibling, and oldNextSibling.
|
||||||
|
old_parent->queue_tree_mutation_record({}, { *this }, old_previous_sibling, old_next_sibling);
|
||||||
|
|
||||||
|
// 26. Queue a tree mutation record for newParent with « node », « », newPreviousSibling, and child.
|
||||||
|
new_parent.queue_tree_mutation_record({ *this }, {}, new_previous_sibling, child);
|
||||||
|
|
||||||
|
document().bump_dom_tree_version();
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#clone-a-single-node
|
// https://dom.spec.whatwg.org/#clone-a-single-node
|
||||||
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_single_node(Document& document) const
|
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_single_node(Document& document) const
|
||||||
{
|
{
|
||||||
|
@ -1488,6 +1673,11 @@ void Node::removed_from(Node*, Node&)
|
||||||
play_or_cancel_animations_after_display_property_change();
|
play_or_cancel_animations_after_display_property_change();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://dom.spec.whatwg.org/#concept-node-move-ext
|
||||||
|
void Node::moved_from(GC::Ptr<Node>)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
ParentNode* Node::parent_or_shadow_host()
|
ParentNode* Node::parent_or_shadow_host()
|
||||||
{
|
{
|
||||||
if (is<ShadowRoot>(*this))
|
if (is<ShadowRoot>(*this))
|
||||||
|
|
|
@ -240,6 +240,8 @@ public:
|
||||||
WebIDL::ExceptionOr<GC::Ref<Node>> clone_single_node(Document&) const;
|
WebIDL::ExceptionOr<GC::Ref<Node>> clone_single_node(Document&) const;
|
||||||
WebIDL::ExceptionOr<GC::Ref<Node>> clone_node_binding(bool subtree);
|
WebIDL::ExceptionOr<GC::Ref<Node>> clone_node_binding(bool subtree);
|
||||||
|
|
||||||
|
WebIDL::ExceptionOr<void> move_node(Node& new_parent, Node* child);
|
||||||
|
|
||||||
// NOTE: This is intended for the JS bindings.
|
// NOTE: This is intended for the JS bindings.
|
||||||
bool has_child_nodes() const { return has_children(); }
|
bool has_child_nodes() const { return has_children(); }
|
||||||
GC::Ref<NodeList> child_nodes();
|
GC::Ref<NodeList> child_nodes();
|
||||||
|
@ -298,6 +300,8 @@ public:
|
||||||
virtual void inserted();
|
virtual void inserted();
|
||||||
virtual void post_connection();
|
virtual void post_connection();
|
||||||
virtual void removed_from(Node* old_parent, Node& old_root);
|
virtual void removed_from(Node* old_parent, Node& old_root);
|
||||||
|
virtual void moved_from(GC::Ptr<Node> old_parent);
|
||||||
|
|
||||||
struct ChildrenChangedMetadata {
|
struct ChildrenChangedMetadata {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
Inserted,
|
Inserted,
|
||||||
|
|
|
@ -244,6 +244,22 @@ WebIDL::ExceptionOr<void> ParentNode::replace_children(Vector<Variant<GC::Root<N
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://dom.spec.whatwg.org/#dom-parentnode-movebefore
|
||||||
|
WebIDL::ExceptionOr<void> ParentNode::move_before(GC::Ref<Node> node, GC::Ptr<Node> child)
|
||||||
|
{
|
||||||
|
// 1. Let referenceChild be child.
|
||||||
|
auto reference_child = child;
|
||||||
|
|
||||||
|
// 2. If referenceChild is node, then set referenceChild to node’s next sibling.
|
||||||
|
if (reference_child == node)
|
||||||
|
reference_child = node->next_sibling();
|
||||||
|
|
||||||
|
// 3. Move node into this before referenceChild.
|
||||||
|
TRY(node->move_node(*this, reference_child));
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname
|
// https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname
|
||||||
GC::Ref<HTMLCollection> ParentNode::get_elements_by_class_name(StringView class_names)
|
GC::Ref<HTMLCollection> ParentNode::get_elements_by_class_name(StringView class_names)
|
||||||
{
|
{
|
||||||
|
|
|
@ -35,6 +35,7 @@ public:
|
||||||
WebIDL::ExceptionOr<void> prepend(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
WebIDL::ExceptionOr<void> prepend(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
||||||
WebIDL::ExceptionOr<void> append(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
WebIDL::ExceptionOr<void> append(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
||||||
WebIDL::ExceptionOr<void> replace_children(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
WebIDL::ExceptionOr<void> replace_children(Vector<Variant<GC::Root<Node>, String>> const& nodes);
|
||||||
|
WebIDL::ExceptionOr<void> move_before(GC::Ref<Node> node, GC::Ptr<Node> child);
|
||||||
|
|
||||||
GC::Ref<HTMLCollection> get_elements_by_class_name(StringView);
|
GC::Ref<HTMLCollection> get_elements_by_class_name(StringView);
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ interface mixin ParentNode {
|
||||||
[CEReactions, Unscopable] undefined append((Node or DOMString)... nodes);
|
[CEReactions, Unscopable] undefined append((Node or DOMString)... nodes);
|
||||||
[CEReactions, Unscopable] undefined replaceChildren((Node or DOMString)... nodes);
|
[CEReactions, Unscopable] undefined replaceChildren((Node or DOMString)... nodes);
|
||||||
|
|
||||||
|
[CEReactions] undefined moveBefore(Node node, Node? child);
|
||||||
|
|
||||||
Element? querySelector(DOMString selectors);
|
Element? querySelector(DOMString selectors);
|
||||||
[NewObject] NodeList querySelectorAll(DOMString selectors);
|
[NewObject] NodeList querySelectorAll(DOMString selectors);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace Web::HTML::CustomElementReactionNames {
|
||||||
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(connectedCallback) \
|
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(connectedCallback) \
|
||||||
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(disconnectedCallback) \
|
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(disconnectedCallback) \
|
||||||
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(adoptedCallback) \
|
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(adoptedCallback) \
|
||||||
|
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(connectedMoveCallback) \
|
||||||
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(attributeChangedCallback) \
|
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(attributeChangedCallback) \
|
||||||
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formAssociatedCallback) \
|
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formAssociatedCallback) \
|
||||||
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formDisabledCallback) \
|
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formDisabledCallback) \
|
||||||
|
|
|
@ -199,14 +199,15 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
|
||||||
auto& prototype = prototype_value.as_object();
|
auto& prototype = prototype_value.as_object();
|
||||||
|
|
||||||
// 3. Let lifecycleCallbacks be the ordered map «[ "connectedCallback" → null, "disconnectedCallback" → null, "adoptedCallback" → null,
|
// 3. Let lifecycleCallbacks be the ordered map «[ "connectedCallback" → null, "disconnectedCallback" → null, "adoptedCallback" → null,
|
||||||
// "attributeChangedCallback" → null ]».
|
// "connectedMoveCallback" → null, "attributeChangedCallback" → null ]».
|
||||||
lifecycle_callbacks.set(CustomElementReactionNames::connectedCallback, {});
|
lifecycle_callbacks.set(CustomElementReactionNames::connectedCallback, {});
|
||||||
lifecycle_callbacks.set(CustomElementReactionNames::disconnectedCallback, {});
|
lifecycle_callbacks.set(CustomElementReactionNames::disconnectedCallback, {});
|
||||||
lifecycle_callbacks.set(CustomElementReactionNames::adoptedCallback, {});
|
lifecycle_callbacks.set(CustomElementReactionNames::adoptedCallback, {});
|
||||||
|
lifecycle_callbacks.set(CustomElementReactionNames::connectedMoveCallback, {});
|
||||||
lifecycle_callbacks.set(CustomElementReactionNames::attributeChangedCallback, {});
|
lifecycle_callbacks.set(CustomElementReactionNames::attributeChangedCallback, {});
|
||||||
|
|
||||||
// 4. For each callbackName of the keys of lifecycleCallbacks:
|
// 4. For each callbackName of the keys of lifecycleCallbacks:
|
||||||
for (auto const& callback_name : { CustomElementReactionNames::connectedCallback, CustomElementReactionNames::disconnectedCallback, CustomElementReactionNames::adoptedCallback, CustomElementReactionNames::attributeChangedCallback }) {
|
for (auto const& callback_name : { CustomElementReactionNames::connectedCallback, CustomElementReactionNames::disconnectedCallback, CustomElementReactionNames::adoptedCallback, CustomElementReactionNames::connectedMoveCallback, CustomElementReactionNames::attributeChangedCallback }) {
|
||||||
// 1. Let callbackValue be ? Get(prototype, callbackName).
|
// 1. Let callbackValue be ? Get(prototype, callbackName).
|
||||||
auto callback_value = TRY(prototype.get(callback_name));
|
auto callback_value = TRY(prototype.get(callback_name));
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,14 @@ void FormAssociatedElement::form_node_was_removed()
|
||||||
reset_form_owner();
|
reset_form_owner();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:attr-fae-form-2
|
||||||
|
void FormAssociatedElement::form_node_was_moved()
|
||||||
|
{
|
||||||
|
// When a listed form-associated element's form attribute is set, changed, or removed, then the user agent must reset the form owner of that element.
|
||||||
|
if (m_form && &form_associated_element_to_html_element().root() != &m_form->root())
|
||||||
|
reset_form_owner();
|
||||||
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-3
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms:category-listed-3
|
||||||
void FormAssociatedElement::form_node_attribute_changed(FlyString const& name, Optional<String> const& value)
|
void FormAssociatedElement::form_node_attribute_changed(FlyString const& name, Optional<String> const& value)
|
||||||
{
|
{
|
||||||
|
|
|
@ -45,6 +45,13 @@ private:
|
||||||
form_associated_element_was_removed(old_parent); \
|
form_associated_element_was_removed(old_parent); \
|
||||||
} \
|
} \
|
||||||
\
|
\
|
||||||
|
virtual void moved_from(GC::Ptr<DOM::Node> old_parent) override \
|
||||||
|
{ \
|
||||||
|
ElementBaseClass::moved_from(old_parent); \
|
||||||
|
form_node_was_moved(); \
|
||||||
|
form_associated_element_was_moved(old_parent); \
|
||||||
|
} \
|
||||||
|
\
|
||||||
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override \
|
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override \
|
||||||
{ \
|
{ \
|
||||||
ElementBaseClass::attribute_changed(name, old_value, value, namespace_); \
|
ElementBaseClass::attribute_changed(name, old_value, value, namespace_); \
|
||||||
|
@ -137,10 +144,12 @@ protected:
|
||||||
|
|
||||||
virtual void form_associated_element_was_inserted() { }
|
virtual void form_associated_element_was_inserted() { }
|
||||||
virtual void form_associated_element_was_removed(DOM::Node*) { }
|
virtual void form_associated_element_was_removed(DOM::Node*) { }
|
||||||
|
virtual void form_associated_element_was_moved(GC::Ptr<DOM::Node>) { }
|
||||||
virtual void form_associated_element_attribute_changed(FlyString const&, Optional<String> const&, Optional<FlyString> const&) { }
|
virtual void form_associated_element_attribute_changed(FlyString const&, Optional<String> const&, Optional<FlyString> const&) { }
|
||||||
|
|
||||||
void form_node_was_inserted();
|
void form_node_was_inserted();
|
||||||
void form_node_was_removed();
|
void form_node_was_removed();
|
||||||
|
void form_node_was_moved();
|
||||||
void form_node_attribute_changed(FlyString const&, Optional<String> const&);
|
void form_node_attribute_changed(FlyString const&, Optional<String> const&);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -48,7 +48,16 @@ void HTMLSourceElement::inserted()
|
||||||
// count this as a relevant mutation for child.
|
// count this as a relevant mutation for child.
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element:html-element-removing-steps
|
// https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element:the-source-element-17
|
||||||
|
void HTMLSourceElement::moved_from(GC::Ptr<DOM::Node> old_parent)
|
||||||
|
{
|
||||||
|
Base::moved_from(old_parent);
|
||||||
|
|
||||||
|
// FIXME: 1. If oldParent is a picture element, then for each child of oldParent's children, if child is an img
|
||||||
|
// element, then count this as a relevant mutation for child.
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element:the-source-element-18
|
||||||
void HTMLSourceElement::removed_from(DOM::Node* old_parent, DOM::Node& old_root)
|
void HTMLSourceElement::removed_from(DOM::Node* old_parent, DOM::Node& old_root)
|
||||||
{
|
{
|
||||||
// The source HTML element removing steps, given removedNode and oldParent, are:
|
// The source HTML element removing steps, given removedNode and oldParent, are:
|
||||||
|
|
|
@ -24,6 +24,7 @@ private:
|
||||||
|
|
||||||
virtual void inserted() override;
|
virtual void inserted() override;
|
||||||
virtual void removed_from(DOM::Node* old_parent, DOM::Node& old_root) override;
|
virtual void removed_from(DOM::Node* old_parent, DOM::Node& old_root) override;
|
||||||
|
virtual void moved_from(GC::Ptr<Node> old_parent) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<link rel="help" href="https://crbug.com/338071841">
|
||||||
|
<div id="p"><span></span><!-- comment --></div>
|
||||||
|
<script>
|
||||||
|
p.moveBefore(p.lastChild, p.firstChild);
|
||||||
|
</script>
|
|
@ -0,0 +1,38 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 32 tests
|
||||||
|
|
||||||
|
31 Pass
|
||||||
|
1 Fail
|
||||||
|
Pass If node is a host-including inclusive ancestor of parent, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is a Text node and parent is a document, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is a doctype and parent is not a document, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is a DocumentFragment with multiple elements and parent is a document, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is a DocumentFragment with an element and parent is a document with another element, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is an Element and parent is a document with another element, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is a doctype and parent is a document with another doctype, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass If node is a doctype and parent is a document with an element, then throw a HierarchyRequestError DOMException.
|
||||||
|
Pass Calling moveBefore with a non-Node first argument must throw TypeError.
|
||||||
|
Pass Calling moveBefore with second argument missing, or other than Node, null, or undefined, must throw TypeError.
|
||||||
|
Pass moveBefore() method does not exist on non-ParentNode Nodes
|
||||||
|
Pass moveBefore() on disconnected parent throws a HierarchyRequestError
|
||||||
|
Pass moveBefore() with disconnected target node throws a HierarchyRequestError
|
||||||
|
Fail moveBefore() on a cross-document target node throws a HierarchyRequestError
|
||||||
|
Pass moveBefore() into a Document throws a HierarchyRequestError
|
||||||
|
Pass moveBefore() CharacterData into a Document
|
||||||
|
Pass moveBefore() with node being an inclusive ancestor of parent throws a HierarchyRequestError
|
||||||
|
Pass moveBefore() with a non-{Element, CharacterData} throws a HierarchyRequestError
|
||||||
|
Pass moveBefore with an Element or CharacterData succeeds
|
||||||
|
Pass moveBefore on a paragraph's Text node child
|
||||||
|
Pass moveBefore with reference child whose parent is NOT the destination parent (context node) throws a NotFoundError.
|
||||||
|
Pass moveBefore() returns undefined
|
||||||
|
Pass Moving a node before itself should not move the node
|
||||||
|
Pass Moving a node from a disconnected container to a disconnected new parent without a shared ancestor throws a HIERARCHY_REQUEST_ERR
|
||||||
|
Pass Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds
|
||||||
|
Pass Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds,also across shadow-roots
|
||||||
|
Pass Moving a node from disconnected->connected throws a HIERARCHY_REQUEST_ERR
|
||||||
|
Pass Moving a node from connected->disconnected throws a HIERARCHY_REQUEST_ERR
|
||||||
|
Pass No custom element callbacks are run during disconnected moveBefore()
|
||||||
|
Pass Invalid node hierarchy with null old parent does not crash
|
||||||
|
Pass Move disconnected iframe does not crash
|
|
@ -0,0 +1,11 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 6 tests
|
||||||
|
|
||||||
|
6 Pass
|
||||||
|
Pass the disconnected/connected callbacks should be called when no other callback is defined
|
||||||
|
Pass the element should stay connected during the callbacks
|
||||||
|
Pass When connectedMoveCallback is defined, it is called instead of disconnectedCallback/connectedCallback
|
||||||
|
Pass Reactions to atomic move are called in order of element, not in order of operation
|
||||||
|
Pass When connectedCallback is not defined, no crash
|
||||||
|
Pass When disconnectedCallback is not defined, no crash
|
|
@ -0,0 +1,11 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 5 tests
|
||||||
|
|
||||||
|
3 Pass
|
||||||
|
2 Fail
|
||||||
|
Pass focus-within should be updated when reparenting focused element directly
|
||||||
|
Pass focus-within should be updated when reparenting an element that has focus within
|
||||||
|
Pass focus-within should remain the same when moving to the same parent
|
||||||
|
Fail :focus-within should be eventually up to date when moving to an inert subtree
|
||||||
|
Fail :focus-within should be eventually up to date when moving to a subtree that would become inert via style
|
|
@ -0,0 +1,8 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 3 tests
|
||||||
|
|
||||||
|
3 Pass
|
||||||
|
Pass moveBefore still results in range startContainer snapping up to parent when startContainer is moved
|
||||||
|
Pass moveBefore still causes range startContainer to snap up to parent, when startContainer ancestor is moved
|
||||||
|
Pass moveBefore still causes range endContainer to snap up to parent, when endContainer ancestor is moved
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Pass
|
||||||
|
Pass moveBefore-shadow-inside
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Pass
|
||||||
|
Pass moveBefore() is allowed in ShadowRoots (i.e., connected DocumentFragments)
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Pass
|
||||||
|
Pass MutationEvents (if supported by the UA) are suppressed during `moveBefore()`
|
|
@ -0,0 +1,7 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 2 tests
|
||||||
|
|
||||||
|
2 Pass
|
||||||
|
Pass [Connected move] MutationObserver removal + insertion is tracked by moveBefore()
|
||||||
|
Pass [Disconnected move] MutationObserver removal + insertion is tracked by moveBefore()
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Fail
|
||||||
|
Fail Element nonce content attribute is not cleared after move
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Pass
|
||||||
|
Pass Moving an object element does not crash
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Pass
|
||||||
|
Pass when reparenting an open popover, it shouldn't be closed automatically
|
|
@ -0,0 +1,6 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 1 tests
|
||||||
|
|
||||||
|
1 Pass
|
||||||
|
Pass Moving a style inside the document should not affect whether it's applied
|
|
@ -0,0 +1,349 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>Node.moveBefore</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<div id="log"></div>
|
||||||
|
<!-- First test shared pre-insertion checks that work similarly for replaceChild
|
||||||
|
and moveBefore -->
|
||||||
|
<script>
|
||||||
|
var insertFunc = Node.prototype.moveBefore;
|
||||||
|
</script>
|
||||||
|
<script src="../../pre-insertion-validation-hierarchy.js"></script>
|
||||||
|
<script>
|
||||||
|
preInsertionValidateHierarchy("moveBefore");
|
||||||
|
|
||||||
|
test(function() {
|
||||||
|
// WebIDL: first argument.
|
||||||
|
assert_throws_js(TypeError, function() { document.body.moveBefore(null, null) })
|
||||||
|
assert_throws_js(TypeError, function() { document.body.moveBefore(null, document.body.firstChild) })
|
||||||
|
assert_throws_js(TypeError, function() { document.body.moveBefore({'a':'b'}, document.body.firstChild) })
|
||||||
|
}, "Calling moveBefore with a non-Node first argument must throw TypeError.")
|
||||||
|
|
||||||
|
test(function() {
|
||||||
|
// WebIDL: second argument.
|
||||||
|
assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child")) })
|
||||||
|
assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child"), {'a':'b'}) })
|
||||||
|
}, "Calling moveBefore with second argument missing, or other than Node, null, or undefined, must throw TypeError.")
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
assert_false("moveBefore" in document.doctype, "moveBefore() not on DocumentType");
|
||||||
|
assert_false("moveBefore" in document.createTextNode("text"), "moveBefore() not on TextNode");
|
||||||
|
assert_false("moveBefore" in new Comment("comment"), "moveBefore() not on CommentNode");
|
||||||
|
assert_false("moveBefore" in document.createProcessingInstruction("foo", "bar"), "moveBefore() not on ProcessingInstruction");
|
||||||
|
}, "moveBefore() method does not exist on non-ParentNode Nodes");
|
||||||
|
|
||||||
|
// Pre-move validity, step 1:
|
||||||
|
// "If either parent or node are not connected, then throw a
|
||||||
|
// "HierarchyRequestError" DOMException."
|
||||||
|
//
|
||||||
|
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
|
||||||
|
test(t => {
|
||||||
|
const connectedTarget = document.body.appendChild(document.createElement('div'));
|
||||||
|
const disconnectedDestination = document.createElement('div');
|
||||||
|
t.add_cleanup(() => connectedTarget.remove());
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
disconnectedDestination.moveBefore(connectedTarget, null);
|
||||||
|
});
|
||||||
|
}, "moveBefore() on disconnected parent throws a HierarchyRequestError");
|
||||||
|
test(t => {
|
||||||
|
const connectedDestination = document.body.appendChild(document.createElement('div'));
|
||||||
|
const disconnectedTarget = document.createElement('div');
|
||||||
|
t.add_cleanup(() => connectedDestination.remove());
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
connectedDestination.moveBefore(disconnectedTarget, null);
|
||||||
|
});
|
||||||
|
}, "moveBefore() with disconnected target node throws a HierarchyRequestError");
|
||||||
|
|
||||||
|
// Pre-move validity, step 2:
|
||||||
|
// "If parent’s shadow-including root is not the same as node’s shadow-including
|
||||||
|
// "root, then throw a "HierarchyRequestError" DOMException."
|
||||||
|
//
|
||||||
|
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
|
||||||
|
test(t => {
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
document.body.append(iframe);
|
||||||
|
const connectedCrossDocChild = iframe.contentDocument.createElement('div');
|
||||||
|
const connectedLocalParent = document.querySelector('div');
|
||||||
|
t.add_cleanup(() => iframe.remove());
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
connectedLocalParent.moveBefore(connectedCrossDocChild, null);
|
||||||
|
});
|
||||||
|
}, "moveBefore() on a cross-document target node throws a HierarchyRequestError");
|
||||||
|
|
||||||
|
// Pre-move validity, step 3:
|
||||||
|
// "If parent is not a Document, DocumentFragment, or Element node, then throw a
|
||||||
|
// "HierarchyRequestError" DOMException."
|
||||||
|
//
|
||||||
|
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
|
||||||
|
test(t => {
|
||||||
|
const iframe = document.body.appendChild(document.createElement('iframe'));
|
||||||
|
const innerBody = iframe.contentDocument.querySelector('body');
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", iframe.contentWindow.DOMException, () => {
|
||||||
|
// Moving the body into the same place that it already is, which is a valid
|
||||||
|
// action in the normal case, when moving an Element directly under the
|
||||||
|
// document. This is not `moveBefore()`-specific behavior; it is consistent
|
||||||
|
// with traditional Document insertion rules, just like `insertBefore()`.
|
||||||
|
iframe.contentDocument.moveBefore(innerBody, null);
|
||||||
|
});
|
||||||
|
}, "moveBefore() into a Document throws a HierarchyRequestError");
|
||||||
|
test(t => {
|
||||||
|
const iframe = document.body.appendChild(document.createElement('iframe'));
|
||||||
|
const comment = iframe.contentDocument.createComment("comment");
|
||||||
|
iframe.contentDocument.body.append(comment);
|
||||||
|
|
||||||
|
iframe.contentDocument.moveBefore(comment, null);
|
||||||
|
assert_equals(comment.parentNode, iframe.contentDocument);
|
||||||
|
}, "moveBefore() CharacterData into a Document");
|
||||||
|
|
||||||
|
// Pre-move validity, step 4:
|
||||||
|
// "If node is a host-including inclusive ancestor of parent, then throw a
|
||||||
|
// "HierarchyRequestError" DOMException."
|
||||||
|
//
|
||||||
|
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
|
||||||
|
test(t => {
|
||||||
|
const parentDiv = document.body.appendChild(document.createElement('div'));
|
||||||
|
const childDiv = parentDiv.appendChild(document.createElement('div'));
|
||||||
|
t.add_cleanup(() => {
|
||||||
|
parentDiv.remove();
|
||||||
|
childDiv.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
parentDiv.moveBefore(parentDiv, null);
|
||||||
|
}, "parent moving itself");
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
childDiv.moveBefore(parentDiv, null);
|
||||||
|
}, "Moving parent into immediate child");
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
childDiv.moveBefore(document.body, null);
|
||||||
|
}, "Moving grandparent into grandchild");
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
document.body.moveBefore(document.documentElement, childDiv);
|
||||||
|
}, "Moving documentElement (<html>) into a deeper child");
|
||||||
|
}, "moveBefore() with node being an inclusive ancestor of parent throws a " +
|
||||||
|
"HierarchyRequestError");
|
||||||
|
|
||||||
|
// Pre-move validity, step 5:
|
||||||
|
// "If node is not an Element or a CharacterData node, then throw a
|
||||||
|
// "HierarchyRequestError" DOMException."
|
||||||
|
//
|
||||||
|
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
|
||||||
|
test(t => {
|
||||||
|
assert_true(document.doctype.isConnected);
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
document.body.moveBefore(document.doctype, null);
|
||||||
|
}, "DocumentType throws");
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
document.body.moveBefore(new DocumentFragment(), null);
|
||||||
|
}, "DocumentFragment throws");
|
||||||
|
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
assert_true(doc.isConnected);
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
document.body.moveBefore(doc, null);
|
||||||
|
});
|
||||||
|
}, "moveBefore() with a non-{Element, CharacterData} throws a HierarchyRequestError");
|
||||||
|
promise_test(async t => {
|
||||||
|
const text = new Text("child text");
|
||||||
|
document.body.prepend(text);
|
||||||
|
|
||||||
|
const childElement = document.createElement('p');
|
||||||
|
document.body.prepend(childElement);
|
||||||
|
|
||||||
|
const comment = new Comment("comment");
|
||||||
|
document.body.prepend(comment);
|
||||||
|
|
||||||
|
t.add_cleanup(() => {
|
||||||
|
text.remove();
|
||||||
|
childElement.remove();
|
||||||
|
comment.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until style is computed once, then continue after. This is necessary
|
||||||
|
// to reproduce a Chromium crash regression with moving Comment nodes in the
|
||||||
|
// DOM.
|
||||||
|
await new Promise(r => {
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => r()));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.moveBefore(text, null);
|
||||||
|
assert_equals(document.body.lastChild, text);
|
||||||
|
|
||||||
|
document.body.moveBefore(childElement, null);
|
||||||
|
assert_equals(document.body.lastChild, childElement);
|
||||||
|
|
||||||
|
document.body.moveBefore(text, null);
|
||||||
|
assert_equals(document.body.lastChild, text);
|
||||||
|
|
||||||
|
document.body.moveBefore(comment, null);
|
||||||
|
assert_equals(document.body.lastChild, comment);
|
||||||
|
}, "moveBefore with an Element or CharacterData succeeds");
|
||||||
|
test(t => {
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.textContent = "Some content";
|
||||||
|
document.body.prepend(p);
|
||||||
|
|
||||||
|
const text_node = p.firstChild;
|
||||||
|
|
||||||
|
// The Text node is *inside* the paragraph.
|
||||||
|
assert_equals(text_node.textContent, "Some content");
|
||||||
|
assert_not_equals(document.body.lastChild, text_node);
|
||||||
|
|
||||||
|
t.add_cleanup(() => {
|
||||||
|
text_node.remove();
|
||||||
|
p.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.moveBefore(p.firstChild, null);
|
||||||
|
assert_equals(document.body.lastChild, text_node);
|
||||||
|
}, "moveBefore on a paragraph's Text node child");
|
||||||
|
|
||||||
|
// Pre-move validity, step 6:
|
||||||
|
// "If child is non-null and its parent is not parent, then throw a
|
||||||
|
// "NotFoundError" DOMException."
|
||||||
|
//
|
||||||
|
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
|
||||||
|
test(t => {
|
||||||
|
const a = document.body.appendChild(document.createElement("div"));
|
||||||
|
const b = document.body.appendChild(document.createElement("div"));
|
||||||
|
const c = document.body.appendChild(document.createElement("div"));
|
||||||
|
|
||||||
|
t.add_cleanup(() => {
|
||||||
|
a.remove();
|
||||||
|
b.remove();
|
||||||
|
c.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_throws_dom("NotFoundError", () => {
|
||||||
|
a.moveBefore(b, c);
|
||||||
|
});
|
||||||
|
}, "moveBefore with reference child whose parent is NOT the destination " +
|
||||||
|
"parent (context node) throws a NotFoundError.")
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const a = document.body.appendChild(document.createElement("div"));
|
||||||
|
const b = document.createElement("div");
|
||||||
|
const c = document.createElement("div");
|
||||||
|
a.append(b);
|
||||||
|
a.append(c);
|
||||||
|
assert_array_equals(a.childNodes, [b, c]);
|
||||||
|
assert_equals(a.moveBefore(c, b), undefined, "moveBefore() returns undefined");
|
||||||
|
assert_array_equals(a.childNodes, [c, b]);
|
||||||
|
}, "moveBefore() returns undefined");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const a = document.body.appendChild(document.createElement("div"));
|
||||||
|
const b = document.createElement("div");
|
||||||
|
const c = document.createElement("div");
|
||||||
|
a.append(b);
|
||||||
|
a.append(c);
|
||||||
|
assert_array_equals(a.childNodes, [b, c]);
|
||||||
|
a.moveBefore(b, b);
|
||||||
|
assert_array_equals(a.childNodes, [b, c]);
|
||||||
|
a.moveBefore(c, c);
|
||||||
|
assert_array_equals(a.childNodes, [b, c]);
|
||||||
|
}, "Moving a node before itself should not move the node");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const disconnectedOrigin = document.createElement('div');
|
||||||
|
const disconnectedDestination = document.createElement('div');
|
||||||
|
const p = disconnectedOrigin.appendChild(document.createElement('p'));
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
|
||||||
|
disconnectedDestination.moveBefore(p, null);
|
||||||
|
});
|
||||||
|
}, "Moving a node from a disconnected container to a disconnected new parent " +
|
||||||
|
"without a shared ancestor throws a HIERARCHY_REQUEST_ERR");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const disconnectedOrigin = document.createElement('div');
|
||||||
|
const disconnectedDestination = disconnectedOrigin.appendChild(document.createElement('div'));
|
||||||
|
const p = disconnectedOrigin.appendChild(document.createElement('p'));
|
||||||
|
|
||||||
|
disconnectedDestination.moveBefore(p, null);
|
||||||
|
|
||||||
|
assert_equals(disconnectedDestination.firstChild, p, "<p> Was successfully moved");
|
||||||
|
}, "Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const disconnectedOrigin = document.createElement('div');
|
||||||
|
const disconnectedHost = disconnectedOrigin.appendChild(document.createElement('div'));
|
||||||
|
const p = disconnectedOrigin.appendChild(document.createElement('p'));
|
||||||
|
const shadow = disconnectedHost.attachShadow({mode: "closed"});
|
||||||
|
const disconnectedDestination = shadow.appendChild(document.createElement('div'));
|
||||||
|
|
||||||
|
disconnectedDestination.moveBefore(p, null);
|
||||||
|
|
||||||
|
assert_equals(disconnectedDestination.firstChild, p, "<p> Was successfully moved");
|
||||||
|
}, "Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds," +
|
||||||
|
"also across shadow-roots");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const disconnectedOrigin = document.createElement('div');
|
||||||
|
const connectedDestination = document.body.appendChild(document.createElement('div'));
|
||||||
|
const p = disconnectedOrigin.appendChild(document.createElement('p'));
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => connectedDestination.moveBefore(p, null));
|
||||||
|
}, "Moving a node from disconnected->connected throws a HIERARCHY_REQUEST_ERR");
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
const connectedOrigin = document.body.appendChild(document.createElement('div'));
|
||||||
|
const disconnectedDestination = document.createElement('div');
|
||||||
|
const p = connectedOrigin.appendChild(document.createElement('p'));
|
||||||
|
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => disconnectedDestination.moveBefore(p, null));
|
||||||
|
}, "Moving a node from connected->disconnected throws a HIERARCHY_REQUEST_ERR");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
let reactions = [];
|
||||||
|
const element_name = `ce-${performance.now()}`;
|
||||||
|
customElements.define(element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
connectedMoveCallback() { reactions.push("connectedMove"); }
|
||||||
|
connectedCallback() { reactions.push("connected"); }
|
||||||
|
disconnectedCallback() { reactions.push("disconnected"); }
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldParent = document.createElement('div');
|
||||||
|
const newParent = oldParent.appendChild(document.createElement('div'));
|
||||||
|
const element = oldParent.appendChild(document.createElement(element_name));
|
||||||
|
t.add_cleanup(() => {
|
||||||
|
element.remove();
|
||||||
|
newParent.remove();
|
||||||
|
oldParent.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a microtask to let any custom element reactions run (should be none,
|
||||||
|
// since the initial parent is disconnected).
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
newParent.moveBefore(element, null);
|
||||||
|
await Promise.resolve();
|
||||||
|
assert_array_equals(reactions, []);
|
||||||
|
}, "No custom element callbacks are run during disconnected moveBefore()");
|
||||||
|
|
||||||
|
// This is a regression test for a Chromium crash: https://crbug.com/388934346.
|
||||||
|
test(t => {
|
||||||
|
// This test caused a crash in Chromium because after the detection of invalid
|
||||||
|
// /node hierarchy, and throwing the JS error, we did not return from native
|
||||||
|
// code, and continued to operate on the node tree on bad assumptions.
|
||||||
|
const outer = document.createElement('div');
|
||||||
|
const div = outer.appendChild(document.createElement('div'));
|
||||||
|
assert_throws_dom("HIERARCHY_REQUEST_ERR", () => div.moveBefore(outer, null));
|
||||||
|
}, "Invalid node hierarchy with null old parent does not crash");
|
||||||
|
|
||||||
|
test(t => {
|
||||||
|
const outerDiv = document.createElement('div');
|
||||||
|
const innerDiv = outerDiv.appendChild(document.createElement('div'));
|
||||||
|
const iframe = innerDiv.appendChild(document.createElement('iframe'));
|
||||||
|
outerDiv.moveBefore(iframe, null);
|
||||||
|
}, "Move disconnected iframe does not crash");
|
||||||
|
</script>
|
|
@ -0,0 +1,133 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>Node.moveBefore custom element reactions</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<script>
|
||||||
|
;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<section id="section"></section>
|
||||||
|
<script>
|
||||||
|
promise_test(async t => {
|
||||||
|
const ce = document.getElementById("ce");
|
||||||
|
let reactions = [];
|
||||||
|
const element_name = `ce-${performance.now()}`;
|
||||||
|
customElements.define(element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
connectedCallback() { reactions.push("connected"); }
|
||||||
|
disconnectedCallback() { reactions.push("disconnected"); }
|
||||||
|
});
|
||||||
|
const element = document.createElement(element_name);
|
||||||
|
t.add_cleanup(() => element.remove());
|
||||||
|
document.body.append(element);
|
||||||
|
await Promise.resolve();
|
||||||
|
reactions = [];
|
||||||
|
document.getElementById("section").moveBefore(element, null);
|
||||||
|
await Promise.resolve();
|
||||||
|
assert_array_equals(reactions, ["disconnected", "connected"]);
|
||||||
|
}, "the disconnected/connected callbacks should be called when no other callback is defined");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const ce = document.getElementById("ce");
|
||||||
|
let reactions = [];
|
||||||
|
const element_name = `ce-${performance.now()}`;
|
||||||
|
customElements.define(element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
connectedCallback() { reactions.push(this.isConnected); }
|
||||||
|
disconnectedCallback() { reactions.push(this.isConnected); }
|
||||||
|
});
|
||||||
|
const element = document.createElement(element_name);
|
||||||
|
t.add_cleanup(() => element.remove());
|
||||||
|
document.body.append(element);
|
||||||
|
await Promise.resolve();
|
||||||
|
reactions = [];
|
||||||
|
document.getElementById("section").moveBefore(element, null);
|
||||||
|
await Promise.resolve();
|
||||||
|
assert_array_equals(reactions, [true, true]);
|
||||||
|
}, "the element should stay connected during the callbacks");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const ce = document.getElementById("ce");
|
||||||
|
let reactions = [];
|
||||||
|
const element_name = `ce-${performance.now()}`;
|
||||||
|
customElements.define(element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
connectedMoveCallback() { reactions.push("connectedMove"); }
|
||||||
|
connectedCallback() { reactions.push("connected"); }
|
||||||
|
disconnectedCallback() { reactions.push("disconnected"); }
|
||||||
|
});
|
||||||
|
const element = document.createElement(element_name);
|
||||||
|
t.add_cleanup(() => element.remove());
|
||||||
|
document.body.append(element);
|
||||||
|
await Promise.resolve();
|
||||||
|
reactions = [];
|
||||||
|
document.getElementById("section").moveBefore(element, null);
|
||||||
|
await Promise.resolve();
|
||||||
|
assert_array_equals(reactions, ["connectedMove"]);
|
||||||
|
}, "When connectedMoveCallback is defined, it is called instead of disconnectedCallback/connectedCallback");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const ce = document.getElementById("ce");
|
||||||
|
let reactions = [];
|
||||||
|
const outer_element_name = `ce-${performance.now()}-outer`;
|
||||||
|
const inner_element_name = `ce-${performance.now()}-inner`;
|
||||||
|
customElements.define(outer_element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
connectedCallback() { reactions.push("outer connected"); }
|
||||||
|
disconnectedCallback() { reactions.push("outer disconnected"); }
|
||||||
|
});
|
||||||
|
customElements.define(inner_element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
connectedCallback() { reactions.push("inner connected"); }
|
||||||
|
disconnectedCallback() { reactions.push("inner disconnected"); }
|
||||||
|
});
|
||||||
|
const outer = document.createElement(outer_element_name);
|
||||||
|
const inner = document.createElement(inner_element_name);
|
||||||
|
t.add_cleanup(() => outer.remove());
|
||||||
|
outer.append(inner);
|
||||||
|
document.body.append(outer);
|
||||||
|
await Promise.resolve();
|
||||||
|
reactions = [];
|
||||||
|
document.getElementById("section").moveBefore(outer, null);
|
||||||
|
await Promise.resolve();
|
||||||
|
assert_array_equals(reactions, ["outer disconnected", "outer connected", "inner disconnected", "inner connected"]);
|
||||||
|
}, "Reactions to atomic move are called in order of element, not in order of operation");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const ce = document.getElementById("ce");
|
||||||
|
let reactions = [];
|
||||||
|
const element_name = `ce-${performance.now()}`;
|
||||||
|
customElements.define(element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
disconnectedCallback() { reactions.push("disconnected"); }
|
||||||
|
});
|
||||||
|
const element = document.createElement(element_name);
|
||||||
|
t.add_cleanup(() => element.remove());
|
||||||
|
document.body.append(element);
|
||||||
|
await Promise.resolve();
|
||||||
|
reactions = [];
|
||||||
|
document.getElementById("section").moveBefore(element, null);
|
||||||
|
await Promise.resolve();
|
||||||
|
assert_array_equals(reactions, ["disconnected"]);
|
||||||
|
}, "When connectedCallback is not defined, no crash");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const ce = document.getElementById("ce");
|
||||||
|
let reactions = [];
|
||||||
|
const element_name = `ce-${performance.now()}`;
|
||||||
|
customElements.define(element_name,
|
||||||
|
class MockCustomElement extends HTMLElement {
|
||||||
|
connectedCallback() { reactions.push("connected"); }
|
||||||
|
});
|
||||||
|
const element = document.createElement(element_name);
|
||||||
|
t.add_cleanup(() => element.remove());
|
||||||
|
document.body.append(element);
|
||||||
|
await Promise.resolve();
|
||||||
|
reactions = [];
|
||||||
|
document.getElementById("section").moveBefore(element, null);
|
||||||
|
await Promise.resolve();
|
||||||
|
assert_array_equals(reactions, ["connected"]);
|
||||||
|
}, "When disconnectedCallback is not defined, no crash");
|
||||||
|
</script>
|
||||||
|
</body>
|
|
@ -0,0 +1,81 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>moveBefore should handle focus bubbling correctly</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<body>
|
||||||
|
<section id="old_parent">
|
||||||
|
<button id="button" tabindex="1">Button</button>
|
||||||
|
</section>
|
||||||
|
<section id="new_parent">
|
||||||
|
</section>
|
||||||
|
<section id="inert_parent" inert>
|
||||||
|
</section>
|
||||||
|
<section id="inert_when_not_empty_parent">
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#inert_when_not_empty_parent:has(button) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function assert_focus_within(expected) {
|
||||||
|
const element_to_string = e => e.id || e.nodeName;
|
||||||
|
assert_array_equals(
|
||||||
|
Array.from(document.querySelectorAll(":focus-within"), element_to_string),
|
||||||
|
expected.map(element_to_string));
|
||||||
|
}
|
||||||
|
|
||||||
|
test(t => {
|
||||||
|
const old_parent = document.querySelector("#old_parent");
|
||||||
|
const button = document.querySelector("#button");
|
||||||
|
t.add_cleanup(() => old_parent.append(button));
|
||||||
|
button.focus();
|
||||||
|
assert_focus_within([document.documentElement, document.body, old_parent, button]);
|
||||||
|
new_parent.moveBefore(button, null);
|
||||||
|
assert_focus_within([document.documentElement, document.body, new_parent, button]);
|
||||||
|
}, "focus-within should be updated when reparenting focused element directly");
|
||||||
|
|
||||||
|
test(t => {
|
||||||
|
const old_parent = document.querySelector("#old_parent");
|
||||||
|
const button = document.querySelector("#button");
|
||||||
|
t.add_cleanup(() => document.body.append(old_parent));
|
||||||
|
button.focus();
|
||||||
|
new_parent.moveBefore(old_parent, null);
|
||||||
|
assert_focus_within([document.documentElement, document.body, new_parent, old_parent, button]);
|
||||||
|
}, "focus-within should be updated when reparenting an element that has focus within");
|
||||||
|
|
||||||
|
test(t => {
|
||||||
|
const old_parent = document.querySelector("#old_parent");
|
||||||
|
const button = document.querySelector("#button");
|
||||||
|
t.add_cleanup(() => old_parent.append(button));
|
||||||
|
button.focus();
|
||||||
|
old_parent.moveBefore(button, null);
|
||||||
|
assert_focus_within([document.documentElement, document.body, old_parent, button]);
|
||||||
|
}, "focus-within should remain the same when moving to the same parent");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const old_parent = document.querySelector("#old_parent");
|
||||||
|
const inert_parent= document.querySelector("#inert_parent");
|
||||||
|
const button = document.querySelector("#button");
|
||||||
|
t.add_cleanup(() => old_parent.append(button));
|
||||||
|
button.focus();
|
||||||
|
inert_parent.moveBefore(button, null);
|
||||||
|
assert_focus_within([document.documentElement, document.body, inert_parent, button]);
|
||||||
|
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||||
|
assert_focus_within([]);
|
||||||
|
}, ":focus-within should be eventually up to date when moving to an inert subtree");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const old_parent = document.querySelector("#old_parent");
|
||||||
|
const inert_when_not_empty_parent = document.querySelector("#inert_when_not_empty_parent");
|
||||||
|
const button = document.querySelector("#button");
|
||||||
|
t.add_cleanup(() => old_parent.append(button));
|
||||||
|
button.focus();
|
||||||
|
inert_when_not_empty_parent.moveBefore(button, null);
|
||||||
|
assert_focus_within([document.documentElement, document.body, inert_when_not_empty_parent, button]);
|
||||||
|
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||||
|
assert_focus_within([]);
|
||||||
|
}, ":focus-within should be eventually up to date when moving to a subtree that would become inert via style");
|
||||||
|
</script>
|
|
@ -0,0 +1,94 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src='../../../../resources/testharness.js'></script>
|
||||||
|
<script src='../../../../resources/testharnessreport.js'></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
test(t => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id=old_parent>
|
||||||
|
<span id=start>RangeStartTarget</span>
|
||||||
|
<span id=middle>Middle</span>
|
||||||
|
<span id=end>RangeEndTarget</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const range = new Range();
|
||||||
|
range.setStart(start, 0);
|
||||||
|
range.setEnd(end, 0);
|
||||||
|
|
||||||
|
assert_true(range.intersectsNode(middle), "Intersection before move");
|
||||||
|
// Moves `start` span to the very bottom of the container.
|
||||||
|
old_parent.moveBefore(start, null);
|
||||||
|
|
||||||
|
// In an ordinary removal, when a node whose descendant is the start (or end)
|
||||||
|
// of a live range is removed, the range's start is set to the removed node's
|
||||||
|
// parent. For now, the same thing happens during `moveBefore()`.
|
||||||
|
assert_equals(range.startContainer, old_parent, "startContainer updates during move");
|
||||||
|
assert_equals(range.endContainer, end, "endContainer does not update after move");
|
||||||
|
assert_true(range.intersectsNode(middle), "adjusted range still intersects " +
|
||||||
|
"middle node after move");
|
||||||
|
}, "moveBefore still results in range startContainer snapping up to parent " +
|
||||||
|
"when startContainer is moved");
|
||||||
|
|
||||||
|
test(t => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id=old_parent>
|
||||||
|
<div id=movable_div>
|
||||||
|
<span id=start>RangeStartTarget</span>
|
||||||
|
<span id=middle>Middle</span>
|
||||||
|
</div>
|
||||||
|
<span id=end>RangeEndTarget</span>
|
||||||
|
</div>
|
||||||
|
<div id=new_parent></div>`;
|
||||||
|
|
||||||
|
const range = new Range();
|
||||||
|
range.setStart(start, 0);
|
||||||
|
range.setEnd(end, 0);
|
||||||
|
|
||||||
|
assert_true(range.intersectsNode(middle), "Intersection before move");
|
||||||
|
new_parent.moveBefore(movable_div, null);
|
||||||
|
|
||||||
|
assert_equals(range.startContainer, old_parent, "startContainer still " +
|
||||||
|
"updates during move, to snap to parent");
|
||||||
|
assert_equals(range.endContainer, end, "endContainer does not update after move");
|
||||||
|
assert_false(range.intersectsNode(middle), "range no longer intersects " +
|
||||||
|
"middle node after move, since middle node was moved outside of the range");
|
||||||
|
}, "moveBefore still causes range startContainer to snap up to parent, when " +
|
||||||
|
"startContainer ancestor is moved");
|
||||||
|
|
||||||
|
test(t => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id=old_parent>
|
||||||
|
<span id=start>RangeStartTarget</span>
|
||||||
|
<div id=movable_div>
|
||||||
|
<span id=end>RangeEndTarget</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id=new_parent>
|
||||||
|
<span id=middle>Middle</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const range = new Range();
|
||||||
|
range.setStart(start, 0);
|
||||||
|
range.setEnd(end, 0);
|
||||||
|
|
||||||
|
assert_false(range.intersectsNode(middle), "No intersection before move");
|
||||||
|
new_parent.moveBefore(movable_div, null);
|
||||||
|
|
||||||
|
assert_equals(range.startContainer, start, "startContainer does not update " +
|
||||||
|
"after move");
|
||||||
|
assert_equals(range.endContainer, old_parent, "endContainer still snaps up " +
|
||||||
|
"to parent after move");
|
||||||
|
assert_false(range.intersectsNode(middle), "adjusted range still does not " +
|
||||||
|
"intersect middle node after move");
|
||||||
|
}, "moveBefore still causes range endContainer to snap up to parent, when " +
|
||||||
|
"endContainer ancestor is moved");
|
||||||
|
</script>
|
||||||
|
</html>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#item {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
#section1 {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#section2 {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<section id="section1">
|
||||||
|
<div id="item">
|
||||||
|
<template shadowRootMode="open">
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-color: var(--color, red);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="section2">
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
promise_test(async () => {
|
||||||
|
const item = document.querySelector("#item");
|
||||||
|
document.querySelector("#section2").moveBefore(item, null);
|
||||||
|
await new Promise(resolve => requestAnimationFrame(() => resolve()));
|
||||||
|
assert_equals(item.shadowRoot.querySelector("div").getBoundingClientRect().width, 300);
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id=shadowTarget></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
test(() => {
|
||||||
|
shadowTarget.attachShadow({mode: 'open'});
|
||||||
|
const child1 = document.createElement('p');
|
||||||
|
child1.textContent = 'Child1';
|
||||||
|
const child2 = document.createElement('p');
|
||||||
|
child2.textContent = 'Child2';
|
||||||
|
|
||||||
|
shadowTarget.shadowRoot.append(child1, child2);
|
||||||
|
shadowTarget.shadowRoot.moveBefore(child2, child1);
|
||||||
|
assert_equals(shadowTarget.shadowRoot.firstChild, child2, "Original lastChild is now firstChild");
|
||||||
|
assert_equals(shadowTarget.shadowRoot.lastChild, child1, "Original firstChild is now lastChild");
|
||||||
|
}, "moveBefore() is allowed in ShadowRoots (i.e., connected DocumentFragments)");
|
||||||
|
</script>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>Mutation events are suppressed during moveBefore()</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p id=reference>reference</p>
|
||||||
|
<p id=target>target</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const reference = document.querySelector('#reference');
|
||||||
|
const target = document.querySelector('#target');
|
||||||
|
|
||||||
|
test(() => {
|
||||||
|
target.addEventListener('DOMNodeInserted', () => assert_unreached('DOMNodeInserted not called'));
|
||||||
|
target.addEventListener('DOMNodeRemoved', () => assert_unreached('DOMNodeRemoved not called'));
|
||||||
|
document.body.moveBefore(target, reference);
|
||||||
|
}, "MutationEvents (if supported by the UA) are suppressed during `moveBefore()`");
|
||||||
|
</script>
|
||||||
|
</body>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>slotchanged event</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id=oldParent>
|
||||||
|
<p id=target></p>
|
||||||
|
</div>
|
||||||
|
<div id=newParent></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function runTest(oldParent, target, newParent) {
|
||||||
|
const observations = [];
|
||||||
|
const observer = new MutationObserver(mutationList => observations.push(mutationList));
|
||||||
|
|
||||||
|
observer.observe(oldParent, {childList: true});
|
||||||
|
observer.observe(target, {childList: true});
|
||||||
|
observer.observe(newParent, {childList: true});
|
||||||
|
|
||||||
|
newParent.moveBefore(target, null);
|
||||||
|
|
||||||
|
// Wait for microtasks to settle.
|
||||||
|
await new Promise(resolve => queueMicrotask(resolve));
|
||||||
|
|
||||||
|
assert_equals(observations.length, 1, "MutationObserver has emitted a single mutation list");
|
||||||
|
assert_equals(observations[0].length, 2, "Mutation list has two MutationRecords");
|
||||||
|
|
||||||
|
const removalRecord = observations[0][0];
|
||||||
|
const insertionRecord = observations[0][1];
|
||||||
|
assert_equals(removalRecord.target, oldParent, "removalRecord target is correct");
|
||||||
|
assert_equals(removalRecord.removedNodes[0], target, "removedNodes contains the moved node");
|
||||||
|
assert_equals(insertionRecord.target, newParent, "insertionRecord target is correct");
|
||||||
|
assert_equals(insertionRecord.addedNodes[0], target, "addedNodes contains the moved node");
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
await runTest(oldParent, target, newParent);
|
||||||
|
}, "[Connected move] MutationObserver removal + insertion is tracked by moveBefore()");
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const oldParent = document.createElement('div');
|
||||||
|
const target = document.createElement('p');
|
||||||
|
const newParent = document.createElement('div');
|
||||||
|
// We must append `newParent` as well, since the origin and destination nodes
|
||||||
|
// must share the same shadow-including root.
|
||||||
|
oldParent.append(target, newParent);
|
||||||
|
|
||||||
|
await runTest(oldParent, target, newParent);
|
||||||
|
}, "[Disconnected move] MutationObserver removal + insertion is tracked by moveBefore()");
|
||||||
|
</script>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>Nonce attribute is not cleared</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<section id="new_parent"></section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
test(t => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
document.body.append(div);
|
||||||
|
|
||||||
|
const kNonce = '8IBTHwOdqNKAWeKl7plt8g==';
|
||||||
|
div.setAttribute('nonce', kNonce);
|
||||||
|
assert_equals(div.getAttribute('nonce'), kNonce);
|
||||||
|
|
||||||
|
new_parent.moveBefore(div, null);
|
||||||
|
assert_equals(div.getAttribute('nonce'), kNonce);
|
||||||
|
|
||||||
|
new_parent.insertBefore(div, null);
|
||||||
|
assert_equals(div.getAttribute('nonce'), "");
|
||||||
|
}, "Element nonce content attribute is not cleared after move");
|
||||||
|
</script>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>Object element moveBefore() regression test</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<b id="p"><object>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
test(t => {
|
||||||
|
// Per https://crbug.com/373924127, simply moving an object element would
|
||||||
|
// crash, due to an internal subframe count mechanism getting out of sync.
|
||||||
|
p.moveBefore(p.lastChild, p.firstChild);
|
||||||
|
}, "Moving an object element does not crash");
|
||||||
|
</script>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<title>moveBefore should not close a popover</title>
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
<body>
|
||||||
|
<section id="old_parent">
|
||||||
|
<div popover>
|
||||||
|
Popover
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="new_parent">
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
promise_test(async t => {
|
||||||
|
const popover = document.querySelector("div[popover]");
|
||||||
|
popover.showPopover();
|
||||||
|
await new Promise(resolve => requestAnimationFrame(() => resolve()));
|
||||||
|
assert_equals(document.querySelector(":popover-open"), popover);
|
||||||
|
document.querySelector("#new_parent").moveBefore(popover, null);
|
||||||
|
assert_equals(document.querySelector(":popover-open"), popover);
|
||||||
|
}, "when reparenting an open popover, it shouldn't be closed automatically");
|
||||||
|
</script>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<script src='../../../../resources/testharness.js'></script>
|
||||||
|
<script src='../../../../resources/testharnessreport.js'></script>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
promise_test(async t => {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.href = "data:text/css,body{background: green}";
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
t.add_cleanup(() => link.remove());
|
||||||
|
document.body.append(link);
|
||||||
|
const backgroundColorBefore = getComputedStyle(document.body).backgroundColor;
|
||||||
|
document.body.moveBefore(link, null);
|
||||||
|
assert_equals(getComputedStyle(document.body).backgroundColor, backgroundColorBefore);
|
||||||
|
}, "Moving a style inside the document should not affect whether it's applied");
|
||||||
|
</script>
|
||||||
|
</html>
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* Validations where `child` argument is irrelevant.
|
||||||
|
* @param {Function} methodName
|
||||||
|
*/
|
||||||
|
function preInsertionValidateHierarchy(methodName) {
|
||||||
|
function insert(parent, node) {
|
||||||
|
if (parent[methodName].length > 1) {
|
||||||
|
// This is for insertBefore(). We can't blindly pass `null` for all methods
|
||||||
|
// as doing so will move nodes before validation.
|
||||||
|
parent[methodName](node, null);
|
||||||
|
} else {
|
||||||
|
parent[methodName](node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc.body, doc.body));
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc.body, doc.documentElement));
|
||||||
|
}, "If node is a host-including inclusive ancestor of parent, then throw a HierarchyRequestError DOMException.");
|
||||||
|
|
||||||
|
// Step 4
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const doc2 = document.implementation.createHTMLDocument("title2");
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc, doc2));
|
||||||
|
}, "If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, then throw a HierarchyRequestError DOMException.");
|
||||||
|
|
||||||
|
// Step 5, in case of inserting a text node into a document
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc, doc.createTextNode("text")));
|
||||||
|
}, "If node is a Text node and parent is a document, then throw a HierarchyRequestError DOMException.");
|
||||||
|
|
||||||
|
// Step 5, in case of inserting a doctype into a non-document
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const doctype = doc.childNodes[0];
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc.createElement("a"), doctype));
|
||||||
|
}, "If node is a doctype and parent is not a document, then throw a HierarchyRequestError DOMException.")
|
||||||
|
|
||||||
|
// Step 6, in case of DocumentFragment including multiple elements
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
doc.documentElement.remove();
|
||||||
|
const df = doc.createDocumentFragment();
|
||||||
|
df.appendChild(doc.createElement("a"));
|
||||||
|
df.appendChild(doc.createElement("b"));
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc, df));
|
||||||
|
}, "If node is a DocumentFragment with multiple elements and parent is a document, then throw a HierarchyRequestError DOMException.");
|
||||||
|
|
||||||
|
// Step 6, in case of DocumentFragment has multiple elements when document already has an element
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const df = doc.createDocumentFragment();
|
||||||
|
df.appendChild(doc.createElement("a"));
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc, df));
|
||||||
|
}, "If node is a DocumentFragment with an element and parent is a document with another element, then throw a HierarchyRequestError DOMException.");
|
||||||
|
|
||||||
|
// Step 6, in case of an element
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const el = doc.createElement("a");
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc, el));
|
||||||
|
}, "If node is an Element and parent is a document with another element, then throw a HierarchyRequestError DOMException.");
|
||||||
|
|
||||||
|
// Step 6, in case of a doctype when document already has another doctype
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const doctype = doc.childNodes[0].cloneNode();
|
||||||
|
doc.documentElement.remove();
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc, doctype));
|
||||||
|
}, "If node is a doctype and parent is a document with another doctype, then throw a HierarchyRequestError DOMException.");
|
||||||
|
|
||||||
|
// Step 6, in case of a doctype when document has an element
|
||||||
|
if (methodName !== "prepend") {
|
||||||
|
// Skip `.prepend` as this doesn't throw if `child` is an element
|
||||||
|
test(() => {
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const doctype = doc.childNodes[0].cloneNode();
|
||||||
|
doc.childNodes[0].remove();
|
||||||
|
assert_throws_dom("HierarchyRequestError", () => insert(doc, doctype));
|
||||||
|
}, "If node is a doctype and parent is a document with an element, then throw a HierarchyRequestError DOMException.");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue