diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt index 3079f78644..841b5b1505 100644 --- a/bridge/CMakeLists.txt +++ b/bridge/CMakeLists.txt @@ -248,6 +248,7 @@ if ($ENV{WEBF_JS_ENGINE} MATCHES "quickjs") bindings/qjs/union_base.cc # Core sources core/executing_context.cc + core/script_forbidden_scope.cc core/script_state.cc core/page.cc core/dart_methods.cc @@ -287,6 +288,7 @@ if ($ENV{WEBF_JS_ENGINE} MATCHES "quickjs") core/binding_object.cc core/dom/node.cc core/dom/node_list.cc + core/dom/static_node_list.cc core/dom/node_traversal.cc core/dom/live_node_list_base.cc core/dom/character_data.cc @@ -305,6 +307,11 @@ if ($ENV{WEBF_JS_ENGINE} MATCHES "quickjs") core/dom/document_fragment.cc core/dom/child_node_list.cc core/dom/empty_node_list.cc + core/dom/mutation_observer.cc + core/dom/mutation_observer_registration.cc + core/dom/mutation_observer_interest_group.cc + core/dom/mutation_record.cc + core/dom/child_list_mutation_scope.cc core/dom/container_node.cc core/html/custom/widget_element.cc core/events/error_event.cc @@ -407,6 +414,10 @@ if ($ENV{WEBF_JS_ENGINE} MATCHES "quickjs") out/qjs_touch.cc out/qjs_touch_init.cc out/qjs_touch_list.cc + out/qjs_mutation_record.cc + out/qjs_mutation_observer.cc + out/qjs_mutation_observer_init.cc + out/qjs_mutation_observer_registration.cc out/qjs_touch_event.cc out/qjs_touch_event_init.cc out/qjs_pointer_event.cc @@ -444,6 +455,7 @@ if ($ENV{WEBF_JS_ENGINE} MATCHES "quickjs") out/qjs_node_list.cc out/event_type_names.cc out/built_in_string.cc + out/mutation_record_types.cc out/binding_call_methods.cc out/qjs_scroll_options.cc out/qjs_scroll_to_options.cc diff --git a/bridge/bindings/qjs/binding_initializer.cc b/bridge/bindings/qjs/binding_initializer.cc index f37e50dce0..c052556f02 100644 --- a/bridge/bindings/qjs/binding_initializer.cc +++ b/bridge/bindings/qjs/binding_initializer.cc @@ -61,6 +61,9 @@ #include "qjs_message_event.h" #include "qjs_module_manager.h" #include "qjs_mouse_event.h" +#include "qjs_mutation_observer.h" +#include "qjs_mutation_observer_registration.h" +#include "qjs_mutation_record.h" #include "qjs_node.h" #include "qjs_node_list.h" #include "qjs_performance.h" @@ -166,6 +169,9 @@ void InstallBindings(ExecutingContext* context) { QJSTouch::Install(context); QJSTouchList::Install(context); QJSDOMStringMap::Install(context); + QJSMutationObserver::Install(context); + QJSMutationRecord::Install(context); + QJSMutationObserverRegistration::Install(context); QJSDOMTokenList::Install(context); QJSPerformance::Install(context); QJSPerformanceEntry::Install(context); diff --git a/bridge/bindings/qjs/converter_impl.h b/bridge/bindings/qjs/converter_impl.h index eaa4e7ce71..ab9de9f42b 100644 --- a/bridge/bindings/qjs/converter_impl.h +++ b/bridge/bindings/qjs/converter_impl.h @@ -237,6 +237,10 @@ struct Converter> : public ConverterBase } static JSValue ToValue(JSContext* ctx, const std::string& str) { return Converter::ToValue(ctx, str); } static JSValue ToValue(JSContext* ctx, typename Converter::ImplType value) { + if (value == AtomicString::Null()) { + return JS_UNDEFINED; + } + return Converter::ToValue(ctx, std::move(value)); } }; @@ -250,7 +254,12 @@ struct Converter> : public ConverterBase } static JSValue ToValue(JSContext* ctx, const std::string& value) { return AtomicString(ctx, value).ToQuickJS(ctx); } - static JSValue ToValue(JSContext* ctx, const AtomicString& value) { return value.ToQuickJS(ctx); } + static JSValue ToValue(JSContext* ctx, const AtomicString& value) { + if (value == AtomicString::Null()) { + return JS_NULL; + } + return value.ToQuickJS(ctx); + } }; template <> @@ -347,6 +356,8 @@ struct Converter>> : public ConverterBase>::FromValue(ctx, value, exception_state); } + + static JSValue ToValue(JSContext* ctx, ImplType value) { return Converter>::ToValue(ctx, value); } }; template diff --git a/bridge/bindings/qjs/cppgc/member.h b/bridge/bindings/qjs/cppgc/member.h index 076377227c..8a899f7028 100644 --- a/bridge/bindings/qjs/cppgc/member.h +++ b/bridge/bindings/qjs/cppgc/member.h @@ -10,6 +10,7 @@ #include "bindings/qjs/qjs_engine_patch.h" #include "bindings/qjs/script_value.h" #include "bindings/qjs/script_wrappable.h" +#include "core/executing_context.h" #include "foundation/casting.h" #include "mutation_scope.h" @@ -25,24 +26,22 @@ class ScriptWrappable; template > class Member { public: + struct KeyHasher { + std::size_t operator()(const Member& k) const { return reinterpret_cast(k.raw_); } + }; + Member() = default; Member(T* ptr) { SetRaw(ptr); } Member(const Member& other) { raw_ = other.raw_; runtime_ = other.runtime_; js_object_ptr_ = other.js_object_ptr_; + ((JSRefCountHeader*)other.js_object_ptr_)->ref_count++; } ~Member() { if (raw_ != nullptr) { assert(runtime_ != nullptr); - // There are two ways to free the member values: - // One is by GC marking and sweep stage. - // Two is by free directly when running out of function body. - // We detect the GC phase to handle case two, and free our members by hand(call JS_FreeValueRT directly). - JSGCPhaseEnum phase = JS_GetEnginePhase(runtime_); - if (phase == JS_GC_PHASE_DECREF || phase == JS_GC_PHASE_REMOVE_CYCLES) { - JS_FreeValueRT(runtime_, JS_MKPTR(JS_TAG_OBJECT, js_object_ptr_)); - } + JS_FreeValueRT(runtime_, JS_MKPTR(JS_TAG_OBJECT, js_object_ptr_)); } }; @@ -70,6 +69,7 @@ class Member { raw_ = other.raw_; runtime_ = other.runtime_; js_object_ptr_ = other.js_object_ptr_; + ((JSRefCountHeader*)other.js_object_ptr_)->ref_count++; return *this; } // Move assignment. @@ -94,6 +94,12 @@ class Member { T* operator->() const { return Get(); } T& operator*() const { return *Get(); } + T* Release() { + T* result = Get(); + Clear(); + return result; + } + private: void SetRaw(T* p) { if (p != nullptr) { diff --git a/bridge/bindings/qjs/heap_vector.h b/bridge/bindings/qjs/heap_vector.h index f51bf045a9..e1111a60b7 100644 --- a/bridge/bindings/qjs/heap_vector.h +++ b/bridge/bindings/qjs/heap_vector.h @@ -12,19 +12,27 @@ class HeapVector final { public: HeapVector() = default; - void Trace(GCVisitor* visitor) const; + void TraceValue(GCVisitor* visitor) const; + void TraceMember(GCVisitor* visitor) const; private: std::vector entries_; }; template -void HeapVector::Trace(GCVisitor* visitor) const { +void HeapVector::TraceValue(GCVisitor* visitor) const { for (auto& item : entries_) { visitor->TraceValue(item); } } +template +void HeapVector::TraceMember(GCVisitor* visitor) const { + for (auto& item : entries_) { + visitor->TraceMember(item); + } +} + } // namespace webf #endif // BRIDGE_BINDINGS_QJS_HEAP_VECTOR_H_ diff --git a/bridge/bindings/qjs/qjs_function.cc b/bridge/bindings/qjs/qjs_function.cc index a38c83c15a..9fe6b1bca7 100644 --- a/bridge/bindings/qjs/qjs_function.cc +++ b/bridge/bindings/qjs/qjs_function.cc @@ -60,7 +60,7 @@ ScriptValue QJSFunction::Invoke(JSContext* ctx, const ScriptValue& this_val, int JSValue returnValue = JS_Call(ctx, function_, this_val.QJSValue(), argc, argv); ExecutingContext* context = ExecutingContext::From(ctx); - context->DrainPendingPromiseJobs(); + context->DrainMicrotasks(); // Free the previous duplicated function. JS_FreeValue(ctx, function_); diff --git a/bridge/bindings/qjs/script_promise_resolver.cc b/bridge/bindings/qjs/script_promise_resolver.cc index e5040ec835..2e8b50e7a7 100644 --- a/bridge/bindings/qjs/script_promise_resolver.cc +++ b/bridge/bindings/qjs/script_promise_resolver.cc @@ -54,7 +54,7 @@ void ScriptPromiseResolver::ResolveOrRejectImmediately(JSValue value) { JS_FreeValue(context_->ctx(), return_value); } } - context_->DrainPendingPromiseJobs(); + context_->DrainMicrotasks(); } } // namespace webf diff --git a/bridge/bindings/qjs/script_wrappable.h b/bridge/bindings/qjs/script_wrappable.h index 4af076a2ab..6263d915a3 100644 --- a/bridge/bindings/qjs/script_wrappable.h +++ b/bridge/bindings/qjs/script_wrappable.h @@ -8,7 +8,6 @@ #include #include "bindings/qjs/cppgc/garbage_collected.h" -#include "core/executing_context.h" #include "foundation/macros.h" #include "wrapper_type_info.h" diff --git a/bridge/bindings/qjs/wrapper_type_info.h b/bridge/bindings/qjs/wrapper_type_info.h index 085bbef1ae..cb8cf5d58a 100644 --- a/bridge/bindings/qjs/wrapper_type_info.h +++ b/bridge/bindings/qjs/wrapper_type_info.h @@ -70,6 +70,9 @@ enum { JS_CLASS_HTML_LINK_ELEMENT, JS_CLASS_HTML_CANVAS_ELEMENT, JS_CLASS_IMAGE, + JS_CLASS_MUTATION_OBSERVER, + JS_CLASS_MUTATION_RECORD, + JS_CLASS_MUTATION_OBSERVER_REGISTRATION, JS_CLASS_CANVAS_RENDERING_CONTEXT, JS_CLASS_CANVAS_RENDERING_CONTEXT_2_D, JS_CLASS_CANVAS_GRADIENT, diff --git a/bridge/core/binding_object.cc b/bridge/core/binding_object.cc index db4375b104..1a5633cb37 100644 --- a/bridge/core/binding_object.cc +++ b/bridge/core/binding_object.cc @@ -8,6 +8,7 @@ #include "bindings/qjs/exception_state.h" #include "bindings/qjs/script_promise_resolver.h" #include "core/dom/events/event_target.h" +#include "core/dom/mutation_observer_interest_group.h" #include "core/executing_context.h" #include "foundation/native_string.h" #include "foundation/native_value_converter.h" @@ -123,6 +124,17 @@ NativeValue BindingObject::SetBindingProperty(const AtomicString& prop, "Can not set binding property on BindingObject, dart binding object had been disposed"); return Native_NewNull(); } + + if (auto element = const_cast(DynamicTo(this))) { + if (std::shared_ptr recipients = + MutationObserverInterestGroup::CreateForAttributesMutation(*element, prop)) { + NativeValue old_native_value = GetBindingProperty(prop, exception_state); + ScriptValue old_value = ScriptValue(ctx(), old_native_value); + recipients->EnqueueMutationRecord( + MutationRecord::CreateAttributes(element, prop, AtomicString::Null(), old_value.ToString(ctx()))); + } + } + GetExecutingContext()->FlushUICommand(); const NativeValue argv[] = {Native_NewString(prop.ToNativeString(GetExecutingContext()->ctx()).release()), value}; return InvokeBindingMethod(BindingMethodCallOperations::kSetProperty, 2, argv, exception_state); diff --git a/bridge/core/css/inline_css_style_declaration.cc b/bridge/core/css/inline_css_style_declaration.cc index 5df00f014b..09ba0e4937 100644 --- a/bridge/core/css/inline_css_style_declaration.cc +++ b/bridge/core/css/inline_css_style_declaration.cc @@ -5,9 +5,12 @@ #include "inline_css_style_declaration.h" #include #include "core/dom/element.h" +#include "core/dom/mutation_observer_interest_group.h" #include "core/executing_context.h" #include "core/html/parser/html_parser.h" #include "css_property_list.h" +#include "element_namespace_uris.h" +#include "html_names.h" namespace webf { @@ -52,6 +55,27 @@ static std::string parseJavaScriptCSSPropertyName(std::string& propertyName) { return result; } +static std::string convertCamelCaseToKebabCase(const std::string& propertyName) { + static std::unordered_map propertyCache{}; + + if (propertyCache.count(propertyName) > 0) { + return propertyCache[propertyName]; + } + + std::string result; + for (char c : propertyName) { + if (std::isupper(c)) { + result += '-'; + result += std::tolower(c); + } else { + result += c; + } + } + + propertyCache[propertyName] = result; + return result; +} + InlineCssStyleDeclaration* InlineCssStyleDeclaration::Create(ExecutingContext* context, ExceptionState& exception_state) { exception_state.ThrowException(context->ctx(), ErrorType::TypeError, "Illegal constructor."); @@ -79,7 +103,10 @@ bool InlineCssStyleDeclaration::SetItem(const AtomicString& key, } std::string propertyName = key.ToStdString(ctx()); - return InternalSetProperty(propertyName, value.ToLegacyDOMString(ctx())); + bool success = InternalSetProperty(propertyName, value.ToLegacyDOMString(ctx())); + if (success) + InlineStyleChanged(); + return success; } bool InlineCssStyleDeclaration::DeleteItem(const webf::AtomicString& key, webf::ExceptionState& exception_state) { @@ -90,6 +117,10 @@ int64_t InlineCssStyleDeclaration::length() const { return properties_.size(); } +void InlineCssStyleDeclaration::Clear() { + InternalClearProperty(); +} + AtomicString InlineCssStyleDeclaration::getPropertyValue(const AtomicString& key, ExceptionState& exception_state) { std::string propertyName = key.ToStdString(ctx()); return InternalGetPropertyValue(propertyName); @@ -99,7 +130,9 @@ void InlineCssStyleDeclaration::setProperty(const AtomicString& key, const ScriptValue& value, ExceptionState& exception_state) { std::string propertyName = key.ToStdString(ctx()); - InternalSetProperty(propertyName, value.ToLegacyDOMString(ctx())); + bool success = InternalSetProperty(propertyName, value.ToLegacyDOMString(ctx())); + if (success) + InlineStyleChanged(); } AtomicString InlineCssStyleDeclaration::removeProperty(const AtomicString& key, ExceptionState& exception_state) { @@ -117,7 +150,7 @@ AtomicString InlineCssStyleDeclaration::cssText() const { std::string result; size_t index = 0; for (auto& attr : properties_) { - result += attr.first + ": " + attr.second.ToStdString(ctx()) + ";"; + result += convertCamelCaseToKebabCase(attr.first) + ": " + attr.second.ToStdString(ctx()) + ";"; index++; if (index < properties_.size()) { result += " "; @@ -127,11 +160,12 @@ AtomicString InlineCssStyleDeclaration::cssText() const { } void InlineCssStyleDeclaration::setCssText(const webf::AtomicString& value, webf::ExceptionState& exception_state) { - const std::string css_text = value.ToStdString(ctx()); - setCssText(css_text, exception_state); + SetCSSTextInternal(value); + InlineStyleChanged(); } -void InlineCssStyleDeclaration::setCssText(const std::string& css_text, webf::ExceptionState& exception_state) { +void InlineCssStyleDeclaration::SetCSSTextInternal(const AtomicString& value) { + const std::string css_text = value.ToStdString(ctx()); InternalClearProperty(); std::vector styles; @@ -173,6 +207,24 @@ std::string InlineCssStyleDeclaration::ToString() const { return s; } +void InlineCssStyleDeclaration::InlineStyleChanged() { + assert(owner_element_->IsStyledElement()); + + owner_element_->InvalidateStyleAttribute(); + + if (std::shared_ptr recipients = + MutationObserverInterestGroup::CreateForAttributesMutation(*owner_element_, html_names::kStyleAttr)) { + AtomicString old_value = AtomicString::Null(); + if (owner_element_->attributes()->hasAttribute(html_names::kStyleAttr, ASSERT_NO_EXCEPTION())) { + old_value = owner_element_->attributes()->getAttribute(html_names::kStyleAttr, ASSERT_NO_EXCEPTION()); + } + + recipients->EnqueueMutationRecord( + MutationRecord::CreateAttributes(owner_element_, html_names::kStyleAttr, AtomicString::Null(), old_value)); + owner_element_->SynchronizeStyleAttributeInternal(); + } +} + bool InlineCssStyleDeclaration::NamedPropertyQuery(const AtomicString& key, ExceptionState&) { return cssPropertyList.count(key.ToStdString(ctx())) > 0; } @@ -196,9 +248,11 @@ AtomicString InlineCssStyleDeclaration::InternalGetPropertyValue(std::string& na bool InlineCssStyleDeclaration::InternalSetProperty(std::string& name, const AtomicString& value) { name = parseJavaScriptCSSPropertyName(name); if (properties_[name] == value) { - return true; + return false; } + AtomicString old_value = properties_[name]; + properties_[name] = value; std::unique_ptr args_01 = stringToNativeString(name); @@ -218,6 +272,8 @@ AtomicString InlineCssStyleDeclaration::InternalRemoveProperty(std::string& name AtomicString return_value = properties_[name]; properties_.erase(name); + InlineStyleChanged(); + std::unique_ptr args_01 = stringToNativeString(name); GetExecutingContext()->uiCommandBuffer()->addCommand(UICommand::kSetStyle, std::move(args_01), owner_element_->bindingObject(), nullptr); diff --git a/bridge/core/css/inline_css_style_declaration.h b/bridge/core/css/inline_css_style_declaration.h index 00ecdd4aa9..a4c91dea45 100644 --- a/bridge/core/css/inline_css_style_declaration.h +++ b/bridge/core/css/inline_css_style_declaration.h @@ -28,6 +28,7 @@ class InlineCssStyleDeclaration : public CSSStyleDeclaration { ScriptValue item(const AtomicString& key, ExceptionState& exception_state) override; bool SetItem(const AtomicString& key, const ScriptValue& value, ExceptionState& exception_state) override; bool DeleteItem(const webf::AtomicString& key, webf::ExceptionState& exception_state) override; + void Clear(); [[nodiscard]] int64_t length() const override; AtomicString getPropertyValue(const AtomicString& key, ExceptionState& exception_state) override; @@ -36,6 +37,8 @@ class InlineCssStyleDeclaration : public CSSStyleDeclaration { [[nodiscard]] std::string ToString() const; + void InlineStyleChanged(); + bool NamedPropertyQuery(const AtomicString&, ExceptionState&) override; void NamedPropertyEnumerator(std::vector& names, ExceptionState&) override; @@ -43,7 +46,7 @@ class InlineCssStyleDeclaration : public CSSStyleDeclaration { AtomicString cssText() const override; void setCssText(const AtomicString& value, ExceptionState& exception_state) override; - void setCssText(const std::string& value, ExceptionState& exception_state); + void SetCSSTextInternal(const AtomicString& value); void Trace(GCVisitor* visitor) const override; diff --git a/bridge/core/dom/character_data.cc b/bridge/core/dom/character_data.cc index 140ba89c53..5905ffb321 100644 --- a/bridge/core/dom/character_data.cc +++ b/bridge/core/dom/character_data.cc @@ -6,17 +6,29 @@ #include "character_data.h" #include "built_in_string.h" #include "core/dom/document.h" +#include "mutation_observer_interest_group.h" #include "qjs_character_data.h" namespace webf { void CharacterData::setData(const AtomicString& data, ExceptionState& exception_state) { + AtomicString old_data = data_; data_ = data; std::unique_ptr args_01 = data.ToNativeString(ctx()); std::unique_ptr args_02 = stringToNativeString("data"); GetExecutingContext()->uiCommandBuffer()->addCommand(UICommand::kSetAttribute, std::move(args_01), (void*)bindingObject(), args_02.release()); + + DidModifyData(old_data); +} + +void CharacterData::DidModifyData(const webf::AtomicString& old_data) { + std::shared_ptr mutation_recipients = + MutationObserverInterestGroup::CreateForCharacterDataMutation(*this); + if (mutation_recipients != nullptr) { + mutation_recipients->EnqueueMutationRecord(MutationRecord::CreateCharacterData(this, old_data)); + } } AtomicString CharacterData::nodeValue() const { diff --git a/bridge/core/dom/character_data.h b/bridge/core/dom/character_data.h index dc0553d452..e8b373f613 100644 --- a/bridge/core/dom/character_data.h +++ b/bridge/core/dom/character_data.h @@ -20,6 +20,8 @@ class CharacterData : public Node { int64_t length() const { return data_.length(); }; void setData(const AtomicString& data, ExceptionState& exception_state); + void DidModifyData(const AtomicString& old_data); + AtomicString nodeValue() const override; bool IsCharacterDataNode() const override; void setNodeValue(const AtomicString&, ExceptionState&) override; diff --git a/bridge/core/dom/child_list_mutation_scope.cc b/bridge/core/dom/child_list_mutation_scope.cc new file mode 100644 index 0000000000..80a71ecc23 --- /dev/null +++ b/bridge/core/dom/child_list_mutation_scope.cc @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#include "child_list_mutation_scope.h" + +namespace webf { + +// The accumulator map is used to make sure that there is only one mutation +// accumulator for a given node even if there are multiple +// ChildListMutationScopes on the stack. The map is always empty when there are +// no ChildListMutationScopes on the stack. +typedef std::unordered_map> AccumulatorMap; + +static AccumulatorMap& GetAccumulatorMap() { + thread_local static AccumulatorMap map; + return map; +} + +ChildListMutationAccumulator::ChildListMutationAccumulator( + Node* target, + const std::shared_ptr& observers) + : target_(target), last_added_(nullptr), observers_(observers), mutation_scopes_(0) {} + +ChildListMutationAccumulator::~ChildListMutationAccumulator() {} + +void ChildListMutationAccumulator::LeaveMutationScope() { + assert(mutation_scopes_ > 0u); + if (!--mutation_scopes_) { + if (!IsEmpty()) + EnqueueMutationRecord(); + GetAccumulatorMap().erase(target_.Get()); + } +} + +std::shared_ptr ChildListMutationAccumulator::GetOrCreate(Node& target) { + std::shared_ptr accumulator; + if (GetAccumulatorMap().count(&target) > 0) { + accumulator = GetAccumulatorMap()[&target]; + } else { + accumulator = std::make_shared( + &target, MutationObserverInterestGroup::CreateForChildListMutation(target)); + GetAccumulatorMap()[&target] = accumulator; + } + return accumulator; +} + +inline bool ChildListMutationAccumulator::IsAddedNodeInOrder(Node& child) { + return IsEmpty() || (last_added_ == child.previousSibling() && next_sibling_ == child.nextSibling()); +} + +void ChildListMutationAccumulator::ChildAdded(Node& child) { + assert(HasObservers()); + + if (!IsAddedNodeInOrder(child)) + EnqueueMutationRecord(); + + if (IsEmpty()) { + previous_sibling_ = child.previousSibling(); + next_sibling_ = child.nextSibling(); + } + + last_added_ = &child; + added_nodes_.emplace_back(&child); +} + +inline bool ChildListMutationAccumulator::IsRemovedNodeInOrder(Node& child) { + return IsEmpty() || next_sibling_ == &child; +} + +void ChildListMutationAccumulator::WillRemoveChild(Node& child) { + assert(HasObservers()); + + if (!added_nodes_.empty() || !IsRemovedNodeInOrder(child)) + EnqueueMutationRecord(); + + if (IsEmpty()) { + previous_sibling_ = child.previousSibling(); + next_sibling_ = child.nextSibling(); + last_added_ = child.previousSibling(); + } else { + next_sibling_ = child.nextSibling(); + } + + removed_nodes_.emplace_back(&child); +} + +void ChildListMutationAccumulator::EnqueueMutationRecord() { + assert(HasObservers()); + assert(!IsEmpty()); + + StaticNodeList* added_nodes = StaticNodeList::Adopt(target_->ctx(), added_nodes_); + StaticNodeList* removed_nodes = StaticNodeList::Adopt(target_->ctx(), removed_nodes_); + MutationRecord* record = MutationRecord::CreateChildList(target_, added_nodes, removed_nodes, + previous_sibling_.Release(), next_sibling_.Release()); + observers_->EnqueueMutationRecord(record); + last_added_ = nullptr; + assert(IsEmpty()); +} + +bool ChildListMutationAccumulator::IsEmpty() { + bool result = removed_nodes_.empty() && added_nodes_.empty(); + if (result) { + assert(!previous_sibling_); + assert(!next_sibling_); + assert(!last_added_); + } + return result; +} + +void ChildListMutationAccumulator::Trace(GCVisitor* visitor) const { + visitor->TraceMember(target_); + + for (auto& entry : removed_nodes_) { + visitor->TraceMember(entry); + } + for (auto& entry : added_nodes_) { + visitor->TraceMember(entry); + } + + visitor->TraceMember(previous_sibling_); + visitor->TraceMember(next_sibling_); + visitor->TraceMember(last_added_); + observers_->Trace(visitor); +} + +} // namespace webf \ No newline at end of file diff --git a/bridge/core/dom/child_list_mutation_scope.h b/bridge/core/dom/child_list_mutation_scope.h new file mode 100644 index 0000000000..31ad5b33c0 --- /dev/null +++ b/bridge/core/dom/child_list_mutation_scope.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#ifndef WEBF_CORE_DOM_CHILD_LIST_MUTATION_SCOPE_H_ +#define WEBF_CORE_DOM_CHILD_LIST_MUTATION_SCOPE_H_ + +#include "document.h" +#include "foundation/macros.h" +#include "mutation_observer_interest_group.h" +#include "static_node_list.h" + +namespace webf { + +// ChildListMutationAccumulator is not meant to be used directly; +// ChildListMutationScope is the public interface. +// +// One ChildListMutationAccumulator for a given Node is shared between all the +// active ChildListMutationScopes for that Node. Once the last +// ChildListMutationScope is destructed the accumulator enqueues a mutation +// record for the recorded mutations and the accumulator can be garbage +// collected. +class ChildListMutationAccumulator final { + public: + static std::shared_ptr GetOrCreate(Node&); + + ChildListMutationAccumulator(Node*, const std::shared_ptr& observers); + ~ChildListMutationAccumulator(); + + void ChildAdded(Node&); + void WillRemoveChild(Node&); + + bool HasObservers() const { return observers_ != nullptr; } + + // Register and unregister mutation scopes that are using this mutation + // accumulator. + void EnterMutationScope() { mutation_scopes_++; } + void LeaveMutationScope(); + + void Trace(GCVisitor*) const; + + private: + void EnqueueMutationRecord(); + bool IsEmpty(); + bool IsAddedNodeInOrder(Node&); + bool IsRemovedNodeInOrder(Node&); + + std::vector> removed_nodes_; + std::vector> added_nodes_; + Member target_; + Member previous_sibling_; + Member next_sibling_; + Member last_added_; + + std::shared_ptr observers_; + + unsigned mutation_scopes_; +}; + +class ChildListMutationScope final { + WEBF_STACK_ALLOCATED(); + + public: + explicit ChildListMutationScope(Node& target) { + if (!target.IsDocumentNode() && target.ownerDocument()->HasMutationObserversOfType(kMutationTypeChildList)) { + accumulator_ = ChildListMutationAccumulator::GetOrCreate(target); + // Register another user of the accumulator. + accumulator_->EnterMutationScope(); + } + } + ChildListMutationScope(const ChildListMutationScope&) = delete; + ChildListMutationScope& operator=(const ChildListMutationScope&) = delete; + + ~ChildListMutationScope() { + if (accumulator_) { + // Unregister a user of the accumulator. If this is the last user + // the accumulator will enqueue a mutation record for the mutations. + accumulator_->LeaveMutationScope(); + } + } + + void ChildAdded(Node& child) { + if (accumulator_ && accumulator_->HasObservers()) + accumulator_->ChildAdded(child); + } + + void WillRemoveChild(Node& child) { + if (accumulator_ && accumulator_->HasObservers()) + accumulator_->WillRemoveChild(child); + } + + private: + std::shared_ptr accumulator_ = nullptr; +}; + +} // namespace webf + +#endif // WEBF_CORE_DOM_CHILD_LIST_MUTATION_SCOPE_H_ diff --git a/bridge/core/dom/collection_items_cache.h b/bridge/core/dom/collection_items_cache.h index 1814e03c55..2f1c1d0fc7 100644 --- a/bridge/core/dom/collection_items_cache.h +++ b/bridge/core/dom/collection_items_cache.h @@ -72,9 +72,6 @@ template void CollectionItemsCache::Invalidate() { Base::Invalidate(); if (list_valid_) { - for (auto& item : cached_list_) { - item.Clear(); - } cached_list_.clear(); list_valid_ = false; } @@ -88,7 +85,8 @@ unsigned CollectionItemsCache::NodeCount(const Collection& NodeType* current_node = collection.TraverseToFirst(); unsigned current_index = 0; while (current_node) { - cached_list_.push_back(current_node); + cached_list_.emplace_back(current_node); + // cached_list_.push_back(current_node); current_node = collection.TraverseForwardToOffset(current_index + 1, *current_node, current_index); } diff --git a/bridge/core/dom/container_node.cc b/bridge/core/dom/container_node.cc index b5bd607e64..fac64099c4 100644 --- a/bridge/core/dom/container_node.cc +++ b/bridge/core/dom/container_node.cc @@ -29,6 +29,7 @@ #include "container_node.h" #include "bindings/qjs/cppgc/garbage_collected.h" #include "bindings/qjs/cppgc/gc_visitor.h" +#include "child_list_mutation_scope.h" #include "child_node_list.h" #include "core/html/html_all_collection.h" #include "document.h" @@ -168,7 +169,10 @@ Node* ContainerNode::InsertBefore(Node* new_child, Node* ref_child, ExceptionSta // 5. Insert node into parent before reference child. NodeVector post_insertion_notification_targets; - { InsertNodeVector(targets, ref_child, AdoptAndInsertBefore(), &post_insertion_notification_targets); } + { + ChildListMutationScope scope{*this}; + InsertNodeVector(targets, ref_child, AdoptAndInsertBefore(), &post_insertion_notification_targets); + } DidInsertNodeVector(targets, ref_child, post_insertion_notification_targets); return new_child; } @@ -207,6 +211,7 @@ Node* ContainerNode::ReplaceChild(Node* new_child, Node* old_child, ExceptionSta NodeVector post_insertion_notification_targets; post_insertion_notification_targets.reserve(kInitialNodeVectorSize); { + ChildListMutationScope scope{*this}; // 9. Let previousSibling be child’s previous sibling. // 11. Let removedNodes be the empty list. // 15. Queue a mutation record of "childList" for target parent with @@ -260,6 +265,8 @@ Node* ContainerNode::RemoveChild(Node* old_child, ExceptionState& exception_stat return nullptr; } + WillRemoveChild(*child); + { Node* prev = child->previousSibling(); Node* next = child->nextSibling(); @@ -285,7 +292,10 @@ Node* ContainerNode::AppendChild(Node* new_child, ExceptionState& exception_stat NodeVector post_insertion_notification_targets; post_insertion_notification_targets.reserve(kInitialNodeVectorSize); - { InsertNodeVector(targets, nullptr, AdoptAndAppendChild(), &post_insertion_notification_targets); } + { + ChildListMutationScope mutation_scope(*this); + InsertNodeVector(targets, nullptr, AdoptAndAppendChild(), &post_insertion_notification_targets); + } DidInsertNodeVector(targets, nullptr, post_insertion_notification_targets); return new_child; } @@ -294,6 +304,29 @@ Node* ContainerNode::AppendChild(Node* new_child) { return AppendChild(new_child, ASSERT_NO_EXCEPTION()); } +void ContainerNode::WillRemoveChild(Node& child) { + assert(child.parentNode() == this); + ChildListMutationScope(*this).WillRemoveChild(child); + child.NotifyMutationObserversNodeWillDetach(); + if (&GetDocument() != &child.GetDocument()) { + // |child| was moved to another document by the DOM mutation event handler. + return; + } +} + +void ContainerNode::WillRemoveChildren() { + NodeVector children; + GetChildNodes(*this, children); + + ChildListMutationScope mutation(*this); + for (const auto& node : children) { + assert(node); + Node& child = *node; + mutation.WillRemoveChild(child); + child.NotifyMutationObserversNodeWillDetach(); + } +} + bool ContainerNode::EnsurePreInsertionValidity(const Node& new_child, const Node* next, const Node* old_child, @@ -350,6 +383,10 @@ void ContainerNode::RemoveChildren() { if (!first_child_) return; + // Do any prep work needed before actually starting to detach + // and remove... e.g. stop loading frames, fire unload events. + WillRemoveChildren(); + bool has_element_child = false; while (Node* child = first_child_) { @@ -415,6 +452,7 @@ void ContainerNode::InsertNodeVector(const NodeVector& targets, assert(!target_node->parentNode()); Node& child = *target_node; mutator(*this, child, next); + ChildListMutationScope(*this).ChildAdded(child); NotifyNodeInsertedInternal(child); } } diff --git a/bridge/core/dom/container_node.h b/bridge/core/dom/container_node.h index 80c8762bb8..136d2a77c3 100644 --- a/bridge/core/dom/container_node.h +++ b/bridge/core/dom/container_node.h @@ -41,6 +41,8 @@ class ContainerNode : public Node { Node* RemoveChild(Node* child, ExceptionState&); Node* AppendChild(Node* new_child, ExceptionState&); Node* AppendChild(Node* new_child); + void WillRemoveChildren(); + void WillRemoveChild(Node& child); bool EnsurePreInsertionValidity(const Node& new_child, const Node* next, const Node* old_child, diff --git a/bridge/core/dom/document.cc b/bridge/core/dom/document.cc index 0bede915ca..64f10e369a 100644 --- a/bridge/core/dom/document.cc +++ b/bridge/core/dom/document.cc @@ -372,6 +372,8 @@ HTMLHeadElement* Document::head() const { return Traversal::FirstChild(*de); } +void Document::NodeWillBeRemoved(Node& node) {} + uint32_t Document::RequestAnimationFrame(const std::shared_ptr& callback, ExceptionState& exception_state) { return script_animation_controller_.RegisterFrameCallback(callback, exception_state); diff --git a/bridge/core/dom/document.h b/bridge/core/dom/document.h index 99fe3911b9..88c5b0cfa2 100644 --- a/bridge/core/dom/document.h +++ b/bridge/core/dom/document.h @@ -100,6 +100,13 @@ class Document : public ContainerNode, public TreeScope { ScriptValue location() const; + bool HasMutationObserversOfType(MutationType type) const { return mutation_observer_types_ & type; } + bool HasMutationObservers() const { return mutation_observer_types_; } + void AddMutationObserverTypes(MutationType types) { mutation_observer_types_ |= types; } + + // nodeWillBeRemoved is only safe when removing one node at a time. + void NodeWillBeRemoved(Node&); + void IncrementNodeCount() { node_count_++; } void DecrementNodeCount() { assert(node_count_ > 0); @@ -123,6 +130,7 @@ class Document : public ContainerNode, public TreeScope { private: int node_count_{0}; ScriptAnimationController script_animation_controller_; + MutationObserverOptions mutation_observer_types_; }; template <> diff --git a/bridge/core/dom/dom_token_list.cc b/bridge/core/dom/dom_token_list.cc index b9218f42f0..bd6b8ea537 100644 --- a/bridge/core/dom/dom_token_list.cc +++ b/bridge/core/dom/dom_token_list.cc @@ -39,11 +39,14 @@ bool CheckEmptyToken(JSContext* ctx, const AtomicString& token, ExceptionState& } bool CheckTokenWithWhitespace(JSContext* ctx, const AtomicString& token, ExceptionState& exception_state) { - if (token.Is8Bit() && token.Find(IsHTMLSpace) == -1) { - return true; - } - if (token.Find(IsHTMLSpace) == -1) { - return true; + if (token.Is8Bit()) { + if (token.Find(IsHTMLSpace) == -1) { + return true; + } + } else { + if (token.Find(IsHTMLSpace) == -1) { + return true; + } } exception_state.ThrowException(ctx, ErrorType::TypeError, @@ -261,7 +264,8 @@ void DOMTokenList::UpdateWithTokenSet(const SpaceSplitString& token_set) { } AtomicString DOMTokenList::value() const { - return element_->getAttribute(attribute_name_, ASSERT_NO_EXCEPTION()); + AtomicString result = element_->getAttribute(attribute_name_, ASSERT_NO_EXCEPTION()); + return result == AtomicString::Null() ? AtomicString::Empty() : result; } void DOMTokenList::setValue(const AtomicString& new_value, ExceptionState& exception_state) { diff --git a/bridge/core/dom/element.cc b/bridge/core/dom/element.cc index b82c2dddd5..51a4b9b7b9 100644 --- a/bridge/core/dom/element.cc +++ b/bridge/core/dom/element.cc @@ -9,6 +9,7 @@ #include "bindings/qjs/script_promise.h" #include "bindings/qjs/script_promise_resolver.h" #include "built_in_string.h" +#include "child_list_mutation_scope.h" #include "comment.h" #include "core/dom/document_fragment.h" #include "core/fileapi/blob.h" @@ -18,6 +19,7 @@ #include "element_namespace_uris.h" #include "foundation/native_value_converter.h" #include "html_element_type_helper.h" +#include "mutation_observer_interest_group.h" #include "qjs_element.h" #include "text.h" @@ -34,11 +36,9 @@ Element::Element(const AtomicString& namespace_uri, buffer->addCommand(UICommand::kCreateElement, std::move(local_name.ToNativeString(ctx())), (void*)bindingObject(), nullptr); } else if (namespace_uri == element_namespace_uris::ksvg) { - // TODO: SVG element buffer->addCommand(UICommand::kCreateSVGElement, std::move(local_name.ToNativeString(ctx())), (void*)bindingObject(), nullptr); } else { - // TODO: Unknown namespace uri buffer->addCommand(UICommand::kCreateElementNS, std::move(local_name.ToNativeString(ctx())), (void*)bindingObject(), namespace_uri.ToNativeString(ctx()).release()); } @@ -65,18 +65,8 @@ void Element::setAttribute(const AtomicString& name, const AtomicString& value) } void Element::setAttribute(const AtomicString& name, const AtomicString& value, ExceptionState& exception_state) { - if (EnsureElementAttributes().hasAttribute(name, exception_state)) { - AtomicString&& oldAttribute = EnsureElementAttributes().getAttribute(name, exception_state); - if (!EnsureElementAttributes().setAttribute(name, value, exception_state)) { - return; - }; - _didModifyAttribute(name, oldAttribute, value); - } else { - if (!EnsureElementAttributes().setAttribute(name, value, exception_state)) { - return; - }; - _didModifyAttribute(name, AtomicString::Empty(), value); - } + SynchronizeAttribute(name); + SetAttributeInternal(name, value, AttributeModificationReason::kDirectly, exception_state); } void Element::removeAttribute(const AtomicString& name, ExceptionState& exception_state) { @@ -327,7 +317,7 @@ const AtomicString Element::getUppercasedQualifiedName() const { return name; } -ElementData& Element::EnsureElementData() const { +ElementData& Element::EnsureElementData() { if (element_data_ == nullptr) { element_data_ = std::make_unique(); } @@ -418,6 +408,109 @@ ScriptPromise Element::toBlob(double device_pixel_ratio, ExceptionState& excepti return resolver->Promise(); } +void Element::DidAddAttribute(const AtomicString& name, const AtomicString& value) {} + +void Element::WillModifyAttribute(const AtomicString& name, + const AtomicString& old_value, + const AtomicString& new_value) { + if (std::shared_ptr recipients = + MutationObserverInterestGroup::CreateForAttributesMutation(*this, name)) { + recipients->EnqueueMutationRecord(MutationRecord::CreateAttributes(this, name, AtomicString::Null(), old_value)); + } +} + +void Element::DidModifyAttribute(const AtomicString& name, + const AtomicString& old_value, + const AtomicString& new_value, + AttributeModificationReason reason) { + AttributeChanged(AttributeModificationParams(name, old_value, new_value, reason)); +} + +void Element::DidRemoveAttribute(const AtomicString& name, const AtomicString& old_value) {} + +void Element::SynchronizeStyleAttributeInternal() { + assert(IsStyledElement()); + assert(HasElementData()); + assert(GetElementData()->style_attribute_is_dirty()); + GetElementData()->SetStyleAttributeIsDirty(false); + + InlineCssStyleDeclaration* inline_style = style(); + SetAttributeInternal(html_names::kStyleAttr, inline_style->cssText(), + AttributeModificationReason::kBySynchronizationOfLazyAttribute, ASSERT_NO_EXCEPTION()); +} + +void Element::SetAttributeInternal(const webf::AtomicString& name, + const webf::AtomicString& value, + AttributeModificationReason reason, + ExceptionState& exception_state) { + if (EnsureElementAttributes().hasAttribute(name, exception_state)) { + AtomicString&& oldAttribute = EnsureElementAttributes().getAttribute(name, exception_state); + + if (reason != AttributeModificationReason::kBySynchronizationOfLazyAttribute) { + WillModifyAttribute(name, oldAttribute, value); + } + + if (!EnsureElementAttributes().setAttribute(name, value, exception_state)) { + return; + } + if (reason != AttributeModificationReason::kBySynchronizationOfLazyAttribute) { + DidModifyAttribute(name, oldAttribute, value, AttributeModificationReason::kDirectly); + } + } else { + if (reason != AttributeModificationReason::kBySynchronizationOfLazyAttribute) { + WillModifyAttribute(name, AtomicString::Null(), value); + } + + if (!EnsureElementAttributes().setAttribute(name, value, exception_state)) { + return; + } + + if (reason != AttributeModificationReason::kBySynchronizationOfLazyAttribute) { + DidModifyAttribute(name, AtomicString::Null(), value, AttributeModificationReason::kDirectly); + } + } +} + +void Element::SynchronizeAttribute(const AtomicString& name) { + if (!cssom_wrapper_) + return; + + if (UNLIKELY(name == html_names::kStyleAttr && EnsureElementData().style_attribute_is_dirty())) { + assert(IsStyledElement()); + SynchronizeStyleAttributeInternal(); + return; + } +} + +void Element::InvalidateStyleAttribute() { + EnsureElementData().SetStyleAttributeIsDirty(true); +} + +void Element::AttributeChanged(const AttributeModificationParams& params) { + const AtomicString& name = params.name; + + if (IsStyledElement()) { + if (name == html_names::kStyleAttr) { + StyleAttributeChanged(params.new_value, params.reason); + } + } +} + +void Element::StyleAttributeChanged(const AtomicString& new_style_string, + AttributeModificationReason modification_reason) { + assert(IsStyledElement()); + + if (new_style_string.IsNull() && cssom_wrapper_ != nullptr) { + EnsureCSSStyleDeclaration().Clear(); + } else { + SetInlineStyleFromString(new_style_string); + } +} + +void Element::SetInlineStyleFromString(const webf::AtomicString& new_style_string) { + EnsureCSSStyleDeclaration().SetCSSTextInternal(new_style_string); +} + std::string Element::outerHTML() { std::string tagname = local_name_.ToStdString(ctx()); std::string s = "<" + tagname; @@ -469,6 +562,7 @@ std::string Element::innerHTML() { void Element::setInnerHTML(const AtomicString& value, ExceptionState& exception_state) { auto html = value.ToStdString(ctx()); + ChildListMutationScope scope{*this}; if (auto* template_element = DynamicTo(this)) { HTMLParser::parseHTMLFragment(html.c_str(), html.size(), template_element->content()); } else { @@ -486,8 +580,6 @@ void Element::_notifyNodeInsert(Node* insertNode){ void Element::_notifyChildInsert() {} -void Element::_didModifyAttribute(const AtomicString& name, const AtomicString& oldId, const AtomicString& newId) {} - void Element::_beforeUpdateId(JSValue oldIdValue, JSValue newIdValue) {} Node::NodeType Element::nodeType() const { diff --git a/bridge/core/dom/element.h b/bridge/core/dom/element.h index 4c65f3439c..84cbded132 100644 --- a/bridge/core/dom/element.h +++ b/bridge/core/dom/element.h @@ -22,6 +22,31 @@ class Element : public ContainerNode { public: using ImplType = Element*; + + enum class AttributeModificationReason { + kDirectly, + kByParser, + kByCloning, + kByMoveToNewDocument, + kBySynchronizationOfLazyAttribute + }; + + struct AttributeModificationParams { + WEBF_STACK_ALLOCATED(); + + public: + AttributeModificationParams(const AtomicString& qname, + const AtomicString& old_value, + const AtomicString& new_value, + AttributeModificationReason reason) + : name(qname), old_value(old_value), new_value(new_value), reason(reason) {} + + const AtomicString& name; + const AtomicString& old_value; + const AtomicString& new_value; + const AttributeModificationReason reason; + }; + Element(const AtomicString& namespace_uri, const AtomicString& local_name, const AtomicString& prefix, @@ -54,6 +79,22 @@ class Element : public ContainerNode { ScriptPromise toBlob(double device_pixel_ratio, ExceptionState& exception_state); ScriptPromise toBlob(ExceptionState& exception_state); + void DidAddAttribute(const AtomicString&, const AtomicString&); + void WillModifyAttribute(const AtomicString&, const AtomicString& old_value, const AtomicString& new_value); + void DidModifyAttribute(const AtomicString&, + const AtomicString& old_value, + const AtomicString& new_value, + AttributeModificationReason reason); + void DidRemoveAttribute(const AtomicString&, const AtomicString& old_value); + + void SynchronizeStyleAttributeInternal(); + void SynchronizeAttribute(const AtomicString& name); + + void InvalidateStyleAttribute(); + void AttributeChanged(const AttributeModificationParams& params); + void StyleAttributeChanged(const AtomicString& new_style_string, AttributeModificationReason modification_reason); + void SetInlineStyleFromString(const AtomicString&); + std::string outerHTML(); std::string innerHTML(); void setInnerHTML(const AtomicString& value, ExceptionState& exception_state); @@ -103,10 +144,16 @@ class Element : public ContainerNode { void Trace(GCVisitor* visitor) const override; protected: + void SetAttributeInternal(const AtomicString&, + const AtomicString& value, + AttributeModificationReason reason, + ExceptionState& exception_state); + const ElementData* GetElementData() const { return element_data_.get(); } + bool HasElementData() const { return element_data_ != nullptr; } const AtomicString& getQualifiedName() const { return local_name_; } const AtomicString getUppercasedQualifiedName() const; - ElementData& EnsureElementData() const; + ElementData& EnsureElementData(); AtomicString namespace_uri_ = AtomicString::Null(); AtomicString prefix_ = AtomicString::Null(); AtomicString local_name_ = AtomicString::Empty(); @@ -121,7 +168,6 @@ class Element : public ContainerNode { void _notifyChildRemoved(); void _notifyNodeInsert(Node* insertNode); void _notifyChildInsert(); - void _didModifyAttribute(const AtomicString& name, const AtomicString& oldId, const AtomicString& newId); void _beforeUpdateId(JSValue oldIdValue, JSValue newIdValue); mutable std::unique_ptr element_data_; diff --git a/bridge/core/dom/element_data.h b/bridge/core/dom/element_data.h index 2b24a289e8..59384ed588 100644 --- a/bridge/core/dom/element_data.h +++ b/bridge/core/dom/element_data.h @@ -25,10 +25,14 @@ class ElementData { DOMStringMap* DataSet() const; void SetDataSet(DOMStringMap* data_set); + bool style_attribute_is_dirty() const { return style_attribute_is_dirty_; } + void SetStyleAttributeIsDirty(bool value) const { style_attribute_is_dirty_ = value; } + private: Member class_lists_; Member data_set_; AtomicString class_; + mutable bool style_attribute_is_dirty_; }; } // namespace webf diff --git a/bridge/core/dom/frame_request_callback_collection.cc b/bridge/core/dom/frame_request_callback_collection.cc index e84f95bbab..05bd267b82 100644 --- a/bridge/core/dom/frame_request_callback_collection.cc +++ b/bridge/core/dom/frame_request_callback_collection.cc @@ -27,7 +27,7 @@ void FrameCallback::Fire(double highResTimeStamp) { ScriptValue return_value = callback_->Invoke(ctx, ScriptValue::Empty(ctx), 1, arguments); - context_->DrainPendingPromiseJobs(); + context_->DrainMicrotasks(); if (return_value.IsException()) { context_->HandleException(&return_value); } diff --git a/bridge/core/dom/legacy/element_attributes.cc b/bridge/core/dom/legacy/element_attributes.cc index dc5847a437..09be015ee5 100644 --- a/bridge/core/dom/legacy/element_attributes.cc +++ b/bridge/core/dom/legacy/element_attributes.cc @@ -5,9 +5,10 @@ #include "element_attributes.h" #include "bindings/qjs/exception_state.h" -#include "built_in_string.h" #include "core/dom/element.h" +#include "core/html/custom/widget_element.h" #include "foundation/native_value_converter.h" +#include "html_names.h" namespace webf { @@ -23,20 +24,22 @@ ElementAttributes::ElementAttributes(Element* element) : ScriptWrappable(element AtomicString ElementAttributes::getAttribute(const AtomicString& name, ExceptionState& exception_state) { bool numberIndex = IsNumberIndex(name.ToStringView()); - if (numberIndex || attributes_.count(name) == 0) { - return AtomicString::Empty(); + if (numberIndex) { + return AtomicString::Null(); } - AtomicString value = attributes_[name]; - - // Fallback to directly FFI access to dart. - if (value.IsEmpty()) { - NativeValue dart_result = element_->GetBindingProperty(name, exception_state); - if (dart_result.tag == NativeTag::TAG_STRING) { - return NativeValueConverter::FromNativeValue(element_->ctx(), std::move(dart_result)); + if (attributes_.count(name) == 0) { + if (element_->IsWidgetElement()) { + // Fallback to directly FFI access to dart. + NativeValue dart_result = element_->GetBindingProperty(name, exception_state); + if (dart_result.tag == NativeTag::TAG_STRING) { + return NativeValueConverter::FromNativeValue(element_->ctx(), std::move(dart_result)); + } } + return AtomicString::Null(); } + AtomicString value = attributes_[name]; return value; } @@ -52,8 +55,14 @@ bool ElementAttributes::setAttribute(const AtomicString& name, return false; } + AtomicString existing_attribute = attributes_[name]; + attributes_[name] = value; + // Style attribute will be parsed and separated into multiple setStyle command. + if (name == html_names::kStyleAttr) + return true; + std::unique_ptr args_01 = value.ToNativeString(ctx()); std::unique_ptr args_02 = name.ToNativeString(ctx()); @@ -70,10 +79,24 @@ bool ElementAttributes::hasAttribute(const AtomicString& name, ExceptionState& e return false; } - return attributes_.count(name) > 0; + bool has_attribute = attributes_.count(name) > 0; + + if (!has_attribute && element_->IsWidgetElement()) { + // Fallback to directly FFI access to dart. + NativeValue dart_result = element_->GetBindingProperty(name, exception_state); + return dart_result.tag != NativeTag::TAG_NULL; + } + + return has_attribute; } void ElementAttributes::removeAttribute(const AtomicString& name, ExceptionState& exception_state) { + if (!hasAttribute(name, exception_state)) + return; + + AtomicString old_value = getAttribute(name, exception_state); + element_->WillModifyAttribute(name, old_value, AtomicString::Null()); + attributes_.erase(name); std::unique_ptr args_01 = name.ToNativeString(ctx()); diff --git a/bridge/core/dom/mutation_observer.cc b/bridge/core/dom/mutation_observer.cc new file mode 100644 index 0000000000..42e1ac4e01 --- /dev/null +++ b/bridge/core/dom/mutation_observer.cc @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#include "mutation_observer.h" +#include +#include "bindings/qjs/converter_impl.h" +#include "mutation_observer_registration.h" +#include "mutation_record.h" +#include "node.h" + +namespace webf { + +class MutationObserverAgent; + +static unsigned g_observer_priority = 0; +static thread_local std::unordered_map> agent_map_; + +class MutationObserverAgent { + public: + MutationObserverAgent() = delete; + explicit MutationObserverAgent(ExecutingContext* context) : context_(context){}; + + static std::shared_ptr From(ExecutingContext* context) { + if (agent_map_.count(context) == 0) { + agent_map_[context] = std::make_shared(context); + } + return agent_map_[context]; + } + + void ActivateObserver(MutationObserver* observer) { + if (!isContextValid(context_->contextId())) + return; + + EnsureEnqueueMicrotask(); + active_mutation_observers_.insert(observer); + } + + private: + void DeliverMutations() { + MemberMutationScope scopes{context_}; + // These steps are defined in DOM Standard's "notify mutation observers". + // https://dom.spec.whatwg.org/#notify-mutation-observers + MutationObserverVector observers(active_mutation_observers_.begin(), active_mutation_observers_.end()); + active_mutation_observers_.clear(); + std::sort(observers.begin(), observers.end(), MutationObserver::ObserverLessThan()); + for (const auto& observer : observers) + observer->Deliver(); + } + + void EnsureEnqueueMicrotask() { + if (active_mutation_observers_.empty() && context_->IsContextValid()) { + context_->EnqueueMicrotask( + [](void* p) { + auto* agent = static_cast(p); + agent->DeliverMutations(); + }, + this); + } + } + + MutationObserverSet active_mutation_observers_; + ExecutingContext* context_; +}; + +static void ActivateObserver(MutationObserver* observer) { + if (!observer->GetExecutingContext()) + return; + + ExecutingContext* context = observer->GetExecutingContext(); + auto agent = MutationObserverAgent::From(context); + agent->ActivateObserver(observer); +} + +MutationObserver* MutationObserver::Create(ExecutingContext* context, + const std::shared_ptr& function, + ExceptionState& exception_state) { + return MakeGarbageCollected(context, function); +} + +MutationObserver::MutationObserver(ExecutingContext* context, const std::shared_ptr& function) + : ScriptWrappable(context->ctx()), function_(function) { + priority_ = g_observer_priority++; +} + +MutationObserver::~MutationObserver() {} + +void MutationObserver::observe(Node* node, + const std::shared_ptr& observer_init, + ExceptionState& exception_state) { + assert(node != nullptr); + + MutationObserverOptions options = 0; + + if (observer_init->hasAttributeOldValue() && observer_init->attributeOldValue()) + options |= kAttributeOldValue; + + std::set attribute_filter; + if (observer_init->hasAttributeFilter()) { + for (const auto& name : observer_init->attributeFilter()) + attribute_filter.insert(AtomicString(name)); + options |= kAttributeFilter; + } + + bool attributes = observer_init->hasAttributes() && observer_init->attributes(); + if (attributes || (!observer_init->hasAttributes() && + (observer_init->hasAttributeOldValue() || observer_init->hasAttributeFilter()))) + options |= kMutationTypeAttributes; + + if (observer_init->hasCharacterDataOldValue() && observer_init->characterDataOldValue()) + options |= kCharacterDataOldValue; + + bool character_data = observer_init->hasCharacterData() && observer_init->characterData(); + if (character_data || (!observer_init->hasCharacterData() && observer_init->hasCharacterDataOldValue())) + options |= kMutationTypeCharacterData; + + if (observer_init->hasChildList() && observer_init->childList()) + options |= kMutationTypeChildList; + + if (observer_init->hasSubtree() && observer_init->subtree()) + options |= kSubtree; + + if (!(options & kMutationTypeAttributes)) { + if (options & kAttributeOldValue) { + exception_state.ThrowException(ctx(), ErrorType::TypeError, + "The options object may only set 'attributeOldValue' to true when " + "'attributes' is true or not present."); + return; + } + if (options & kAttributeFilter) { + exception_state.ThrowException(ctx(), ErrorType::TypeError, + "The options object may only set 'attributeFilter' when 'attributes' " + "is true or not present."); + return; + } + } + if (!((options & kMutationTypeCharacterData) || !(options & kCharacterDataOldValue))) { + exception_state.ThrowException(ctx(), ErrorType::TypeError, + "The options object may only set 'characterDataOldValue' to true when " + "'characterData' is true or not present."); + return; + } + + if (!(options & kMutationTypeAll)) { + exception_state.ThrowException(ctx(), ErrorType::TypeError, + "The options object must set at least one of 'attributes', " + "'characterData', or 'childList' to true."); + return; + } + + node->RegisterMutationObserver(*this, options, attribute_filter); +} + +void MutationObserver::observe(Node* node, ExceptionState& exception_state) { + observe(node, MutationObserverInit::Create(), exception_state); +} + +MutationRecordVector MutationObserver::takeRecords(ExceptionState& exception_state) { + MutationRecordVector records; + std::swap(records_, records); + return records; +} + +void MutationObserver::disconnect(ExceptionState& exception_state) { + records_.clear(); + MutationObserverRegistrationSet registrations(registrations_); + for (auto& registration : registrations) { + // The registration may be already unregistered while iteration. + // Only call unregister if it is still in the original set. + if (registrations_.count(registration) > 0) + registration->Unregister(); + } + assert(registrations_.empty()); +} + +void MutationObserver::ObservationStarted(MutationObserverRegistration* registration) { + assert(registrations_.count(registration) == 0); + registrations_.insert(registration); +} + +void MutationObserver::ObservationEnded(MutationObserverRegistration* registration) { + assert(registrations_.count(registration) > 0); + registrations_.erase(registration); +} + +void MutationObserver::EnqueueMutationRecord(MutationRecord* mutation) { + records_.emplace_back(mutation); + ActivateObserver(this); +} + +void MutationObserver::Deliver() { + if (!GetExecutingContext() || !GetExecutingContext()->IsContextValid()) + return; + + // Calling ClearTransientRegistrations() can modify registrations_, so it's + // necessary to make a copy of the transient registrations before operating on + // them. + std::vector> transient_registrations; + for (auto& registration : registrations_) { + if (registration->HasTransientRegistrations()) + transient_registrations.push_back(registration); + } + for (const auto& registration : transient_registrations) + registration->ClearTransientRegistrations(); + + if (records_.empty()) + return; + + MutationRecordVector records; + swap(records_, records); + + assert(function_ != nullptr); + JSValue v = Converter>::ToValue(ctx(), records); + ScriptValue arguments[] = {ScriptValue(ctx(), v), ToValue()}; + + JS_FreeValue(ctx(), v); + function_->Invoke(ctx(), ToValue(), 2, arguments); +} + +void MutationObserver::SetHasTransientRegistration() { + ActivateObserver(this); +} + +std::set> MutationObserver::GetObservedNodes() const { + std::set> observed_nodes; + for (const auto& registration : registrations_) + registration->AddRegistrationNodesToSet(observed_nodes); + return observed_nodes; +} + +void MutationObserver::Trace(GCVisitor* visitor) const { + for (auto& record : records_) { + visitor->TraceMember(record); + } + + for (auto& re : registrations_) { + visitor->TraceMember(re); + } + function_->Trace(visitor); +} + +} // namespace webf \ No newline at end of file diff --git a/bridge/core/dom/mutation_observer.d.ts b/bridge/core/dom/mutation_observer.d.ts new file mode 100644 index 0000000000..7ca13d527d --- /dev/null +++ b/bridge/core/dom/mutation_observer.d.ts @@ -0,0 +1,10 @@ +import {Node} from "./node"; +import {MutationObserverInit} from "./mutation_observer_init"; +import {MutationRecord} from "./mutation_record"; + +interface MutationObserver { + new(mutationCallback: Function): MutationObserver; + observe(targe: Node, options?: MutationObserverInit): void; + disconnect(): void; + takeRecords(): MutationRecord[]; +} \ No newline at end of file diff --git a/bridge/core/dom/mutation_observer.h b/bridge/core/dom/mutation_observer.h new file mode 100644 index 0000000000..bc3ee3a7b3 --- /dev/null +++ b/bridge/core/dom/mutation_observer.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#ifndef WEBF_MUTATION_OBSERVER_H +#define WEBF_MUTATION_OBSERVER_H + +#include "bindings/qjs/cppgc/member.h" +#include "bindings/qjs/exception_state.h" +#include "bindings/qjs/script_wrappable.h" +#include "mutation_record.h" +#include "qjs_mutation_observer_init.h" + +namespace webf { + +class Node; +class MutationObserver; +class MutationObserverInit; +class MutationObserverRegistration; +class MutationRecord; + +using MutationObserverSet = std::set>; +using MutationObserverRegistrationSet = std::set>; +using MutationObserverRegistrationVector = std::vector>; +using MutationObserverVector = std::vector>; +using MutationRecordVector = std::vector>; + +class MutationObserver final : public ScriptWrappable { + DEFINE_WRAPPERTYPEINFO(); + + public: + enum ObservationFlags { kSubtree = 1 << 3, kAttributeFilter = 1 << 4 }; + enum DeliveryFlags { + kAttributeOldValue = 1 << 5, + kCharacterDataOldValue = 1 << 6, + }; + + struct ObserverLessThan { + bool operator()(const Member& lhs, const Member& rhs) { + return lhs->priority_ < rhs->priority_; + } + }; + + static MutationObserver* Create(ExecutingContext* context, + const std::shared_ptr& function, + ExceptionState& exception_state); + + MutationObserver(ExecutingContext*, const std::shared_ptr& function); + ~MutationObserver() override; + + void observe(Node*, const std::shared_ptr& init, ExceptionState&); + void observe(Node*, ExceptionState&); + MutationRecordVector takeRecords(ExceptionState&); + void disconnect(ExceptionState& exception_state); + void ObservationStarted(MutationObserverRegistration*); + void ObservationEnded(MutationObserverRegistration*); + void EnqueueMutationRecord(MutationRecord*); + void Deliver(); + void SetHasTransientRegistration(); + + std::set> GetObservedNodes() const; + + bool HasPendingActivity() const { return !records_.empty(); } + + void Trace(webf::GCVisitor* visitor) const override; + + private: + MutationRecordVector records_; + MutationObserverRegistrationSet registrations_; + std::shared_ptr function_; + unsigned priority_; +}; + +} // namespace webf + +#endif // WEBF_MUTATION_OBSERVER_H diff --git a/bridge/core/dom/mutation_observer_init.d.ts b/bridge/core/dom/mutation_observer_init.d.ts new file mode 100644 index 0000000000..2520221388 --- /dev/null +++ b/bridge/core/dom/mutation_observer_init.d.ts @@ -0,0 +1,12 @@ + +// @ts-ignore +@Dictionary() +export interface MutationObserverInit { + childList?: boolean; + attributes?: boolean; + characterData?: boolean; + subtree?: boolean; + attributeOldValue?: boolean; + characterDataOldValue?: boolean; + attributeFilter?: string[]; +} diff --git a/bridge/core/dom/mutation_observer_interest_group.cc b/bridge/core/dom/mutation_observer_interest_group.cc new file mode 100644 index 0000000000..498866cc02 --- /dev/null +++ b/bridge/core/dom/mutation_observer_interest_group.cc @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#include "mutation_observer_interest_group.h" +#include "bindings/qjs/cppgc/member.h" +#include "node.h" + +namespace webf { + +std::shared_ptr MutationObserverInterestGroup::CreateIfNeeded( + Node& target, + MutationType type, + MutationRecordDeliveryOptions old_value_flag, + const AtomicString* attribute_name) { + assert((type == kMutationTypeAttributes && attribute_name) || !attribute_name); + MutationObserverOptionsMap observers; + target.GetRegisteredMutationObserversOfType(observers, type, attribute_name); + if (observers.empty()) + return nullptr; + + return std::make_shared(observers, old_value_flag); +} + +MutationObserverInterestGroup::MutationObserverInterestGroup(MutationObserverOptionsMap& observers, + webf::MutationRecordDeliveryOptions old_value_flag) + : old_value_flag_(old_value_flag) { + assert(!observers.empty()); + observers_.swap(observers); +} + +MutationObserverInterestGroup::~MutationObserverInterestGroup() {} + +bool MutationObserverInterestGroup::IsOldValueRequested() { + for (auto& observer : observers_) { + if (HasOldValue(observer.second)) + return true; + } + return false; +} + +void MutationObserverInterestGroup::EnqueueMutationRecord(MutationRecord* mutation) { + MutationRecord* mutation_with_null_old_value = nullptr; + + for (auto& iter : observers_) { + MutationObserver* observer = iter.first; + if (HasOldValue(iter.second)) { + observer->EnqueueMutationRecord(mutation); + continue; + } + if (!mutation_with_null_old_value) { + if (mutation->oldValue().IsNull()) + mutation_with_null_old_value = mutation; + else + mutation_with_null_old_value = MutationRecord::CreateWithNullOldValue(mutation); + } + observer->EnqueueMutationRecord(mutation_with_null_old_value); + } +} + +void MutationObserverInterestGroup::Trace(GCVisitor* visitor) const {} + +} // namespace webf diff --git a/bridge/core/dom/mutation_observer_interest_group.h b/bridge/core/dom/mutation_observer_interest_group.h new file mode 100644 index 0000000000..2fba388a5d --- /dev/null +++ b/bridge/core/dom/mutation_observer_interest_group.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#ifndef WEBF_CORE_DOM_MUTATION_OBSERVER_INTEREST_GROUP_H_ +#define WEBF_CORE_DOM_MUTATION_OBSERVER_INTEREST_GROUP_H_ + +#include "bindings/qjs/cppgc/garbage_collected.h" +#include "document.h" +#include "mutation_record.h" + +namespace webf { + +class Node; + +class MutationObserverInterestGroup { + public: + static std::shared_ptr CreateForChildListMutation(Node& target) { + if (!target.GetDocument().HasMutationObserversOfType(kMutationTypeChildList)) + return nullptr; + + MutationRecordDeliveryOptions old_value_flag = 0; + return CreateIfNeeded(target, kMutationTypeChildList, old_value_flag); + } + + static std::shared_ptr CreateForCharacterDataMutation(Node& target) { + if (!target.GetDocument().HasMutationObserversOfType(kMutationTypeCharacterData)) + return nullptr; + + return CreateIfNeeded(target, kMutationTypeCharacterData, MutationObserver::kCharacterDataOldValue); + } + + static std::shared_ptr CreateForAttributesMutation( + Node& target, + const AtomicString& attribute_name) { + if (!target.GetDocument().HasMutationObserversOfType(kMutationTypeAttributes)) + return nullptr; + + return CreateIfNeeded(target, kMutationTypeAttributes, MutationObserver::kAttributeOldValue, &attribute_name); + } + + MutationObserverInterestGroup(MutationObserverOptionsMap& observers, MutationRecordDeliveryOptions old_value_flag); + ~MutationObserverInterestGroup(); + + bool IsOldValueRequested(); + void EnqueueMutationRecord(MutationRecord*); + + void Trace(GCVisitor*) const; + + private: + static std::shared_ptr CreateIfNeeded(Node& target, + MutationType, + MutationRecordDeliveryOptions old_value_flag, + const AtomicString* attribute_name = nullptr); + + bool HasOldValue(MutationRecordDeliveryOptions options) { return options & old_value_flag_; } + + MutationObserverOptionsMap observers_; + MutationRecordDeliveryOptions old_value_flag_; +}; + +} // namespace webf + +#endif // WEBF_CORE_DOM_MUTATION_OBSERVER_INTEREST_GROUP_H_ diff --git a/bridge/core/dom/mutation_observer_options.h b/bridge/core/dom/mutation_observer_options.h new file mode 100644 index 0000000000..50eb0296b5 --- /dev/null +++ b/bridge/core/dom/mutation_observer_options.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +namespace webf { + +using MutationObserverOptions = unsigned char; +using MutationRecordDeliveryOptions = unsigned char; + +// MutationType represents lower three bits of MutationObserverOptions. +// It doesn't use |enum class| because we'd like to do bitwise operations. +enum MutationType { + kMutationTypeChildList = 1 << 0, + kMutationTypeAttributes = 1 << 1, + kMutationTypeCharacterData = 1 << 2, + + kMutationTypeAll = kMutationTypeChildList | kMutationTypeAttributes | kMutationTypeCharacterData +}; + +} // namespace webf \ No newline at end of file diff --git a/bridge/core/dom/mutation_observer_registration.cc b/bridge/core/dom/mutation_observer_registration.cc new file mode 100644 index 0000000000..512d8e875e --- /dev/null +++ b/bridge/core/dom/mutation_observer_registration.cc @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#include "mutation_observer_registration.h" +#include "mutation_observer.h" +#include "node.h" + +namespace webf { + +MutationObserverRegistration::MutationObserverRegistration(MutationObserver& observer, + Node* registration_node, + MutationObserverOptions options, + const std::set& attribute_filter) + : observer_(&observer), + registration_node_(registration_node), + options_(options), + attribute_filter_(attribute_filter), + ScriptWrappable(observer.ctx()) {} + +MutationObserverRegistration::~MutationObserverRegistration() {} + +void MutationObserverRegistration::Dispose() { + ClearTransientRegistrations(); + observer_->ObservationEnded(this); +} + +void MutationObserverRegistration::ResetObservation(MutationObserverOptions options, + const std::set& attribute_filter) { + ClearTransientRegistrations(); + options_ = options; + attribute_filter_ = attribute_filter; +} + +void MutationObserverRegistration::ObservedSubtreeNodeWillDetach(Node& node) { + if (!IsSubtree()) + return; + + node.RegisterTransientMutationObserver(this); + observer_->SetHasTransientRegistration(); + + if (!transient_registration_nodes_) { + transient_registration_nodes_ = std::make_unique>>(); + + assert(registration_node_); + assert(!registration_node_keep_alive_); + registration_node_keep_alive_ = registration_node_.Get(); // Balanced in clearTransientRegistrations. + } + + transient_registration_nodes_->insert(&node); +} + +void MutationObserverRegistration::ClearTransientRegistrations() { + if (!transient_registration_nodes_) { + assert(!registration_node_keep_alive_); + return; + } + + for (auto& node : *transient_registration_nodes_) + node->UnregisterTransientMutationObserver(this); + + transient_registration_nodes_.reset(); + + assert(registration_node_keep_alive_); + registration_node_keep_alive_ = nullptr; // Balanced in observeSubtreeNodeWillDetach. +} + +void MutationObserverRegistration::Unregister() { + // |this| can outlives registration_node_. + if (registration_node_) + registration_node_->UnregisterMutationObserver(this); + else + Dispose(); +} + +bool MutationObserverRegistration::ShouldReceiveMutationFrom(Node& node, + MutationType type, + const AtomicString* attribute_name) const { + assert((type == kMutationTypeAttributes && attribute_name) || !attribute_name); + if (!(options_ & type)) + return false; + + if (registration_node_ != &node && !IsSubtree()) + return false; + + if (type != kMutationTypeAttributes || !(options_ & MutationObserver::kAttributeFilter)) + return true; + + return attribute_filter_.count(*attribute_name) > 0; +} + +void MutationObserverRegistration::AddRegistrationNodesToSet(std::set>& nodes) const { + assert(registration_node_); + nodes.insert(registration_node_.Get()); + if (transient_registration_nodes_->empty()) + return; + for (const auto& transient_registration_node : *transient_registration_nodes_) + nodes.insert(transient_registration_node.Get()); +} + +void MutationObserverRegistration::Trace(GCVisitor* visitor) const { + visitor->TraceMember(observer_); + visitor->TraceMember(registration_node_); + visitor->TraceMember(registration_node_keep_alive_); + + if (transient_registration_nodes_ != nullptr) { + for (auto& n : *transient_registration_nodes_) { + visitor->TraceMember(n); + } + } +} + +} // namespace webf \ No newline at end of file diff --git a/bridge/core/dom/mutation_observer_registration.d.ts b/bridge/core/dom/mutation_observer_registration.d.ts new file mode 100644 index 0000000000..65ba7707d1 --- /dev/null +++ b/bridge/core/dom/mutation_observer_registration.d.ts @@ -0,0 +1,3 @@ +interface MutationObserverRegistration { + new(): void; +} \ No newline at end of file diff --git a/bridge/core/dom/mutation_observer_registration.h b/bridge/core/dom/mutation_observer_registration.h new file mode 100644 index 0000000000..8f158359df --- /dev/null +++ b/bridge/core/dom/mutation_observer_registration.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#ifndef WEBF_MUTATION_OBSERVER_REGISTRATION_H +#define WEBF_MUTATION_OBSERVER_REGISTRATION_H + +#include +#include "bindings/qjs/script_wrappable.h" +#include "mutation_observer.h" +#include "mutation_observer_options.h" + +namespace webf { + +class MutationObserver; +class Node; + +class MutationObserverRegistration : public ScriptWrappable { + DEFINE_WRAPPERTYPEINFO(); + + public: + MutationObserverRegistration(MutationObserver&, + Node*, + MutationObserverOptions, + const std::set& attribute_filter); + ~MutationObserverRegistration(); + + void ResetObservation(MutationObserverOptions, const std::set& attribute_filter); + void ObservedSubtreeNodeWillDetach(Node&); + void ClearTransientRegistrations(); + bool HasTransientRegistrations() const { + return transient_registration_nodes_.get() != nullptr && !transient_registration_nodes_->empty(); + } + void Unregister(); + + bool ShouldReceiveMutationFrom(Node&, MutationType, const AtomicString* attribute_name) const; + bool IsSubtree() const { return options_ & MutationObserver::kSubtree; } + + MutationObserver* Observer() const { return observer_; } + MutationRecordDeliveryOptions DeliveryOptions() const { + return options_ & (MutationObserver::kAttributeOldValue | MutationObserver::kCharacterDataOldValue); + } + MutationType MutationTypes() const { return static_cast(options_ & kMutationTypeAll); } + + void AddRegistrationNodesToSet(std::set>&) const; + + void Dispose(); + + void Trace(GCVisitor*) const override; + + private: + Member observer_; + Member registration_node_; + Member registration_node_keep_alive_; + std::unique_ptr>> transient_registration_nodes_; + + MutationObserverOptions options_; + std::set attribute_filter_; +}; + +} // namespace webf + +#endif // WEBF_MUTATION_OBSERVER_REGISTRATION_H diff --git a/bridge/core/dom/mutation_record.cc b/bridge/core/dom/mutation_record.cc new file mode 100644 index 0000000000..3dabc305fb --- /dev/null +++ b/bridge/core/dom/mutation_record.cc @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#include "mutation_record.h" +#include "bindings/qjs/cppgc/gc_visitor.h" +#include "built_in_string.h" +#include "mutation_record_types.h" +#include "node.h" +#include "static_node_list.h" + +namespace webf { + +MutationRecord::MutationRecord(JSContext* ctx) : ScriptWrappable(ctx) {} + +MutationRecord::~MutationRecord() = default; + +class ChildListRecord : public MutationRecord { + public: + explicit ChildListRecord(Node* target, + StaticNodeList* added, + StaticNodeList* removed, + Node* previous_sibling, + Node* next_sibling) + : target_(target), + added_nodes_(added), + removed_nodes_(removed), + previous_sibling_(previous_sibling), + next_sibling_(next_sibling), + MutationRecord(target->ctx()) {} + + void Trace(GCVisitor* visitor) const override { + visitor->TraceMember(target_); + visitor->TraceMember(added_nodes_); + visitor->TraceMember(removed_nodes_); + visitor->TraceMember(previous_sibling_); + visitor->TraceMember(next_sibling_); + MutationRecord::Trace(visitor); + } + + private: + const AtomicString& type() override; + Node* target() override { return target_.Get(); } + StaticNodeList* addedNodes() override { return added_nodes_; } + StaticNodeList* removedNodes() override { return removed_nodes_; } + Node* previousSibling() override { return previous_sibling_.Get(); } + Node* nextSibling() override { return next_sibling_.Get(); } + + Member target_; + Member added_nodes_; + Member removed_nodes_; + Member previous_sibling_; + Member next_sibling_; +}; + +class RecordWithEmptyNodeLists : public MutationRecord { + public: + RecordWithEmptyNodeLists(Node* target, const AtomicString& old_value) + : target_(target), old_value_(old_value), MutationRecord(target->ctx()) {} + + void Trace(GCVisitor* visitor) const override { + visitor->TraceMember(target_); + visitor->TraceMember(added_nodes_); + visitor->TraceMember(removed_nodes_); + MutationRecord::Trace(visitor); + } + + private: + Node* target() override { return target_.Get(); } + AtomicString oldValue() override { return old_value_; } + StaticNodeList* addedNodes() override { return LazilyInitializeEmptyNodeList(added_nodes_); } + StaticNodeList* removedNodes() override { return LazilyInitializeEmptyNodeList(removed_nodes_); } + + StaticNodeList* LazilyInitializeEmptyNodeList(Member& node_list) { + if (!node_list) { + node_list = MakeGarbageCollected(ctx()); + } + return node_list.Get(); + } + + Member target_; + AtomicString old_value_; + Member added_nodes_; + Member removed_nodes_; +}; + +class AttributesRecord : public RecordWithEmptyNodeLists { + public: + AttributesRecord(Node* target, + const AtomicString& name, + const AtomicString& attribute_namespace, + const AtomicString& old_value) + : RecordWithEmptyNodeLists(target, old_value), attribute_name_(name), attribute_namespace_(attribute_namespace) {} + + private: + const AtomicString& type() override; + const AtomicString attributeName() override { return attribute_name_; } + const AtomicString attributeNamespace() override { return attribute_namespace_; } + + AtomicString attribute_name_; + AtomicString attribute_namespace_; +}; + +class CharacterDataRecord : public RecordWithEmptyNodeLists { + public: + CharacterDataRecord(Node* target, const AtomicString& old_value) : RecordWithEmptyNodeLists(target, old_value) {} + + private: + const AtomicString& type() override; +}; + +class MutationRecordWithNullOldValue : public MutationRecord { + public: + MutationRecordWithNullOldValue(MutationRecord* record) : record_(record), MutationRecord(record->ctx()) {} + + void Trace(GCVisitor* visitor) const override { + visitor->TraceMember(record_); + MutationRecord::Trace(visitor); + } + + private: + const AtomicString& type() override { return record_->type(); } + Node* target() override { return record_->target(); } + StaticNodeList* addedNodes() override { return record_->addedNodes(); } + StaticNodeList* removedNodes() override { return record_->removedNodes(); } + Node* previousSibling() override { return record_->previousSibling(); } + Node* nextSibling() override { return record_->nextSibling(); } + const AtomicString attributeName() override { return record_->attributeName(); } + const AtomicString attributeNamespace() override { return record_->attributeNamespace(); } + + AtomicString oldValue() override { return AtomicString::Null(); } + + Member record_; +}; + +const AtomicString& ChildListRecord::type() { + return mutation_record_types::kchildList; +} + +const AtomicString& AttributesRecord::type() { + return mutation_record_types::kattributes; +} + +const AtomicString& CharacterDataRecord::type() { + return mutation_record_types::kcharacterData; +} + +MutationRecord* MutationRecord::CreateChildList(Node* target, + StaticNodeList* added, + StaticNodeList* removed, + Node* previous_sibling, + Node* next_sibling) { + return MakeGarbageCollected(target, added, removed, previous_sibling, next_sibling); +} + +MutationRecord* MutationRecord::CreateAttributes(Node* target, + const AtomicString& name, + const AtomicString& namespaceURI, + const AtomicString& old_value) { + return MakeGarbageCollected(target, name, namespaceURI, old_value); +} + +MutationRecord* MutationRecord::CreateCharacterData(Node* target, const AtomicString& old_value) { + return MakeGarbageCollected(target, old_value); +} + +MutationRecord* MutationRecord::CreateWithNullOldValue(MutationRecord* record) { + return MakeGarbageCollected(record); +} + +} // namespace webf \ No newline at end of file diff --git a/bridge/core/dom/mutation_record.d.ts b/bridge/core/dom/mutation_record.d.ts new file mode 100644 index 0000000000..04aca4f151 --- /dev/null +++ b/bridge/core/dom/mutation_record.d.ts @@ -0,0 +1,16 @@ +import {NodeList} from "./node_list"; +import {Node} from "./node"; + +interface MutationRecord { + readonly type: string; + readonly target: Node; + readonly addedNodes: NodeList; + readonly removedNodes: NodeList; + readonly previousSibling: Node | null; + readonly nextSibling: Node | null; + readonly attributeName: string | null; + readonly attributeNamespace: string | null; + readonly oldValue: string | null; + + new(): void; +} \ No newline at end of file diff --git a/bridge/core/dom/mutation_record.h b/bridge/core/dom/mutation_record.h new file mode 100644 index 0000000000..867fcfd914 --- /dev/null +++ b/bridge/core/dom/mutation_record.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#ifndef WEBF_CORE_DOM_MUTATION_RECORD_H_ +#define WEBF_CORE_DOM_MUTATION_RECORD_H_ + +#include "bindings/qjs/cppgc/member.h" +#include "bindings/qjs/script_wrappable.h" + +namespace webf { + +class Node; +class StaticNodeList; + +class MutationRecord : public ScriptWrappable { + DEFINE_WRAPPERTYPEINFO(); + + public: + using ImplType = Member; + static MutationRecord* CreateChildList(Node* target, + StaticNodeList* added, + StaticNodeList* removed, + Node* previous_sibling, + Node* next_sibling); + static MutationRecord* CreateAttributes(Node* target, + const AtomicString& name, + const AtomicString& namespaceURI, + const AtomicString& old_value); + static MutationRecord* CreateCharacterData(Node* target, const AtomicString& old_value); + static MutationRecord* CreateWithNullOldValue(MutationRecord*); + + MutationRecord() = delete; + MutationRecord(JSContext* ctx); + + ~MutationRecord() override; + + virtual const AtomicString& type() = 0; + virtual Node* target() = 0; + + virtual StaticNodeList* addedNodes() = 0; + virtual StaticNodeList* removedNodes() = 0; + virtual Node* previousSibling() { return nullptr; } + virtual Node* nextSibling() { return nullptr; } + + virtual const AtomicString attributeName() { return AtomicString::Null(); } + virtual const AtomicString attributeNamespace() { return AtomicString::Null(); } + + virtual AtomicString oldValue() { return AtomicString::Null(); } + + private: +}; + +} // namespace webf + +#endif // WEBF_CORE_DOM_MUTATION_RECORD_H_ diff --git a/bridge/core/dom/mutation_record_type.json5 b/bridge/core/dom/mutation_record_type.json5 new file mode 100644 index 0000000000..8dc4294c37 --- /dev/null +++ b/bridge/core/dom/mutation_record_type.json5 @@ -0,0 +1,15 @@ +{ + "metadata": { + "templates": [ + { + "template": "make_names", + "filename": "mutation_record_types" + } + ] + }, + "data": [ + "attributes", + "characterData", + "childList" + ] +} diff --git a/bridge/core/dom/node.cc b/bridge/core/dom/node.cc index dffffe49ed..3b7f732728 100644 --- a/bridge/core/dom/node.cc +++ b/bridge/core/dom/node.cc @@ -32,7 +32,9 @@ #include "node.h" #include #include "character_data.h" +#include "child_list_mutation_scope.h" #include "child_node_list.h" +#include "core/script_forbidden_scope.h" #include "document.h" #include "document_fragment.h" #include "element.h" @@ -104,6 +106,108 @@ EventTargetData& Node::EnsureEventTargetData() { return event_target_data_->GetEventTargetData(); } +template +static inline void CollectMatchingObserversForMutation(MutationObserverOptionsMap& observers, + Registry* registry, + Node& target, + MutationType type, + const AtomicString* attribute_name) { + if (!registry) + return; + + for (const auto& registration : *registry) { + if (registration->ShouldReceiveMutationFrom(target, type, attribute_name)) { + MutationRecordDeliveryOptions delivery_options = registration->DeliveryOptions(); + MutationObserver* ob = registration->Observer(); + + bool inserted = false; + auto position = observers.end(); + std::tie(position, inserted) = observers.insert(std::make_pair(ob, delivery_options)); + if (inserted) { + position->second |= delivery_options; + } else { + position->second = delivery_options; + } + } + } +} + +void Node::GetRegisteredMutationObserversOfType(MutationObserverOptionsMap& observers, + MutationType type, + const AtomicString* attribute_name) { + assert((type == kMutationTypeAttributes && attribute_name) || !attribute_name); + CollectMatchingObserversForMutation(observers, MutationObserverRegistry(), *this, type, attribute_name); + CollectMatchingObserversForMutation(observers, TransientMutationObserverRegistry(), *this, type, attribute_name); + ScriptForbiddenScope forbid_script_during_raw_iteration; + for (Node* node = parentNode(); node; node = node->parentNode()) { + CollectMatchingObserversForMutation(observers, node->MutationObserverRegistry(), *this, type, attribute_name); + CollectMatchingObserversForMutation(observers, node->TransientMutationObserverRegistry(), *this, type, + attribute_name); + } +} + +void Node::RegisterMutationObserver(MutationObserver& observer, + MutationObserverOptions options, + const std::set& attribute_filter) { + MutationObserverRegistration* registration = nullptr; + + for (const auto& item : EnsureNodeData().EnsureMutationObserverData().Registry()) { + if (item->Observer() == &observer) { + registration = item; + registration->ResetObservation(options, attribute_filter); + } + } + + if (!registration) { + registration = MakeGarbageCollected(observer, this, options, attribute_filter); + observer.ObservationStarted(registration); + EnsureNodeData().EnsureMutationObserverData().AddRegistration(registration); + } + + GetDocument().AddMutationObserverTypes(registration->MutationTypes()); +} + +void Node::UnregisterMutationObserver(MutationObserverRegistration* registration) { + const std::vector>* registry = MutationObserverRegistry(); + assert(registry); + if (!registry) + return; + + registration->Dispose(); + EnsureNodeData().EnsureMutationObserverData().RemoveRegistration(registration); +} + +void Node::RegisterTransientMutationObserver(MutationObserverRegistration* registration) { + EnsureNodeData().EnsureMutationObserverData().AddTransientRegistration(registration); +} + +void Node::UnregisterTransientMutationObserver(MutationObserverRegistration* registration) { + const MutationObserverRegistrationSet* transient_registry = TransientMutationObserverRegistry(); + assert(transient_registry != nullptr); + if (!transient_registry) + return; + + EnsureNodeData().EnsureMutationObserverData().RemoveTransientRegistration(registration); +} + +void Node::NotifyMutationObserversNodeWillDetach() { + if (!GetDocument().HasMutationObservers()) + return; + + ScriptForbiddenScope forbid_script_during_raw_iteration; + for (Node* node = parentNode(); node; node = node->parentNode()) { + if (const MutationObserverRegistrationVector* registry = node->MutationObserverRegistry()) { + for (const auto& registration : *registry) + registration->ObservedSubtreeNodeWillDetach(*this); + } + + if (const MutationObserverRegistrationSet* transient_registry = node->TransientMutationObserverRegistry()) { + for (auto& registration : *transient_registry) + registration->ObservedSubtreeNodeWillDetach(*this); + } + } +} + NodeData& Node::CreateNodeData() { node_data_ = std::make_unique(); SetFlag(kHasDataFlag); @@ -116,6 +220,26 @@ NodeData& Node::EnsureNodeData() { return CreateNodeData(); } +const std::vector>* Node::MutationObserverRegistry() { + if (!HasNodeData()) + return nullptr; + NodeMutationObserverData* data = EnsureNodeData().MutationObserverData(); + if (!data) { + return nullptr; + } + return &data->Registry(); +} + +const std::set>* Node::TransientMutationObserverRegistry() { + if (!HasNodeData()) + return nullptr; + NodeMutationObserverData* data = EnsureNodeData().MutationObserverData(); + if (!data) { + return nullptr; + } + return &data->TransientRegistry(); +} + Node& Node::TreeRoot() const { const Node* node = this; while (node->parentNode()) @@ -229,8 +353,7 @@ static Node* ConvertNodesIntoNode(const Node* parent, return fragment; } -void Node::prepend(const std::vector>& nodes, - webf::ExceptionState& exception_state) { +void Node::prepend(const std::vector>& nodes, ExceptionState& exception_state) { auto* this_node = DynamicTo(this); if (!this_node) { exception_state.ThrowException(ctx(), ErrorType::TypeError, "This node type does not support this method."); @@ -241,8 +364,7 @@ void Node::prepend(const std::vector>& no this_node->InsertBefore(node, this_node->firstChild(), exception_state); } -void Node::append(const std::vector>& nodes, - webf::ExceptionState& exception_state) { +void Node::append(const std::vector>& nodes, ExceptionState& exception_state) { auto* this_node = DynamicTo(this); if (!this_node) { exception_state.ThrowException(ctx(), ErrorType::TypeError, "This node type does not support this method."); @@ -261,7 +383,7 @@ void Node::before(ExceptionState& exception_state) { before(std::vector>(), exception_state); } -void Node::after(webf::ExceptionState& exception_state) { +void Node::after(ExceptionState& exception_state) { after(std::vector>(), exception_state); } @@ -290,8 +412,7 @@ static Node* FindViableNextSibling(const Node& node, const std::vector>& nodes, - webf::ExceptionState& exception_state) { +void Node::before(const std::vector>& nodes, ExceptionState& exception_state) { ContainerNode* parent = parentNode(); if (!parent) return; @@ -302,8 +423,7 @@ void Node::before(const std::vector>& nod } } -void Node::after(const std::vector>& nodes, - webf::ExceptionState& exception_state) { +void Node::after(const std::vector>& nodes, ExceptionState& exception_state) { ContainerNode* parent = parentNode(); if (!parent) return; @@ -408,6 +528,8 @@ void Node::setTextContent(const AtomicString& text, ExceptionState& exception_st if (container->HasOneTextChild() && To(container->firstChild())->data() == text && !text.IsEmpty()) return; + ChildListMutationScope mutation(*this); + // Note: This API will not insert empty text nodes: // https://dom.spec.whatwg.org/#dom-node-textcontent if (text.IsEmpty()) { diff --git a/bridge/core/dom/node.h b/bridge/core/dom/node.h index 6238bbf73f..3d42b00830 100644 --- a/bridge/core/dom/node.h +++ b/bridge/core/dom/node.h @@ -11,12 +11,16 @@ #include "events/event_target.h" #include "foundation/macros.h" +#include "mutation_observer.h" +#include "mutation_observer_registration.h" #include "node_data.h" #include "qjs_union_dom_stringnode.h" #include "tree_scope.h" namespace webf { +using MutationObserverOptionsMap = std::unordered_map; + const int kDOMNodeTypeShift = 2; const int kElementNamespaceTypeShift = 4; const int kNodeStyleChangeShift = 15; @@ -234,12 +238,26 @@ class Node : public EventTarget { void SetSelfOrAncestorHasDirAutoAttribute() { SetFlag(kSelfOrAncestorHasDirAutoAttribute); } void ClearSelfOrAncestorHasDirAutoAttribute() { ClearFlag(kSelfOrAncestorHasDirAutoAttribute); } + void GetRegisteredMutationObserversOfType(MutationObserverOptionsMap&, + MutationType, + const AtomicString* attribute_name); + void RegisterMutationObserver(MutationObserver&, + MutationObserverOptions, + const std::set& attribute_filter); + void UnregisterMutationObserver(MutationObserverRegistration*); + void RegisterTransientMutationObserver(MutationObserverRegistration*); + void UnregisterTransientMutationObserver(MutationObserverRegistration*); + void NotifyMutationObserversNodeWillDetach(); + NodeData& CreateNodeData(); [[nodiscard]] bool HasNodeData() const { return GetFlag(kHasDataFlag); } // |RareData| cannot be replaced or removed once assigned. [[nodiscard]] NodeData* Data() const { return node_data_.get(); } NodeData& EnsureNodeData(); + const MutationObserverRegistrationVector* MutationObserverRegistry(); + const MutationObserverRegistrationSet* TransientMutationObserverRegistry(); + void Trace(GCVisitor*) const override; private: diff --git a/bridge/core/dom/node_data.cc b/bridge/core/dom/node_data.cc index 0cefcedc1c..64e911cc9b 100644 --- a/bridge/core/dom/node_data.cc +++ b/bridge/core/dom/node_data.cc @@ -5,6 +5,7 @@ #include "node_data.h" #include "bindings/qjs/cppgc/garbage_collected.h" +#include "bindings/qjs/cppgc/gc_visitor.h" #include "child_node_list.h" #include "container_node.h" #include "empty_node_list.h" @@ -12,6 +13,36 @@ namespace webf { +void NodeMutationObserverData::Trace(GCVisitor* visitor) const { + for (auto& entry : registry_) { + visitor->TraceMember(entry); + } + + for (auto& entry : transient_registry_) { + visitor->TraceMember(entry); + } +} + +NodeMutationObserverData::~NodeMutationObserverData() {} + +void NodeMutationObserverData::AddTransientRegistration(MutationObserverRegistration* registration) { + transient_registry_.insert(registration); +} + +void NodeMutationObserverData::RemoveTransientRegistration(MutationObserverRegistration* registration) { + assert(transient_registry_.count(registration) > 0); + transient_registry_.erase(registration); +} + +void NodeMutationObserverData::AddRegistration(MutationObserverRegistration* registration) { + registry_.emplace_back(registration); +} + +void NodeMutationObserverData::RemoveRegistration(MutationObserverRegistration* registration) { + assert(std::find(registry_.begin(), registry_.end(), registration) != registry_.end()); + registry_.erase(std::find(registry_.begin(), registry_.end(), registration)); +} + ChildNodeList* NodeData::GetChildNodeList(ContainerNode& node) { assert(!node_list_ || &node == node_list_->VirtualOwnerNode()); return To(node_list_.Get()); @@ -37,6 +68,9 @@ void NodeData::Trace(GCVisitor* visitor) const { if (node_list_ != nullptr) { visitor->TraceValue(node_list_->ToQuickJSUnsafe()); } + if (mutation_observer_data_ != nullptr) { + mutation_observer_data_->Trace(visitor); + } } } // namespace webf diff --git a/bridge/core/dom/node_data.h b/bridge/core/dom/node_data.h index 9366f5883d..09ad5423ef 100644 --- a/bridge/core/dom/node_data.h +++ b/bridge/core/dom/node_data.h @@ -9,6 +9,7 @@ #include #include "bindings/qjs/cppgc/garbage_collected.h" #include "bindings/qjs/cppgc/gc_visitor.h" +#include "mutation_observer_registration.h" namespace webf { @@ -18,6 +19,28 @@ class ContainerNode; class NodeList; class Node; +class NodeMutationObserverData final { + public: + NodeMutationObserverData() = default; + NodeMutationObserverData(const NodeMutationObserverData&) = delete; + NodeMutationObserverData& operator=(const NodeMutationObserverData&) = delete; + ~NodeMutationObserverData(); + + const std::vector>& Registry() { return registry_; } + const std::set>& TransientRegistry() { return transient_registry_; } + + void AddTransientRegistration(MutationObserverRegistration* registration); + void RemoveTransientRegistration(MutationObserverRegistration* registration); + void AddRegistration(MutationObserverRegistration* registration); + void RemoveRegistration(MutationObserverRegistration* registration); + + void Trace(GCVisitor* visitor) const; + + private: + std::vector> registry_; + std::set> transient_registry_; +}; + class NodeData { public: enum class ClassType : uint8_t { @@ -30,12 +53,21 @@ class NodeData { ChildNodeList* EnsureChildNodeList(ContainerNode& node); NodeList* NodeLists() { return node_list_; } + NodeMutationObserverData* MutationObserverData() { return mutation_observer_data_.get(); } + NodeMutationObserverData& EnsureMutationObserverData() { + if (!mutation_observer_data_) { + mutation_observer_data_ = std::make_shared(); + } + return *mutation_observer_data_; + } + EmptyNodeList* EnsureEmptyChildNodeList(Node& node); void Trace(GCVisitor* visitor) const; private: Member node_list_; + std::shared_ptr mutation_observer_data_; }; } // namespace webf diff --git a/bridge/core/dom/node_test.cc b/bridge/core/dom/node_test.cc index 0402215904..f0f9f05b38 100644 --- a/bridge/core/dom/node_test.cc +++ b/bridge/core/dom/node_test.cc @@ -28,6 +28,52 @@ TEST(Node, appendChild) { EXPECT_EQ(logCalled, true); } +TEST(Node, MutationObserver) { + bool static errorCalled = false; + bool static logCalled = false; + webf::WebFPage::consoleMessageHandler = [](void* ctx, const std::string& message, int logLevel) { logCalled = true; }; + auto env = TEST_init([](int32_t contextId, const char* errmsg) { errorCalled = true; }); + auto context = env->page()->GetExecutingContext(); + const char* code = R"( +const container = document.createElement('div'); +document.body.appendChild(container); + +// Callback function to execute when mutations are observed +const callback = function (mutationList, observer) { + console.log('c'); +}; + +// Create an observer instance linked to the callback function +const observer = new MutationObserver(callback); + +// Options for the observer (which mutations to observe) +const config = { attributes: true, childList: true, subtree: true, attributeOldValue: true }; + +// Start observing the target node for configured mutations +observer.observe(container, config); + +container.appendChild(document.createTextNode('TEXT')); + +Promise.resolve().then(() => { + console.log('1234'); + container.appendChild(document.createTextNode('TEXT')); + Promise.resolve().then(() => { + console.log('444'); + container.removeChild(container.firstChild); + Promise.resolve().then(() => { + console.log('555'); + }); + }); +}); +)"; + env->page()->evaluateScript(code, strlen(code), "vm://", 0); + + TEST_runLoop(context); + + EXPECT_EQ(errorCalled, false); + EXPECT_EQ(logCalled, true); +} + TEST(Node, nodeName) { bool static errorCalled = false; bool static logCalled = false; diff --git a/bridge/core/dom/static_node_list.cc b/bridge/core/dom/static_node_list.cc new file mode 100644 index 0000000000..79ed82c28e --- /dev/null +++ b/bridge/core/dom/static_node_list.cc @@ -0,0 +1,47 @@ +#include "static_node_list.h" + +namespace webf { + +StaticNodeList* StaticNodeList::Adopt(JSContext* ctx, std::vector>& nodes) { + auto* node_list = MakeGarbageCollected(ctx); + swap(node_list->nodes_, nodes); + return node_list; +} + +StaticNodeList::~StaticNodeList() = default; + +unsigned StaticNodeList::length() const { + return nodes_.size(); +} + +Node* StaticNodeList::item(unsigned index, ExceptionState& exception_state) const { + if (index < nodes_.size()) + return nodes_[index].Get(); + return nullptr; +} + +void StaticNodeList::Trace(GCVisitor* visitor) const { + for (auto& entry : nodes_) { + visitor->TraceMember(entry); + } + + NodeList::Trace(visitor); +} + +void StaticNodeList::NamedPropertyEnumerator(std::vector& names, webf::ExceptionState& exception_state) { + for (int i = 0; i < nodes_.size(); i++) { + names.emplace_back(AtomicString(ctx(), std::to_string(i))); + } +} + +bool StaticNodeList::NamedPropertyQuery(const webf::AtomicString& key, webf::ExceptionState& exception_state) { + std::string str = key.ToStdString(ctx()); + int number = std::stoi(str); + if (number >= nodes_.size()) { + return false; + } + + return nodes_[number]; +} + +} // namespace webf \ No newline at end of file diff --git a/bridge/core/dom/static_node_list.h b/bridge/core/dom/static_node_list.h new file mode 100644 index 0000000000..e880cc5b11 --- /dev/null +++ b/bridge/core/dom/static_node_list.h @@ -0,0 +1,30 @@ +#ifndef WEBF_CORE_DOM_STATIC_NODE_LIST_H_ +#define WEBF_CORE_DOM_STATIC_NODE_LIST_H_ + +#include "bindings/qjs/cppgc/gc_visitor.h" +#include "node_list.h" + +namespace webf { + +class StaticNodeList final : public NodeList { + public: + static StaticNodeList* Adopt(JSContext* ctx, std::vector>& nodes); + + explicit StaticNodeList(JSContext* ctx) : NodeList(ctx){}; + ~StaticNodeList() override; + + unsigned length() const override; + Node* item(unsigned index, ExceptionState& exception_state) const override; + + void Trace(GCVisitor*) const override; + + bool NamedPropertyQuery(const AtomicString& key, ExceptionState& exception_state) override; + void NamedPropertyEnumerator(std::vector& names, ExceptionState& exception_state) override; + + private: + std::vector> nodes_; +}; + +} // namespace webf + +#endif // WEBF_CORE_DOM_STATIC_NODE_LIST_H_ diff --git a/bridge/core/executing_context.cc b/bridge/core/executing_context.cc index 468e21532b..4e8f2159cc 100644 --- a/bridge/core/executing_context.cc +++ b/bridge/core/executing_context.cc @@ -8,12 +8,14 @@ #include "bindings/qjs/converter_impl.h" #include "built_in_string.h" #include "core/dom/document.h" +#include "core/dom/mutation_observer.h" #include "core/events/error_event.h" #include "core/events/promise_rejection_event.h" #include "event_type_names.h" #include "foundation/logging.h" #include "polyfill.h" #include "qjs_window.h" +#include "script_forbidden_scope.h" #include "timing/performance.h" namespace webf { @@ -123,6 +125,10 @@ bool ExecutingContext::EvaluateJavaScript(const char* code, uint64_t* bytecode_len, const char* sourceURL, int startLine) { + if (ScriptForbiddenScope::IsScriptForbidden()) { + return false; + } + JSValue result; if (parsed_bytecodes == nullptr) { result = JS_Eval(script_state_.ctx(), code, code_len, sourceURL, JS_EVAL_TYPE_GLOBAL); @@ -140,7 +146,7 @@ bool ExecutingContext::EvaluateJavaScript(const char* code, result = JS_EvalFunction(script_state_.ctx(), byte_object); } - DrainPendingPromiseJobs(); + DrainMicrotasks(); bool success = HandleException(&result); JS_FreeValue(script_state_.ctx(), result); return success; @@ -149,7 +155,7 @@ bool ExecutingContext::EvaluateJavaScript(const char* code, bool ExecutingContext::EvaluateJavaScript(const char16_t* code, size_t length, const char* sourceURL, int startLine) { std::string utf8Code = toUTF8(std::u16string(reinterpret_cast(code), length)); JSValue result = JS_Eval(script_state_.ctx(), utf8Code.c_str(), utf8Code.size(), sourceURL, JS_EVAL_TYPE_GLOBAL); - DrainPendingPromiseJobs(); + DrainMicrotasks(); bool success = HandleException(&result); JS_FreeValue(script_state_.ctx(), result); return success; @@ -157,7 +163,7 @@ bool ExecutingContext::EvaluateJavaScript(const char16_t* code, size_t length, c bool ExecutingContext::EvaluateJavaScript(const char* code, size_t codeLength, const char* sourceURL, int startLine) { JSValue result = JS_Eval(script_state_.ctx(), code, codeLength, sourceURL, JS_EVAL_TYPE_GLOBAL); - DrainPendingPromiseJobs(); + DrainMicrotasks(); bool success = HandleException(&result); JS_FreeValue(script_state_.ctx(), result); return success; @@ -169,7 +175,7 @@ bool ExecutingContext::EvaluateByteCode(uint8_t* bytes, size_t byteLength) { if (!HandleException(&obj)) return false; val = JS_EvalFunction(script_state_.ctx(), obj); - DrainPendingPromiseJobs(); + DrainMicrotasks(); if (!HandleException(&val)) return false; JS_FreeValue(script_state_.ctx(), val); @@ -262,6 +268,42 @@ void ExecutingContext::ReportError(JSValueConst error) { JS_FreeCString(ctx, type); } +void ExecutingContext::DrainMicrotasks() { + DrainPendingPromiseJobs(); +} + +namespace { + +struct MicroTaskDeliver { + MicrotaskCallback callback; + void* data; +}; + +} // namespace + +void ExecutingContext::EnqueueMicrotask(MicrotaskCallback callback, void* data) { + JSValue proxy_data = JS_NewObject(ctx()); + + auto* deliver = new MicroTaskDeliver(); + deliver->data = data; + deliver->callback = callback; + + JS_SetOpaque(proxy_data, deliver); + + JS_EnqueueJob( + ctx(), + [](JSContext* ctx, int argc, JSValueConst* argv) -> JSValue { + auto* deliver = static_cast(JS_GetOpaque(argv[0], JS_CLASS_OBJECT)); + deliver->callback(deliver->data); + + delete deliver; + return JS_NULL; + }, + 1, &proxy_data); + + JS_FreeValue(ctx(), proxy_data); +} + void ExecutingContext::DrainPendingPromiseJobs() { // should executing pending promise jobs. JSContext* pctx; diff --git a/bridge/core/executing_context.h b/bridge/core/executing_context.h index 5c81c1736c..a5a1d9b361 100644 --- a/bridge/core/executing_context.h +++ b/bridge/core/executing_context.h @@ -44,9 +44,11 @@ class Performance; class MemberMutationScope; class ErrorEvent; class DartContext; +class MutationObserver; class ScriptWrappable; using JSExceptionHandler = std::function; +using MicrotaskCallback = void (*)(void* data); bool isContextValid(int32_t contextId); @@ -84,7 +86,8 @@ class ExecutingContext { bool HandleException(ScriptValue* exc); bool HandleException(ExceptionState& exception_state); void ReportError(JSValueConst error); - void DrainPendingPromiseJobs(); + void DrainMicrotasks(); + void EnqueueMicrotask(MicrotaskCallback callback, void* data = nullptr); void DefineGlobalProperty(const char* prop, JSValueConst value); ExecutionContextData* contextData(); uint8_t* DumpByteCode(const char* code, uint32_t codeLength, const char* sourceURL, size_t* bytecodeLength); @@ -155,6 +158,9 @@ class ExecutingContext { void InstallDocument(); void InstallPerformance(); + void DrainPendingPromiseJobs(); + void EnsureEnqueueMicrotask(); + static void promiseRejectTracker(JSContext* ctx, JSValueConst promise, JSValueConst reason, diff --git a/bridge/core/html/canvas/canvas_gradient.cc b/bridge/core/html/canvas/canvas_gradient.cc index ff31e6bd65..0a3cc051cf 100644 --- a/bridge/core/html/canvas/canvas_gradient.cc +++ b/bridge/core/html/canvas/canvas_gradient.cc @@ -3,6 +3,7 @@ */ #include "canvas_gradient.h" +#include "core/executing_context.h" namespace webf { diff --git a/bridge/core/html/custom/widget_element.h b/bridge/core/html/custom/widget_element.h index 5e4cefeeaf..d3ce1bba01 100644 --- a/bridge/core/html/custom/widget_element.h +++ b/bridge/core/html/custom/widget_element.h @@ -54,6 +54,10 @@ class WidgetElement : public HTMLElement { template <> struct DowncastTraits { static bool AllowFrom(const Element& element) { return element.IsWidgetElement(); } + static bool AllowFrom(const BindingObject& binding_object) { + return binding_object.IsEventTarget() && To(binding_object).IsNode() && + To(binding_object).IsElementNode() && To(binding_object).IsWidgetElement(); + } }; } // namespace webf diff --git a/bridge/core/html/parser/html_parser.cc b/bridge/core/html/parser/html_parser.cc index c0ad0cd5dc..8f5aa53682 100644 --- a/bridge/core/html/parser/html_parser.cc +++ b/bridge/core/html/parser/html_parser.cc @@ -198,17 +198,9 @@ void HTMLParser::parseProperty(Element* element, GumboElement* gumboElement) { for (int j = 0; j < attributes->length; ++j) { auto* attribute = (GumboAttribute*)attributes->data[j]; - if (strcmp(attribute->name, "style") == 0) { - auto* style = element->style(); - if (style == nullptr) { - return; - } - style->setCssText(AtomicString(ctx, attribute->value), ASSERT_NO_EXCEPTION()); - } else { - std::string strName = attribute->name; - std::string strValue = attribute->value; - element->setAttribute(AtomicString(ctx, strName), AtomicString(ctx, strValue), ASSERT_NO_EXCEPTION()); - } + std::string strName = attribute->name; + std::string strValue = attribute->value; + element->setAttribute(AtomicString(ctx, strName), AtomicString(ctx, strValue), ASSERT_NO_EXCEPTION()); } } diff --git a/bridge/core/page.cc b/bridge/core/page.cc index dfd5beb572..25066f862d 100644 --- a/bridge/core/page.cc +++ b/bridge/core/page.cc @@ -27,7 +27,7 @@ WebFPage::WebFPage(DartIsolateContext* dart_isolate_context, int32_t contextId, context_ = new ExecutingContext( dart_isolate_context, contextId, [](ExecutingContext* context, const char* message) { - if (context->dartMethodPtr()->onJsError != nullptr) { + if (context->IsContextValid() && context->dartMethodPtr()->onJsError != nullptr) { context->dartMethodPtr()->onJsError(context->contextId(), message); } WEBF_LOG(ERROR) << message << std::endl; diff --git a/bridge/core/script_forbidden_scope.cc b/bridge/core/script_forbidden_scope.cc new file mode 100644 index 0000000000..026196375b --- /dev/null +++ b/bridge/core/script_forbidden_scope.cc @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#include "script_forbidden_scope.h" + +namespace webf { + +unsigned ScriptForbiddenScope::g_main_thread_counter_ = 0; + +} \ No newline at end of file diff --git a/bridge/core/script_forbidden_scope.h b/bridge/core/script_forbidden_scope.h new file mode 100644 index 0000000000..622e468937 --- /dev/null +++ b/bridge/core/script_forbidden_scope.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +#ifndef WEBF_CORE_SCRIPT_FORBIDDEN_SCOPE_H_ +#define WEBF_CORE_SCRIPT_FORBIDDEN_SCOPE_H_ + +#include +#include "foundation/macros.h" + +namespace webf { + +// Scoped disabling of script execution. +class ScriptForbiddenScope final { + WEBF_STACK_ALLOCATED(); + + public: + ScriptForbiddenScope() { Enter(); } + ScriptForbiddenScope(const ScriptForbiddenScope&) = delete; + ScriptForbiddenScope& operator=(const ScriptForbiddenScope&) = delete; + ~ScriptForbiddenScope() { Exit(); } + + static bool IsScriptForbidden() { return g_main_thread_counter_ > 0; } + + private: + static void Enter() { ++g_main_thread_counter_; } + static void Exit() { + assert(IsScriptForbidden()); + --g_main_thread_counter_; + } + + static unsigned g_main_thread_counter_; +}; + +} // namespace webf + +#endif // WEBF_CORE_SCRIPT_FORBIDDEN_SCOPE_H_ diff --git a/bridge/core/timing/performance.cc b/bridge/core/timing/performance.cc index d6baa6ad7d..54699266a0 100644 --- a/bridge/core/timing/performance.cc +++ b/bridge/core/timing/performance.cc @@ -108,8 +108,6 @@ void Performance::clearMarks(ExceptionState& exception_state) { while (it != entries_.end()) { if ((*it)->entryType() != performance_entry_names::kmark) { new_entries.emplace_back(*it); - } else { - it->Clear(); } it++; } @@ -124,8 +122,6 @@ void Performance::clearMarks(const AtomicString& name, ExceptionState& exception while (it != std::end(entries_)) { if (!((*it)->entryType() == performance_entry_names::kmark && (*it)->name() == name)) { new_entries.emplace_back(*it); - } else { - it->Clear(); } it++; } @@ -140,8 +136,6 @@ void Performance::clearMeasures(ExceptionState& exception_state) { while (it != std::end(entries_)) { if ((*it)->entryType() != performance_entry_names::kmeasure) { new_entries.emplace_back(*it); - } else { - it->Clear(); } it++; } @@ -156,8 +150,6 @@ void Performance::clearMeasures(const AtomicString& name, ExceptionState& except while (it != std::end(entries_)) { if (!((*it)->entryType() == performance_entry_names::kmeasure && (*it)->name() == name)) { new_entries.emplace_back(*it); - } else { - it->Clear(); } it++; } diff --git a/bridge/scripts/code_generator/templates/idl_templates/base.cc.tpl b/bridge/scripts/code_generator/templates/idl_templates/base.cc.tpl index 716b6c8404..3ad1eb6eaa 100644 --- a/bridge/scripts/code_generator/templates/idl_templates/base.cc.tpl +++ b/bridge/scripts/code_generator/templates/idl_templates/base.cc.tpl @@ -19,6 +19,7 @@ #include "core/dom/document_fragment.h" #include "core/dom/comment.h" #include "core/input/touch_list.h" +#include "core/dom/static_node_list.h" #include "core/html/html_all_collection.h" #include "defined_properties.h" diff --git a/bridge/test/webf_test_context.cc b/bridge/test/webf_test_context.cc index ed45a9b4a0..7cfb206068 100644 --- a/bridge/test/webf_test_context.cc +++ b/bridge/test/webf_test_context.cc @@ -82,7 +82,7 @@ static JSValue matchImageSnapshot(JSContext* ctx, JSValueConst this_val, int arg JS_FreeValue(ctx, errmsgValue); } - callbackContext->context->DrainPendingPromiseJobs(); + callbackContext->context->DrainMicrotasks(); JS_FreeValue(callbackContext->context->ctx(), callbackContext->callback); delete callbackContext; }; @@ -126,7 +126,7 @@ static void handleSimulatePointerCallback(void* p, int32_t contextId, const char JS_Call(simulate_context->context->ctx(), simulate_context->callbackValue, JS_NULL, 0, nullptr); JS_FreeValue(simulate_context->context->ctx(), return_value); JS_FreeValue(simulate_context->context->ctx(), simulate_context->callbackValue); - simulate_context->context->DrainPendingPromiseJobs(); + simulate_context->context->DrainMicrotasks(); delete simulate_context; } @@ -311,7 +311,7 @@ void WebFTestContext::invokeExecuteTest(ExecuteCallback executeCallback) { ScriptValue result = execute_test_callback_->Invoke(context_->ctx(), ScriptValue::Empty(context_->ctx()), 1, arguments); context_->HandleException(&result); - context_->DrainPendingPromiseJobs(); + context_->DrainMicrotasks(); JS_FreeValue(context_->ctx(), callback); execute_test_callback_ = nullptr; } diff --git a/bridge/test/webf_test_env.cc b/bridge/test/webf_test_env.cc index 6c6d2e74b3..af4834b26d 100644 --- a/bridge/test/webf_test_env.cc +++ b/bridge/test/webf_test_env.cc @@ -207,6 +207,7 @@ std::unique_ptr TEST_init(OnJSError onJsError) { void* testContext = initTestFramework(page); test_context_map[pageContextId] = reinterpret_cast(testContext); TEST_mockTestEnvDartMethods(testContext, onJsError); + JS_TurnOnGC(static_cast(dart_isolate_context)->runtime()); JSThreadState* th = new JSThreadState(); JS_SetRuntimeOpaque( reinterpret_cast(testContext)->page()->GetExecutingContext()->dartIsolateContext()->runtime(), @@ -280,7 +281,7 @@ static bool jsPool(webf::ExecutingContext* context) { void TEST_runLoop(webf::ExecutingContext* context) { for (;;) { - context->DrainPendingPromiseJobs(); + context->DrainMicrotasks(); if (jsPool(context)) break; } diff --git a/integration_tests/runtime/global.ts b/integration_tests/runtime/global.ts index 51cf1f63f1..e3efd773a5 100644 --- a/integration_tests/runtime/global.ts +++ b/integration_tests/runtime/global.ts @@ -65,6 +65,10 @@ function assert_throws_exactly(error: any, fn: Function) { expect(fn).toThrow(error); } +function assert_throws(error: any, fn: Function) { + expect(fn).toThrow(); +} + function assert_not_equals(a: any, b: any, message?: string) { expect(a !== b).toBe(true, message) } diff --git a/integration_tests/specs/dom/nodes/mutation-observer.ts b/integration_tests/specs/dom/nodes/mutation-observer.ts new file mode 100644 index 0000000000..2c8ff60d33 --- /dev/null +++ b/integration_tests/specs/dom/nodes/mutation-observer.ts @@ -0,0 +1,1504 @@ +// Compares a mutation record to a predefined one +// mutationToCheck is a mutation record from the user agent +// expectedRecord is a mutation record minted by the test +// for expectedRecord, if properties are omitted, they get default ones +function checkRecords(target, mutationToCheck, expectedRecord) { + var mr1; + var mr2; + + function checkField(property, isArray = false) { + var field = mr2[property]; + if (isArray === undefined) { + isArray = false; + } + if (field instanceof Function) { + field = field(); + } else if (field === undefined) { + if (isArray) { + field = new Array(); + } else { + field = null; + } + } + if (isArray) { + assert_array_equals(mr1[property], field, property + " didn't match"); + } else { + expect(mr1[property]).toEqual(field, property + " didn't match"); + } + } + assert_equals(mutationToCheck.length, expectedRecord.length, "mutation records must match"); + for (var item = 0; item < mutationToCheck.length; item++) { + mr1 = mutationToCheck[item]; + mr2 = expectedRecord[item]; + + if (mr2.target instanceof Function) { + assert_equals(mr1.target, mr2.target(), "target node must match"); + } else if (mr2.target !== undefined) { + assert_equals(mr1.target, mr2.target, "target node must match"); + } else { + assert_equals(mr1.target, target, "target node must match"); + } + + checkField("type"); + checkField("addedNodes", true); + checkField("removedNodes", true); + checkField("previousSibling"); + checkField("nextSibling"); + checkField("attributeName"); + // checkField("attributeNamespace"); + checkField("oldValue"); + } +} + +function runMutationTest(node, mutationObserverOptions, mutationRecordSequence, mutationFunction, description, target?: any) { + (new MutationObserver(moc)).observe(node, mutationObserverOptions); + + function moc(mrl, obs) { + if (target === undefined) target = node; + checkRecords(target, mrl, mutationRecordSequence); + } + + mutationFunction(); +} + +describe("MutationObserver Style", function() { + test(async () => { + let called = 0; + const el = document.createElement("div"); + document.body.appendChild(el); + const m = new MutationObserver(() => { + called++; + }); + m.observe(el, { attributes: true }); + el.style.height = "100px"; + await Promise.resolve(); + assert_equals(called, 1, "times callback called"); + el.style.height = "100px"; + await Promise.resolve(); + assert_equals(called, 1, "times callback called"); + }, "Updating style property with the same value does not trigger an observation callback"); + + test(async () => { + let called = 0; + const el = document.createElement("div"); + document.body.appendChild(el); + const m = new MutationObserver(() => { + called++; + }); + m.observe(el, { attributes: true }); + el.style.cssText = "height:100px"; + await Promise.resolve(); + assert_equals(called, 1, "times callback called"); + el.style.cssText = "height:100px"; + await Promise.resolve(); + assert_equals(called, 2, "times callback called"); + }, "Updating cssText triggers an observation callback"); +}); + +describe("MutationObserver sanity", function() { + test(() => { + var m = new MutationObserver(() => { + }); + assert_throws_exactly(new TypeError("The options object must set at least one of 'attributes', 'characterData', or 'childList' to true."), () => { + m.observe(document, {}); + }); + }, "Should throw if none of childList, attributes, characterData are true"); + + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { childList: true }); + m.disconnect(); + }, "Should not throw if childList is true"); + + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { attributes: true }); + m.disconnect(); + }, "Should not throw if attributes is true"); + // + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { characterData: true }); + m.disconnect(); + }, "Should not throw if characterData is true"); + + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { attributeOldValue: true }); + m.disconnect(); + }, "Should not throw if attributeOldValue is true and attributes is omitted"); + + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { characterDataOldValue: true }); + m.disconnect(); + }, "Should not throw if characterDataOldValue is true and characterData is omitted"); + + test(() => { + var m = new MutationObserver(() => { + }); + // @ts-ignore + m.observe(document, { attributes: ["abc"] }); + m.disconnect(); + }, "Should not throw if attributeFilter is present and attributes is omitted"); + + test(() => { + var m = new MutationObserver(() => { + }); + assert_throws_exactly(new TypeError("The options object may only set 'attributeOldValue' to true when 'attributes' is true or not present."), () => { + m.observe(document, { + childList: true, attributeOldValue: true, + attributes: false + }); + }); + }, "Should throw if attributeOldValue is true and attributes is false"); + + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { + childList: true, attributeOldValue: true, + attributes: true + }); + m.disconnect(); + }, "Should not throw if attributeOldValue and attributes are both true"); + + test(() => { + var m = new MutationObserver(() => { + }); + assert_throws_exactly(new TypeError("The options object may only set 'attributeFilter' when 'attributes' is true or not present."), () => { + m.observe(document, { + childList: true, attributeFilter: ["abc"], + attributes: false + }); + }); + }, "Should throw if attributeFilter is present and attributes is false"); + + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { + childList: true, attributeFilter: ["abc"], + attributes: true + }); + m.disconnect(); + }, "Should not throw if attributeFilter is present and attributes is true"); + + test(() => { + var m = new MutationObserver(() => { + }); + assert_throws_exactly(new TypeError("The options object may only set 'characterDataOldValue' to true when 'characterData' is true or not present."), () => { + m.observe(document, { + childList: true, characterDataOldValue: true, + characterData: false + }); + }); + }, "Should throw if characterDataOldValue is true and characterData is false"); + + test(() => { + var m = new MutationObserver(() => { + }); + m.observe(document, { + childList: true, characterDataOldValue: true, + characterData: true + }); + m.disconnect(); + }, "Should not throw if characterDataOldValue is true and characterData is true"); +}); + +describe("MutationObserver document", () => { + it("001", async () => { + var testCounter = 0; + var document_observer = new MutationObserver(function(sequence) { + testCounter++; + if (testCounter == 1) { + checkRecords(document, sequence, + [{ + type: "childList", + addedNodes: function() { + return [newElement]; + }, + previousSibling: function() { + return null; + }, + target: document.body + }]); + } + }); + var newElement = document.createElement("span"); + document_observer.observe(document, { subtree: true, childList: true }); + newElement.id = "inserted_element"; + newElement.setAttribute("style", "display: none"); + newElement.textContent = "my new span for n00"; + document.body.appendChild(newElement); + await Promise.resolve(); + document_observer.disconnect(); + }); + + it('002', async () => { + var testCounter = 0; + function removalMO(sequence, obs) { + testCounter++; + if (testCounter == 1) { + checkRecords(document, sequence, + [{type: "childList", + removedNodes: function () { + return [ newElement]; + }, + previousSibling: function () { + return null; + }, + target: document.body}]); + } + } + var document2_observer; + var newElement = document.createElement("span"); + newElement.id = "inserted_element"; + newElement.setAttribute("style", "display: none"); + newElement.textContent = "my new span for n00"; + document.body.appendChild(newElement); + + document2_observer = new MutationObserver(removalMO); + document2_observer.observe(document, {subtree:true,childList:true}); + document.body.removeChild(newElement); + await Promise.resolve(); + document2_observer.disconnect(); + }); +}); + +describe("Mutation Observer disconnect", function() { + it('001', async () => { + const n00 = document.createElement('p'); + document.body.appendChild(n00); + + function observerCallback(sequence) { + assert_equals(sequence.length, 1); + assert_equals(sequence[0].type, "attributes"); + assert_equals(sequence[0].attributeName, "id"); + assert_equals(sequence[0].oldValue, "latest"); + } + + var observer = new MutationObserver(observerCallback); + observer.observe(n00, {"attributes": true}); + n00.id = "foo"; + n00.id = "bar"; + observer.disconnect(); + observer.observe(n00, {"attributes": true, "attributeOldValue": true}); + n00.id = "latest"; + observer.disconnect(); + observer.observe(n00, {"attributes": true, "attributeOldValue": true}); + n00.id = "n0000"; + await Promise.resolve(); + observer.disconnect(); + }); +}); + +describe("Mutation Observer callback arguments", function() { + it('001', async () => { + const moTarget = createElement('div', { + id: 'mo-target' + }, []); + + const mo = new MutationObserver(function(records, observer) { + // @ts-ignore + assert_equals(this, mo); + assert_equals(arguments.length, 2); + assert_true(Array.isArray(records)); + assert_equals(records.length, 1); + assert_true(records[0] instanceof MutationRecord); + assert_equals(observer, mo); + + mo.disconnect(); + }); + mo.observe(moTarget, {attributes: true}); + moTarget.className = "trigger-mutation"; + await Promise.resolve(); + }); +}); + +describe("MutationObserver takeRecords", function() { + it('001', async (done) => { + var n00 = createElement('div', { + id: 'n00' + }); + + var observer = new MutationObserver(() => { + done.fail('the observer callback should not fire'); + }); + observer.observe(n00, { "subtree": true, + "childList": true, + "attributes": true, + "characterData": true, + "attributeOldValue": true, + "characterDataOldValue": true}); + n00.id = "foo"; + n00.id = "bar"; + n00.className = "bar"; + n00.textContent = "old data"; + // @ts-ignore + n00.firstChild.data = "new data"; + + checkRecords(n00, observer.takeRecords(), [ + {type: "attributes", attributeName: "id", oldValue: "n00"}, + {type: "attributes", attributeName: "id", oldValue: "foo"}, + {type: "attributes", attributeName: "class"}, + {type: "childList", addedNodes: [n00.firstChild]}, + {type: "characterData", oldValue: "old data", target: n00.firstChild} + ]); + + checkRecords(n00, observer.takeRecords(), []); + + await Promise.resolve(); + done(); + }); +}); + +describe("Mutation Observer Styles", function() { + it('Changes to CSS declaration block should queue mutation record for style attribute', async () => { + function createTestElement(style) { + let wrapper = document.createElement("div"); + wrapper.innerHTML = `
`; + return wrapper.querySelector("#test"); + } + let elem = createTestElement("z-index: 40;"); + // @ts-ignore + let style = elem!.style; + assert_equals(style.cssText, "z-index: 40;"); + // Create an observer for the element. + let observer = new MutationObserver(function() {}); + // @ts-ignore + observer.observe(elem, {attributes: true, attributeOldValue: true}); + function assert_record_with_old_value(oldValue, action) { + let records = observer.takeRecords(); + assert_equals(records.length, 1, "number of mutation records after " + action); + let record = records[0]; + assert_equals(record.type, "attributes", "mutation type after " + action); + assert_equals(record.attributeName, "style", "mutated attribute after " + action); + assert_equals(record.oldValue, oldValue, "old value after " + action); + } + style.setProperty("z-index", "41"); + assert_record_with_old_value("z-index: 40;", "changing property in CSS declaration block"); + style.cssText = "z-index: 42;"; + assert_record_with_old_value("z-index: 41;", "changing cssText"); + style.cssText = "z-index: 42;"; + assert_record_with_old_value("z-index: 42;", "changing cssText with the same content"); + style.removeProperty("z-index"); + assert_record_with_old_value("z-index: 42;", "removing property from CSS declaration block"); + // Mutation to shorthand properties should also trigger only one mutation record. + style.setProperty("margin", "1px"); + assert_record_with_old_value("", "adding shorthand property to CSS declaration block"); + style.removeProperty("margin"); + assert_record_with_old_value("margin: 1px;", "removing shorthand property from CSS declaration block"); + // Final sanity check. + // @ts-ignore + assert_equals(elem.getAttribute("style"), ""); + }); +}); + +function log_test(func, expected, description) { + it( description, async function(done) { + var actual: string[] = []; + function log(entry: string) { + actual.push(entry); + if (expected.length == actual.length) { + assert_array_equals(actual, expected); + done(); + } + } + func(log); + }); +} + +describe("MutationObserver microtask looping", function() { + log_test(function(log) { + log('script start'); + + setTimeout(function() { + log('setTimeout'); + }, 0); + + Promise.resolve().then(function() { + log('promise1'); + }).then(function() { + log('promise2'); + }); + + log('script end'); + }, [ + 'script start', + 'script end', + 'promise1', + 'promise2', + 'setTimeout' + ], 'Basic task and microtask ordering'); + + log_test(function(log) { + const container = createElement('div', { + className: 'outer' + }, [ + createElement('div', { + className: 'inner' + }) + ]); + document.body.appendChild(container); + + // Let's get hold of those elements + var outer = document.querySelector('.outer'); + var inner = document.querySelector('.inner'); + + // Let's listen for attribute changes on the + // outer element + new MutationObserver(function() { + log('mutate'); + }).observe(outer!, { + attributes: true + }); + + // Here's a click listener... + function onClick() { + log('click'); + // + setTimeout(function() { + log('timeout'); + }, 0); + + Promise.resolve().then(function() { + log('promise'); + }); + // + outer!.setAttribute('data-random', Math.random().toString()); + } + + // ...which we'll attach to both elements + inner!.addEventListener('click', onClick); + outer!.addEventListener('click', onClick); + + // Note that this will behave differently than a real click, + // since the dispatch is synchronous and microtasks will not + // run between event bubbling steps. + // @ts-ignore + inner!.click(); + }, [ + 'click', + 'promise', + 'mutate', + 'click', + 'promise', + 'mutate', + 'timeout', + 'timeout' + ], 'Level 1 bossfight (synthetic click)'); +}); + +function createFragment() { + var fragment = document.createDocumentFragment(); + fragment.appendChild(document.createTextNode("11")); + fragment.appendChild(document.createTextNode("22")); + return fragment; +} + +describe("MutationObserver childList", function() { + it("n00", async () => { + const n00 = createElement("div", { + id: "n00" + }, [ + createElement("span", {}, [ + createText("text content") + ]) + ]); + document.body.appendChild(n00); + runMutationTest(n00, + {"childList":true, "attributes":true}, + [{type: "attributes", attributeName: "class"}], + function() { n00.nodeValue = ""; n00.setAttribute("class", "dummy");}, + "childList Node.nodeValue: no mutation"); + await Promise.resolve(); + }); + + it('n10', async () => { + const n10 = createElement('div', { + id: 'n00', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + runMutationTest(n10, + {"childList":true}, + [{type: "childList", + removedNodes: [n10.firstChild], + addedNodes: function() {return [n10.firstChild]}}], + function() { n10.textContent = "new data"; }, + "childList Node.textContent: replace content mutation"); + }) + + it('n11', async () => { + const n11 = createElement('p', { + id: 'n01', + }); + runMutationTest(n11, + {"childList":true}, + [{type: "childList", + addedNodes: function() {return [n11.firstChild]}}], + function() { n11.textContent = "new data"; }, + "childList Node.textContent: no previous content mutation"); + }); + + it('n12', async () => { + const n12 = createElement('p', { + id: 'n01', + }); + runMutationTest(n12, + {"childList":true, "attributes":true}, + [{type: "attributes", attributeName: "class"}], + function() { n12.textContent = ""; n12.setAttribute("class", "dummy");}, + "childList Node.textContent: textContent no mutation"); + }); + + it('n13', async () => { + const n13 = createElement('div', { + id: 'n13', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + runMutationTest(n13, + {"childList":true}, + [{type: "childList", removedNodes: [n13.firstChild]}], + function() { n13.textContent = ""; }, + "childList Node.textContent: empty string mutation"); + }); + + // it('n20', async () => { + // const n20 = createElement('div', { + // id: 'n20', + // }, [ + // createText('PAS') + // ]); + // n20.appendChild(document.createTextNode("S")); + // runMutationTest(n20, + // {"childList":true}, + // [{type: "childList", + // removedNodes: [n20.lastChild], + // previousSibling: n20.firstChild}], + // function() { n20.normalize(); }, + // "childList Node.normalize mutation"); + // }); + + it('n30', async () => { + const n30 = createElement('div', { + id: 'n30', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + document.body.appendChild(n30); + let d30; + const dummy = createElement('div', { + id: 'dummy' + }, [ + d30 = createElement('span', { + id: 'd30' + }, [ createText('text content') ]) + ]); + document.body.appendChild(dummy); + + runMutationTest(n30, + {"childList":true}, + [{type: "childList", + addedNodes: [d30], + nextSibling: n30.firstChild}], + function() { n30.insertBefore(d30, n30.firstChild); }, + "childList Node.insertBefore: addition mutation"); + }); + + it('n31', async () => { + const n31 = createElement('div', { + id: 'n31', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + document.body.appendChild(n31); + const dummies = createElement('div', { + id: 'dummy' + }, [ + createElement('span', { + id: 'd30' + }, [ createText('text content') ]) + ]); + runMutationTest(n31, + {"childList":true}, + [{type: "childList", + removedNodes: [n31.firstChild]}], + function() { dummies.insertBefore(n31.firstChild!, dummies.firstChild); }, + "childList Node.insertBefore: removal mutation"); + }); + + it('n32', async () => { + const n32 = createElement('div', { + id: 'n32' + }, [ + createElement('span', {}, [ + createText('AN') + ]), + createElement('span', {}, [ + createText('CH') + ]), + createElement('span', {}, [ + createText('GED') + ]), + ]); + document.body.appendChild(n32); + runMutationTest(n32, + {"childList":true}, + [{type: "childList", + removedNodes: [n32.firstChild!.nextSibling], + previousSibling: n32.firstChild, nextSibling: n32.lastChild}, + {type: "childList", + addedNodes: [n32.firstChild!.nextSibling], + nextSibling: n32.firstChild}], + function() { n32.insertBefore(n32.firstChild!.nextSibling!, n32.firstChild); }, + "childList Node.insertBefore: removal and addition mutations"); + }); + + it('n33', async () => { + const n33 = createElement('div', { + id: 'n33', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n33); + var f33 = createFragment(); + runMutationTest(n33, + {"childList":true}, + [{type: "childList", + addedNodes: [f33.firstChild, f33.lastChild], + nextSibling: n33.firstChild}], + function() { n33.insertBefore(f33, n33.firstChild); }, + "childList Node.insertBefore: fragment addition mutations"); + }); + + it('n34', async () => { + const n34 = createElement('div', { + id: 'n34', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n34); + var f34 = createFragment(); + runMutationTest(f34, + {"childList":true}, + [{type: "childList", + removedNodes: [f34.firstChild, f34.lastChild]}], + function() { n34.insertBefore(f34, n34.firstChild); }, + "childList Node.insertBefore: fragment removal mutations"); + }); + + it('n35', async () => { + const n35 = createElement('div', { + id: 'n35', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n35); + + let d35; + const dummy = createElement('div', { + id: 'dummy' + }, [ + d35 = createElement('span', { + id: 'd35' + }, [ createText('text content') ]) + ]); + BODY.append(dummy); + + runMutationTest(n35, + {"childList":true}, + [{type: "childList", + addedNodes: [d35], + previousSibling: n35.firstChild}], + function() { n35.insertBefore(d35, null); }, + "childList Node.insertBefore: last child addition mutation"); + }); + + it('n40', async () => { + const n40 = createElement('div', { + id: 'n40', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n40); + let d40; + const dummy = createElement('div', { + id: 'dummy' + }, [ + d40 = createElement('span', { + id: 'd40' + }, [ createText('text content') ]) + ]); + BODY.append(dummy); + + runMutationTest(n40, + {"childList":true}, + [{type: "childList", + addedNodes: [d40], + previousSibling: n40.firstChild}], + function() { n40.appendChild(d40); }, + "childList Node.appendChild: addition mutation"); + }); + + it('n41', async () => { + const n41 = createElement('div', { + id: 'n41', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n41); + + const dummies = createElement('div', { + id: 'dummy' + }, [ + createElement('span', { + id: 'd35' + }, [ createText('text content') ]) + ]); + BODY.append(dummies); + + runMutationTest(n41, + {"childList":true}, + [{type: "childList", + removedNodes: [n41.firstChild]}], + function() { dummies.appendChild(n41.firstChild!); }, + "childList Node.appendChild: removal mutation"); + }); + + it('n42', async () => { + const n42 = createElement('div', { + id: 'n42' + }, [ + createElement('span', {}, [ + createText('AN') + ]), + createElement('span', {}, [ + createText('CH') + ]), + createElement('span', {}, [ + createText('GED') + ]), + ]); + BODY.append(n42); + runMutationTest(n42, + {"childList":true}, + [{type: "childList", + removedNodes: [n42.firstChild!.nextSibling], + previousSibling: n42.firstChild, nextSibling: n42.lastChild}, + {type: "childList", + addedNodes: [n42.firstChild!.nextSibling], + previousSibling: n42.lastChild}], + function() { n42.appendChild(n42.firstChild!.nextSibling!); }, + "childList Node.appendChild: removal and addition mutations"); + }); + + it('n43', async () => { + const n43 = createElement('div', { + id: 'n43', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n43); + + var f43 = createFragment(); + + runMutationTest(n43, + {"childList":true}, + [{type: "childList", + addedNodes: [f43.firstChild, f43.lastChild], + previousSibling: n43.firstChild}], + function() { n43.appendChild(f43); }, + "childList Node.appendChild: fragment addition mutations"); + }); + + it('n44', async () => { + const n44 = createElement('div', { + id: 'n44', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n44); + + var f44 = createFragment(); + + runMutationTest(f44, + {"childList":true}, + [{type: "childList", + removedNodes: [f44.firstChild, f44.lastChild]}], + function() { n44.appendChild(f44); }, + "childList Node.appendChild: fragment removal mutations"); + + }); + + it('n45', async () => { + var n45 = document.createElement('p'); + var d45 = document.createElement('span'); + runMutationTest(n45, + {"childList":true}, + [{type: "childList", + addedNodes: [d45]}], + function() { n45.appendChild(d45); }, + "childList Node.appendChild: addition outside document tree mutation"); + }); + + it('n50', async () => { + const n50 = createElement('div', { + id: 'n50', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n50); + + let d50; + const dummies = createElement('div', { + id: 'dummy' + }, [ + d50 = createElement('span', { + id: 'd35' + }, [ createText('text content') ]) + ]); + + runMutationTest(n50, + {"childList":true}, + [{type: "childList", + removedNodes: [n50.firstChild], + addedNodes: [d50]}], + function() { n50.replaceChild(d50, n50.firstChild!); }, + "childList Node.replaceChild: replacement mutation"); + }); + + it('n51', async () => { + const n51 = createElement('div', { + id: 'n51', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n51); + + let d51; + const dummies = createElement('div', { + id: 'dummy' + }, [ + d51 = createElement('span', { + id: 'd35' + }, [ createText('text content') ]) + ]); + + runMutationTest(n51, + {"childList":true}, + [{type: "childList", + removedNodes: [n51.firstChild]}], + function() { d51.parentNode.replaceChild(n51.firstChild, d51); }, + "childList Node.replaceChild: removal mutation"); + }); + + it('n52', async () => { + const n52 = createElement('div', { + id: 'n52' + }, [ + createElement('span', {}, [ + createText('NO ') + ]), + createElement('span', {}, [ + createText('CHANGED') + ]) + ]); + BODY.append(n52); + runMutationTest(n52, + {"childList":true}, + [{type: "childList", + removedNodes: [n52.lastChild], + previousSibling: n52.firstChild}, + {type: "childList", + removedNodes: [n52.firstChild], + addedNodes: [n52.lastChild]}], + function() { n52.replaceChild(n52.lastChild!, n52.firstChild!); }, + "childList Node.replaceChild: internal replacement mutation"); + }); + + it('n53', async () => { + const n53 = createElement('div', { + id: 'n53', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n53); + + runMutationTest(n53, + {"childList":true}, + [{type: "childList", + removedNodes: [n53.firstChild]}, + {type: "childList", + addedNodes: [n53.firstChild]}], + function() { n53.replaceChild(n53.firstChild!, n53.firstChild!); }, + "childList Node.replaceChild: self internal replacement mutation"); + }); + + it('n60', async () => { + const n60 = createElement('div', { + id: 'n60', + }, [ + createElement('span', {}, [ + createText('text content') + ]) + ]); + BODY.appendChild(n60); + + runMutationTest(n60, + { "childList": true }, + [{ + type: "childList", + removedNodes: [n60.firstChild] + }], + function() { + n60.removeChild(n60.firstChild!); + }, + "childList Node.removeChild: removal mutation"); + }); +}); + +describe("MutationObserver Attributes", function() { + it("n", async () => { + const n = createElement("p", { id: "n" }); + BODY.append(n); + + runMutationTest(n, + { "attributes": true }, + [{ type: "attributes", attributeName: "id" }], + function() { + n.id = "n000"; + }, + "attributes Element.id: update, no oldValue, mutation"); + + }); + + it("n00", async () => { + const n00 = createElement("p", { id: "n00" }); + BODY.append(n00); + + runMutationTest(n00, + { "attributes": true, "attributeOldValue": true }, + [{ type: "attributes", oldValue: "n00", attributeName: "id" }], + function() { + n00.id = "n000"; + }, + "attributes Element.id: update mutation"); + }); + + it("n01", async () => { + const n01 = createElement("p", { id: "n01" }); + BODY.append(n01); + runMutationTest(n01, + { "attributes": true, "attributeOldValue": true }, + [{ type: "attributes", oldValue: "n01", attributeName: "id" }], + function() { + n01.id = ""; + }, + "attributes Element.id: empty string update mutation"); + + }); + it("n02", async () => { + const n02 = createElement("p", { id: "n02" }); + BODY.append(n02); + runMutationTest(n02, + { "attributes": true, "attributeOldValue": true }, + [{ type: "attributes", oldValue: "n02", attributeName: "id" }, { type: "attributes", attributeName: "class" }], + function() { + n02.id = "n02"; + n02.setAttribute("class", "c01"); + }, + "attributes Element.id: same value mutation"); + + + }); + it("n03", async () => { + const n03 = createElement("p", { id: "n03" }); + BODY.append(n03); + runMutationTest(n03, + { "attributes": true, "attributeOldValue": true }, + [{ type: "attributes", oldValue: "n03", attributeName: "id" }], + function() { + // @ts-ignore + n03.unknown = "c02"; + n03.id = "n030"; + }, + "attributes Element.unknown: IDL attribute no mutation"); + }); + + it('n04', async () => { + const n04 = createElement('input', {id: 'n04', type: 'text'}); + BODY.append(n04); + runMutationTest(n04, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "text", attributeName: "type"}, {type: "attributes", oldValue: "n04", attributeName: "id"}], + + function() { + // @ts-ignore + n04.type = "unknown"; n04.id = "n040";}, + "attributes HTMLInputElement.type: type update mutation"); + + }); + + it('n10', async () => { + const n10 = createElement("p", { id: "n10" }); + BODY.append(n10); + runMutationTest(n10, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", attributeName: "class"}], + function() { n10.className = "c01";}, + "attributes Element.className: new value mutation"); + }); + + it('n11', async () => { + const n11 = createElement("p", { id: "n11" }); + BODY.append(n11); + runMutationTest(n11, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", attributeName: "class"}], + function() { n11.className = "";}, + "attributes Element.className: empty string update mutation"); + }); + + it('n12', async () => { + const n12 = createElement("p", { id: "n12", className: 'c01' }); + BODY.append(n12); + + runMutationTest(n12, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01", attributeName: "class"}], + function() { n12.className = "c01";}, + "attributes Element.className: same value mutation"); + }); + it('n13', async () => { + const n13 = createElement("p", { id: "n13", className: 'c01 c02' }); + BODY.append(n13); + + runMutationTest(n13, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { n13.className = "c01 c02";}, + "attributes Element.className: same multiple values mutation"); + }); + + it('n20', async () => { + const n20 = createElement("p", { id: "n20"}); + BODY.append(n20); + runMutationTest(n20, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", attributeName: "class"}], + function() { n20.classList.add("c01");}, + "attributes Element.classList.add: single token addition mutation"); + }); + it('n21', async () => { + const n21 = createElement("p", { id: "n21"}); + BODY.append(n21); + runMutationTest(n21, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", attributeName: "class"}], + function() { n21.classList.add("c01", "c02", "c03");}, + "attributes Element.classList.add: multiple tokens addition mutation"); + }); + it('n22', async () => { + const n22 = createElement("p", { id: "n22"}); + BODY.append(n22); + runMutationTest(n22, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "n22", attributeName: "id"}], + function() { try { n22.classList.add("c01", "", "c03"); } catch (e) { }; + n22.id = "n220"; }, + "attributes Element.classList.add: syntax err/no mutation"); + }); + it('n23', async () => { + const n23 = createElement("p", { id: "n23"}); + BODY.append(n23); + runMutationTest(n23, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "n23", attributeName: "id"}], + function() { try { n23.classList.add("c01", "c 02", "c03"); } catch (e) { }; + n23.id = "n230"; }, + "attributes Element.classList.add: invalid character/no mutation"); + }); + it('n24', async () => { + const n24 = createElement("p", { id: "n24", className: 'c01 c02'}); + BODY.append(n24); + runMutationTest(n24, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}, {type: "attributes", oldValue: "n24", attributeName: "id"}], + function() { n24.classList.add("c02"); n24.id = "n240";}, + "attributes Element.classList.add: same value mutation"); + }); + + it('n30', async () => { + const n30 = createElement("p", { id: "n30", className: 'c01 c02'}); + BODY.append(n30); + + runMutationTest(n30, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { n30.classList.remove("c01");}, + "attributes Element.classList.remove: single token removal mutation"); + }); + + it('n31', async () => { + const n31 = createElement("p", { id: "n31", className: 'c01 c02'}); + BODY.append(n31); + + runMutationTest(n31, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { n31.classList.remove("c01", "c02");}, + "attributes Element.classList.remove: multiple tokens removal mutation"); + }); + + it('n32', async () => { + const n32 = createElement("p", { id: "n32", className: 'c01 c02'}); + BODY.append(n32); + + runMutationTest(n32, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}, {type: "attributes", oldValue: "n32", attributeName: "id"}], + function() { n32.classList.remove("c03"); n32.id = "n320";}, + "attributes Element.classList.remove: missing token removal mutation"); + }); + + + it('n40', async () => { + const n40 = createElement("p", { id: "n40", className: 'c01 c02'}); + BODY.append(n40); + + runMutationTest(n40, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { n40.classList.toggle("c01");}, + "attributes Element.classList.toggle: token removal mutation"); + }); + + it('n41', async () => { + const n41 = createElement("p", { id: "n41", className: 'c01 c02'}); + BODY.append(n41); + + runMutationTest(n41, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { n41.classList.toggle("c03");}, + "attributes Element.classList.toggle: token addition mutation"); + }); + + it('n42', async () => { + const n42 = createElement("p", { id: "n42", className: 'c01 c02'}); + BODY.append(n42); + + runMutationTest(n42, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { n42.classList.toggle("c01", false);}, + "attributes Element.classList.toggle: forced token removal mutation"); + }); + + it('n43', async () => { + const n43 = createElement("p", { id: "n43", className: 'c01 c02'}); + BODY.append(n43); + + runMutationTest(n43, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "n43", attributeName: "id"}], + function() { n43.classList.toggle("c03", false); n43.id = "n430"; }, + "attributes Element.classList.toggle: forced missing token removal no mutation"); + }); + + it('n44', async () => { + const n44 = createElement("p", { id: "n44", className: 'c01 c02'}); + BODY.append(n44); + + runMutationTest(n44, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "n44", attributeName: "id"}], + function() { n44.classList.toggle("c01", true); n44.id = "n440"; }, + "attributes Element.classList.toggle: forced existing token addition no mutation"); + }); + + it('n45', async () => { + const n45 = createElement("p", { id: "n45", className: 'c01 c02'}); + BODY.append(n45); + + runMutationTest(n45, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { n45.classList.toggle("c03", true);}, + "attributes Element.classList.toggle: forced token addition mutation"); + }); + + it('n50', async () => { + const n50 = createElement("p", { id: "n50", className: 'c01 c02'}); + BODY.append(n50); + + runMutationTest(n50, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}], + function() { + for (var i = 0; i < n50.attributes.length; i++) { + var attr = n50.attributes[i]; + if (attr.localName === "class") { + attr.value = "c03"; + } + }; + }, + "attributes Element.attributes.value: update mutation"); + }); + + // it('n51', async () => { + // const n51 = createElement("p", { id: "n51"}); + // BODY.append(n51); + // runMutationTest(n51, + // {"attributes":true, "attributeOldValue": true}, + // [{type: "attributes", oldValue: "n51", attributeName: "id"}], + // function() { + // n51.attributes[0].value = "n51"; + // }, + // "attributes Element.attributes.value: same id mutation"); + // }); + + it('n60', async () => { + const n60 = createElement("p", { id: "n60"}); + BODY.append(n60); + runMutationTest(n60, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "n60", attributeName: "id"}], + function() { + n60.setAttribute("id", "n601"); + }, + "attributes Element.setAttribute: id mutation"); + }); + + it('n61', async () => { + const n61 = createElement("p", { id: "n61", className: 'c01'}); + BODY.append(n61); + runMutationTest(n61, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01", attributeName: "class"}], + function() { + n61.setAttribute("class", "c01"); + }, + "attributes Element.setAttribute: same class mutation"); + }); + + it('n62', async () => { + const n62 = createElement("p", { id: "n62"}); + BODY.append(n62); + runMutationTest(n62, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", attributeName: "classname"}], + function() { + n62.setAttribute("classname", "c01"); + }, + "attributes Element.setAttribute: classname mutation"); + }); + + it('n70', async () => { + const n70 = createElement("p", { id: "n70", className: 'c01'}); + BODY.append(n70); + + runMutationTest(n70, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "c01", attributeName: "class"}], + function() { + n70.removeAttribute("class"); + }, + "attributes Element.removeAttribute: removal mutation"); + }); + + it('n71', async () => { + const n71 = createElement("p", { id: "n71"}); + BODY.append(n71); + + runMutationTest(n71, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "n71", attributeName: "id"}], + function() { + n71.removeAttribute("class"); + n71.id = "n710"; + }, + "attributes Element.removeAttribute: removal no mutation"); + }); + + it('n72', async () => { + const n72 = createElement('input', {id: 'n72', type: 'text'}); + BODY.append(n72); + runMutationTest(n72, + {"attributes":true, "attributeOldValue": true}, + [{type: "attributes", oldValue: "text", attributeName: "type"}, {type: "attributes", oldValue: "n72", attributeName: "id"}], + function() { + n72.removeAttribute("type"); + n72.id = "n720"; + }, + "childList HTMLInputElement.removeAttribute: type removal mutation"); + }); + + it('n1000', async () => { + const n1000 = createElement("p", { id: "n1000",}); + BODY.append(n1000); + runMutationTest(n1000, + {"attributes":true, "attributeOldValue": true,"attributeFilter": ["id"]}, + [{type: "attributes", oldValue: "n1000", attributeName: "id"}], + function() { n1000.id = "abc"; n1000.className = "c01"}, + "attributes/attributeFilter Element.id/Element.className: update mutation"); + }); + + it('n1001', async () => { + const n1001 = createElement("p", { id: "n1001", className: 'c01' }); + BODY.append(n1001); + runMutationTest(n1001, + {"attributes":true, "attributeOldValue": true,"attributeFilter": ["id", "class"]}, + [{type: "attributes", oldValue: "n1001", attributeName: "id"}, + {type: "attributes", oldValue: "c01", attributeName: "class"}], + function() { n1001.id = "abc"; n1001.className = "c02"; n1001.setAttribute("lang", "fr");}, + "attributes/attributeFilter Element.id/Element.className: multiple filter update mutation"); + }); + + it('n2000', async () => { + const n2000 = createElement("p", { id: "n2000" }); + BODY.append(n2000); + + runMutationTest(n2000, + {"attributeOldValue": true}, + [{type: "attributes", oldValue: "n2000", attributeName: "id"}], + function() { n2000.id = "abc";}, + "attributeOldValue alone Element.id: update mutation"); + }); + + it('n2001', async () => { + const n2001 = createElement("p", { id: "n2001", className: 'c01' }); + BODY.append(n2001); + runMutationTest(n2001, + {"attributeFilter": ["id", "class"]}, + [{type: "attributes", attributeName: "id"}, + {type: "attributes", attributeName: "class"}], + function() { n2001.id = "abcd"; n2001.className = "c02"; n2001.setAttribute("lang", "fr");}, + "attributeFilter alone Element.id/Element.className: multiple filter update mutation"); + }); + + it('n3000', async () => { + const n3000 = createElement("p", { id: "n3000" }); + BODY.append(n3000); + + runMutationTest(n3000, + {"subtree": true, "childList":false, "attributes" : true}, + [{type: "attributes", attributeName: "id" }], + function() { n3000.textContent = "CHANGED"; n3000.id = "abc";}, + "childList false: no childList mutation"); + }); +}); + +describe("MutationObserver inner outer", function() { + it('n00', async () => { + const n00 = createElement('p', {id: 'n00'}, [createText('old text')]); + BODY.append(n00); + var n00oldText = n00.firstChild; + + runMutationTest(n00, + {childList:true,attributes:true}, + [{type: "childList", + removedNodes: [n00oldText], + addedNodes: function() { + return [n00.firstChild]; + }}, + {type: "attributes", attributeName: "class"}], + function() { n00.innerHTML = "new text"; n00.className = "c01"}, + "innerHTML mutation"); + }); + + it('n01', async () => { + const n01 = createElement('p', {id: 'n01'}, [createText('old text')]); + BODY.append(n01); + var n01oldText = n01.firstChild; + runMutationTest(n01, + {childList:true}, + [{type: "childList", + removedNodes: [n01oldText], + addedNodes: function() { + return [n01.firstChild, + n01.lastChild]; + }}], + function() { n01.innerHTML = "newtext"; }, + "innerHTML with 2 children mutation"); + }); + + it('n02', async () => { + const n02 = createElement('div', { id: 'n02'}, [ + createElement('p', {}, [ + createText('old text') + ]) + ]); + BODY.append(n02); + runMutationTest(n02, + {childList:true}, + [{type: "childList", + removedNodes: [n02.firstChild], + addedNodes: function() { + return [n02.firstChild]; + }}], + // @ts-ignore + function() { n02.firstChild!.outerHTML = "

next text

"; }, + "outerHTML mutation"); + + }); +}); + +describe("MutationObserver CharacterData", function() { + it('n', async () => { + const n = createElement('p', {id: 'n'}, [createText('text content')]); + BODY.append(n); + runMutationTest(n.firstChild, + {"characterData":true}, + [{type: "characterData"}], + // @ts-ignore + function() { n.firstChild.data = "NEW VALUE"; }, + "characterData Text.data: simple mutation without oldValue"); + }); + it('n00', async () => { + const n00 = createElement('p', {id: 'n00'}, [createText('text content')]); + BODY.append(n00); + runMutationTest(n00.firstChild, + {"characterData":true,"characterDataOldValue":true}, + [{type: "characterData", oldValue: "text content" }], + // @ts-ignore + function() { n00.firstChild.data = "CHANGED"; }, + "characterData Text.data: simple mutation"); + }); +}); \ No newline at end of file