diff --git a/third_party/blink/renderer/core/dom/node.cc b/third_party/blink/renderer/core/dom/node.cc index 6936978b34ca0..f74496a0d44bb 100644 --- a/third_party/blink/renderer/core/dom/node.cc +++ b/third_party/blink/renderer/core/dom/node.cc @@ -57,6 +57,7 @@ #include "third_party/blink/renderer/core/dom/events/event_dispatcher.h" #include "third_party/blink/renderer/core/dom/events/event_listener.h" #include "third_party/blink/renderer/core/dom/events/event_path.h" +#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h" #include "third_party/blink/renderer/core/dom/flat_tree_node_data.h" #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/dom/focus_params.h" @@ -771,6 +772,12 @@ void Node::moveBefore(Node* new_child, // impossible for nested `moveBefore()` calls to occur. Assert that no atomic // move is already in progress. DCHECK(!GetDocument().StatePreservingAtomicMoveInProgress()); + + // Insertion can synchronously reach paths that emit scoped events (for + // example, form controls reacting to ancestor disabled-state changes). Defer + // any such events until the state-preserving atomic move has completed so + // that listeners never observe the intermediate state. + EventQueueScope queue_scope; GetDocument().SetStatePreservingAtomicMoveInProgress(true); ContainerNode* old_parent = new_child->parentNode(); diff --git a/third_party/blink/renderer/core/dom/node_test.cc b/third_party/blink/renderer/core/dom/node_test.cc index d68bcfeff76f8..d48f5c3b20c5b 100644 --- a/third_party/blink/renderer/core/dom/node_test.cc +++ b/third_party/blink/renderer/core/dom/node_test.cc @@ -5,11 +5,13 @@ #include "third_party/blink/renderer/core/dom/node.h" #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" +#include "third_party/blink/renderer/bindings/core/v8/v8_mouse_event_init.h" #include "third_party/blink/renderer/bindings/core/v8/v8_shadow_root_init.h" #include "third_party/blink/renderer/core/css/resolver/style_resolver.h" #include "third_party/blink/renderer/core/css/style_engine.h" #include "third_party/blink/renderer/core/dom/comment.h" #include "third_party/blink/renderer/core/dom/element.h" +#include "third_party/blink/renderer/core/dom/events/native_event_listener.h" #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/dom/layout_tree_builder.h" #include "third_party/blink/renderer/core/dom/processing_instruction.h" @@ -18,7 +20,10 @@ #include "third_party/blink/renderer/core/dom/slot_assignment_engine.h" #include "third_party/blink/renderer/core/dom/slot_assignment_recalc_forbidden_scope.h" #include "third_party/blink/renderer/core/editing/testing/editing_test_base.h" +#include "third_party/blink/renderer/core/events/mouse_event.h" +#include "third_party/blink/renderer/core/html/forms/spin_button_element.h" #include "third_party/blink/renderer/core/html/html_div_element.h" +#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" namespace blink { @@ -610,6 +615,66 @@ TEST_F(NodeTest, moveBefore_DetachAfterSlotReassignment) { EXPECT_FALSE(span->GetComputedStyle()); } +namespace { + +class AtomicMoveStateRecorder : public NativeEventListener { + public: + void Invoke(ExecutionContext*, Event* event) override { + fired_ = true; + Node* target = event->target()->ToNode(); + in_atomic_move_ = + target->GetDocument().StatePreservingAtomicMoveInProgress(); + } + + bool fired_ = false; + bool in_atomic_move_ = false; +}; + +} // namespace + +TEST_F(NodeTest, moveBefore_DefersScopedEvents) { + GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"HTML( + +
+ + +
+ )HTML"); + + UpdateAllLifecyclePhasesForTest(); + + Element* fieldset = GetDocument().getElementById(AtomicString("fs")); + Element* legend = GetDocument().getElementById(AtomicString("lg")); + Element* input = GetDocument().getElementById(AtomicString("num")); + + // The number input is inside the first of a disabled
, so + // it remains enabled and its spin button responds to mouse events. Dispatch a + // mousedown to the spin button so that it acquires pointer capture and + // updates the value via a user edit. + ShadowRoot* shadow_root = input->UserAgentShadowRoot(); + ASSERT_TRUE(shadow_root); + auto* spin_button = DynamicTo( + shadow_root->getElementById(shadow_element_names::kIdSpinButton)); + ASSERT_TRUE(spin_button); + MouseEventInit* init = MouseEventInit::Create(); + init->setButton(0); + auto* mousedown = + MakeGarbageCollected(event_type_names::kMousedown, init); + static_cast(spin_button)->DefaultEventHandler(*mousedown); + ASSERT_TRUE(mousedown->DefaultHandled()); + + auto* listener = MakeGarbageCollected(); + input->addEventListener(event_type_names::kChange, listener); + + // Reordering the children of a
recomputes the disabled + // state of their descendant form controls, which makes the number input's + // spin button release capture and dispatch a scoped change event. The + // listener must not observe a state-preserving atomic move in progress. + fieldset->moveBefore(legend, nullptr, ASSERT_NO_EXCEPTION); + EXPECT_TRUE(listener->fired_); + EXPECT_FALSE(listener->in_atomic_move_); +} + #if DCHECK_IS_ON() TEST_F(NodeTest, ToStringDisallowedAssignmentRecalc) { GetDocument().body()->SetHTMLUnsafeWithoutTrustedTypes(R"HTML(