diff --git a/Tests/LibWeb/Text/expected/DOM/shadow-root-boundary-of-inserted-node-is-traversed.txt b/Tests/LibWeb/Text/expected/DOM/shadow-root-boundary-of-inserted-node-is-traversed.txt new file mode 100644 index 00000000000000..2bfbd60d9c4dd4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/DOM/shadow-root-boundary-of-inserted-node-is-traversed.txt @@ -0,0 +1 @@ +Hello from script in the shadow root of the just inserted div! diff --git a/Tests/LibWeb/Text/expected/HTML/iframe-successfully-loads-in-shadow-root.txt b/Tests/LibWeb/Text/expected/HTML/iframe-successfully-loads-in-shadow-root.txt new file mode 100644 index 00000000000000..e3c171e4a7e464 --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/iframe-successfully-loads-in-shadow-root.txt @@ -0,0 +1,2 @@ +Received a message: 'Hello from iframe in the shadow root of the just inserted div!' +Was it from the shadow root iframe? true diff --git a/Tests/LibWeb/Text/input/DOM/shadow-root-boundary-of-inserted-node-is-traversed.html b/Tests/LibWeb/Text/input/DOM/shadow-root-boundary-of-inserted-node-is-traversed.html new file mode 100644 index 00000000000000..b545db3d620b73 --- /dev/null +++ b/Tests/LibWeb/Text/input/DOM/shadow-root-boundary-of-inserted-node-is-traversed.html @@ -0,0 +1,14 @@ + + + diff --git a/Tests/LibWeb/Text/input/HTML/iframe-successfully-loads-in-shadow-root.html b/Tests/LibWeb/Text/input/HTML/iframe-successfully-loads-in-shadow-root.html new file mode 100644 index 00000000000000..21ba827ee83b4d --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/iframe-successfully-loads-in-shadow-root.html @@ -0,0 +1,25 @@ + + + diff --git a/Userland/Libraries/LibWeb/CSS/MediaQueryList.cpp b/Userland/Libraries/LibWeb/CSS/MediaQueryList.cpp index cb8b9de37df90c..b83597611c847e 100644 --- a/Userland/Libraries/LibWeb/CSS/MediaQueryList.cpp +++ b/Userland/Libraries/LibWeb/CSS/MediaQueryList.cpp @@ -73,7 +73,7 @@ bool MediaQueryList::evaluate() } // https://www.w3.org/TR/cssom-view/#dom-mediaquerylist-addlistener -void MediaQueryList::add_listener(DOM::IDLEventListener* listener) +void MediaQueryList::add_listener(JS::GCPtr listener) { // 1. If listener is null, terminate these steps. if (!listener) @@ -87,12 +87,13 @@ void MediaQueryList::add_listener(DOM::IDLEventListener* listener) } // https://www.w3.org/TR/cssom-view/#dom-mediaquerylist-removelistener -void MediaQueryList::remove_listener(DOM::IDLEventListener* listener) +void MediaQueryList::remove_listener(JS::GCPtr listener) { // 1. Remove an event listener from the associated list of event listeners, whose type is change, callback is listener, and capture is false. // NOTE: While the spec doesn't technically use remove_event_listener and instead manipulates the list directly, every major engine uses remove_event_listener. // This means if an event listener removes another event listener that comes after it, the removed event listener will not be invoked. - remove_event_listener_without_options(HTML::EventNames::change, *listener); + if (listener) + remove_event_listener_without_options(HTML::EventNames::change, *listener); } void MediaQueryList::set_onchange(WebIDL::CallbackType* event_handler) diff --git a/Userland/Libraries/LibWeb/CSS/MediaQueryList.h b/Userland/Libraries/LibWeb/CSS/MediaQueryList.h index 728f9e7bf63c01..1a6832612d6ec9 100644 --- a/Userland/Libraries/LibWeb/CSS/MediaQueryList.h +++ b/Userland/Libraries/LibWeb/CSS/MediaQueryList.h @@ -26,8 +26,8 @@ class MediaQueryList final : public DOM::EventTarget { bool matches() const; bool evaluate(); - void add_listener(DOM::IDLEventListener*); - void remove_listener(DOM::IDLEventListener*); + void add_listener(JS::GCPtr); + void remove_listener(JS::GCPtr); void set_onchange(WebIDL::CallbackType*); WebIDL::CallbackType* onchange(); diff --git a/Userland/Libraries/LibWeb/DOM/ShadowRoot.h b/Userland/Libraries/LibWeb/DOM/ShadowRoot.h index c76c3b28878a1e..d0a1c01350d913 100644 --- a/Userland/Libraries/LibWeb/DOM/ShadowRoot.h +++ b/Userland/Libraries/LibWeb/DOM/ShadowRoot.h @@ -97,21 +97,37 @@ class ShadowRoot final : public DocumentFragment { template<> inline bool Node::fast_is() const { return node_type() == to_underlying(NodeType::DOCUMENT_FRAGMENT_NODE) && is_shadow_root(); } +// https://dom.spec.whatwg.org/#concept-shadow-including-tree-order +// In shadow-including tree order is shadow-including preorder, depth-first traversal of a node tree. +// Shadow-including preorder, depth-first traversal of a node tree tree is preorder, depth-first traversal +// of tree, with for each shadow host encountered in tree, shadow-including preorder, depth-first traversal +// of that element’s shadow root’s node tree just after it is encountered. + +// https://dom.spec.whatwg.org/#concept-shadow-including-descendant +// An object A is a shadow-including descendant of an object B, if A is a descendant of B, or A’s root is a +// shadow root and A’s root’s host is a shadow-including inclusive descendant of B. + +// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-descendant +// A shadow-including inclusive descendant is an object or one of its shadow-including descendants. + template inline TraversalDecision Node::for_each_shadow_including_inclusive_descendant(Callback callback) { if (callback(*this) == TraversalDecision::Break) return TraversalDecision::Break; - for (auto* child = first_child(); child; child = child->next_sibling()) { - if (child->is_element()) { - if (auto shadow_root = static_cast(child)->shadow_root()) { - if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break) - return TraversalDecision::Break; - } + + if (is_element()) { + if (auto shadow_root = static_cast(this)->shadow_root()) { + if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break) + return TraversalDecision::Break; } + } + + for (auto* child = first_child(); child; child = child->next_sibling()) { if (child->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break) return TraversalDecision::Break; } + return TraversalDecision::Continue; } @@ -119,12 +135,6 @@ template inline TraversalDecision Node::for_each_shadow_including_descendant(Callback callback) { for (auto* child = first_child(); child; child = child->next_sibling()) { - if (child->is_element()) { - if (JS::GCPtr shadow_root = static_cast(child)->shadow_root()) { - if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break) - return TraversalDecision::Break; - } - } if (child->for_each_shadow_including_inclusive_descendant(callback) == TraversalDecision::Break) return TraversalDecision::Break; } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLIFrameElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLIFrameElement.cpp index da0cdad6379dcb..1cf23a8e5b025d 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLIFrameElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLIFrameElement.cpp @@ -58,17 +58,26 @@ void HTMLIFrameElement::inserted() { HTMLElement::inserted(); - // When an iframe element element is inserted into a document whose browsing context is non-null, the user agent must run these steps: - if (in_a_document_tree() && document().browsing_context() && document().is_fully_active()) { - // 1. Create a new child navigable for element. - MUST(create_new_child_navigable(JS::create_heap_function(realm().heap(), [this] { - // 3. Process the iframe attributes for element, with initialInsertion set to true. - process_the_iframe_attributes(true); - set_content_navigable_initialized(); - }))); - - // FIXME: 2. If element has a sandbox attribute, then parse the sandboxing directive given the attribute's value and element's iframe sandboxing flag set. - } + // The iframe HTML element insertion steps, given insertedNode, are: + // 1. If insertedNode's shadow-including root's browsing context is null, then return. + if (!is(shadow_including_root())) + return; + + DOM::Document& document = verify_cast(shadow_including_root()); + + // NOTE: The check for "not fully active" is to prevent a crash on the dom/nodes/node-appendchild-crash.html WPT test. + if (!document.browsing_context() || !document.is_fully_active()) + return; + + // 2. Create a new child navigable for insertedNode. + MUST(create_new_child_navigable(JS::create_heap_function(realm().heap(), [this] { + // FIXME: 3. If insertedNode has a sandbox attribute, then parse the sandboxing directive given the attribute's + // value and insertedNode's iframe sandboxing flag set. + + // 4. Process the iframe attributes for insertedNode, with initialInsertion set to true. + process_the_iframe_attributes(true); + set_content_navigable_initialized(); + }))); } // https://html.spec.whatwg.org/multipage/iframe-embed-object.html#process-the-iframe-attributes diff --git a/Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp b/Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp index 57c23b0e921f0c..80fd4011f0f9a1 100644 --- a/Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp +++ b/Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -92,7 +93,6 @@ void AudioContext::initialize(JS::Realm& realm) void AudioContext::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); - visitor.visit(m_pending_promises); visitor.visit(m_pending_resume_promises); } @@ -107,7 +107,6 @@ AudioTimestamp AudioContext::get_output_timestamp() WebIDL::ExceptionOr> AudioContext::resume() { auto& realm = this->realm(); - auto& vm = realm.vm(); // 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException. auto const& associated_document = verify_cast(HTML::relevant_global_object(*this)).associated_document(); @@ -128,8 +127,8 @@ WebIDL::ExceptionOr> AudioContext::resume() // 5. If the context is not allowed to start, append promise to [[pending promises]] and [[pending resume promises]] and abort these steps, returning promise. if (m_allowed_to_start) { - TRY_OR_THROW_OOM(vm, m_pending_promises.try_append(promise)); - TRY_OR_THROW_OOM(vm, m_pending_resume_promises.try_append(promise)); + m_pending_promises.append(promise); + m_pending_resume_promises.append(promise); } // 6. Set the [[control thread state]] on the AudioContext to running. @@ -147,28 +146,36 @@ WebIDL::ExceptionOr> AudioContext::resume() if (!start_rendering_audio_graph()) { // 7.4: In case of failure, queue a media element task to execute the following steps: queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, this]() { + HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // 7.4.1: Reject all promises from [[pending resume promises]] in order, then clear [[pending resume promises]]. for (auto const& promise : m_pending_resume_promises) { WebIDL::reject_promise(realm, promise, JS::js_null()); + + // 7.4.2: Additionally, remove those promises from [[pending promises]]. + m_pending_promises.remove_first_matching([&promise](auto& pending_promise) { + return pending_promise == promise; + }); } m_pending_resume_promises.clear(); - - // FIXME: 7.4.2: Additionally, remove those promises from [[pending promises]]. })); } // 7.5: queue a media element task to execute the following steps: queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() { + HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // 7.5.1: Resolve all promises from [[pending resume promises]] in order. + // 7.5.2: Clear [[pending resume promises]]. Additionally, remove those promises from + // [[pending promises]]. for (auto const& pending_resume_promise : m_pending_resume_promises) { *pending_resume_promise->resolve(); + m_pending_promises.remove_first_matching([&pending_resume_promise](auto& pending_promise) { + return pending_promise == pending_resume_promise; + }); } - - // 7.5.2: Clear [[pending resume promises]]. m_pending_resume_promises.clear(); - // FIXME: Additionally, remove those promises from [[pending promises]]. - // 7.5.3: Resolve promise. *promise->resolve(); @@ -192,7 +199,6 @@ WebIDL::ExceptionOr> AudioContext::resume() WebIDL::ExceptionOr> AudioContext::suspend() { auto& realm = this->realm(); - auto& vm = realm.vm(); // 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException. auto const& associated_document = verify_cast(HTML::relevant_global_object(*this)).associated_document(); @@ -209,7 +215,7 @@ WebIDL::ExceptionOr> AudioContext::suspend() } // 4. Append promise to [[pending promises]]. - TRY_OR_THROW_OOM(vm, m_pending_promises.try_append(promise)); + m_pending_promises.append(promise); // 5. Set [[suspended by user]] to true. m_suspended_by_user = true; @@ -227,6 +233,8 @@ WebIDL::ExceptionOr> AudioContext::suspend() // 7.3: queue a media element task to execute the following steps: queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() { + HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // 7.3.1: Resolve promise. *promise->resolve(); @@ -280,6 +288,8 @@ WebIDL::ExceptionOr> AudioContext::close() // 5.4: queue a media element task to execute the following steps: queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() { + HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // 5.4.1: Resolve promise. *promise->resolve(); diff --git a/Userland/Libraries/LibWeb/WebAudio/AudioContext.h b/Userland/Libraries/LibWeb/WebAudio/AudioContext.h index a1155f567813c9..daad254029c034 100644 --- a/Userland/Libraries/LibWeb/WebAudio/AudioContext.h +++ b/Userland/Libraries/LibWeb/WebAudio/AudioContext.h @@ -49,7 +49,6 @@ class AudioContext final : public BaseAudioContext { double m_output_latency { 0 }; bool m_allowed_to_start = true; - Vector> m_pending_promises; Vector> m_pending_resume_promises; bool m_suspended_by_user = false; diff --git a/Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.cpp b/Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.cpp index df7ca9064ca92b..7839b56d3b0f9f 100644 --- a/Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.cpp +++ b/Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.cpp @@ -43,6 +43,7 @@ void BaseAudioContext::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_destination); + visitor.visit(m_pending_promises); } void BaseAudioContext::set_onstatechange(WebIDL::CallbackType* event_handler) @@ -144,7 +145,8 @@ JS::NonnullGCPtr BaseAudioContext::decode_audio_data(JS::Handle BaseAudioContext::decode_audio_data(JS::Handle m_destination; + Vector> m_pending_promises; private: void queue_a_decoding_operation(JS::NonnullGCPtr, JS::Handle, JS::GCPtr, JS::GCPtr);