Title: [205315] trunk
Revision
205315
Author
rn...@webkit.org
Date
2016-09-01 14:16:40 -0700 (Thu, 01 Sep 2016)

Log Message

Add "whenDefined" to CustomElementRegistry
https://bugs.webkit.org/show_bug.cgi?id=161425

Reviewed by Yusuke Suzuki.

Source/WebCore:

Add the support for "whenDefined" method on CustomElementRegistry: 
https://html.spec.whatwg.org/#dom-customelementregistry-whendefined

Because it needs to store the newly created promise when the queried custom element has not been defined yet,
we need to write custom binding code instead of relying on the binding generator.

Tests: fast/custom-elements/CustomElementRegistry.html

* bindings/js/JSCustomElementRegistryCustom.cpp:
(WebCore::validateCustomElementNameAndThrowIfNeeded): Extracted out of JSCustomElementRegistry::define.
(WebCore::JSCustomElementRegistry::define): Fulfill the "whenDefined" promise when the definition succeeds.
(WebCore::whenDefinedPromise): Added. Return an existing promise if there is one, or create a new promise.
We cache the created promise only if the custom element had not been defined yet since we'll indefinitely
retain the resolved promise otherwise.
(WebCore::JSCustomElementRegistry::whenDefined): Added. Calls whenDefinedPromise and returns a rejected
promise when there was an exception.
* dom/CustomElementRegistry.cpp:
* dom/CustomElementRegistry.h:
(WebCore::CustomElementRegistry::promiseMap): Added.
* dom/CustomElementRegistry.idl:

LayoutTests:

Added test cases for "whenDefined" method.

* fast/custom-elements/CustomElementRegistry-expected.txt:
* fast/custom-elements/CustomElementRegistry.html:

Modified Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (205314 => 205315)


--- trunk/LayoutTests/ChangeLog	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/LayoutTests/ChangeLog	2016-09-01 21:16:40 UTC (rev 205315)
@@ -1,3 +1,15 @@
+2016-09-01  Ryosuke Niwa  <rn...@webkit.org>
+
+        Add "whenDefined" to CustomElementRegistry
+        https://bugs.webkit.org/show_bug.cgi?id=161425
+
+        Reviewed by Yusuke Suzuki.
+
+        Added test cases for "whenDefined" method.
+
+        * fast/custom-elements/CustomElementRegistry-expected.txt:
+        * fast/custom-elements/CustomElementRegistry.html:
+
 2016-09-01  Nikita Vasilyev  <nvasil...@apple.com>
 
         Web Inspector: Minification detection produces false positives for small resources

Modified: trunk/LayoutTests/fast/custom-elements/CustomElementRegistry-expected.txt (205314 => 205315)


--- trunk/LayoutTests/fast/custom-elements/CustomElementRegistry-expected.txt	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/LayoutTests/fast/custom-elements/CustomElementRegistry-expected.txt	2016-09-01 21:16:40 UTC (rev 205315)
@@ -27,4 +27,10 @@
 PASS "get" must return undefined when the registry does not contain an entry with the given name 
 PASS "get" must return undefined when the registry does not contain an entry with the given name even if the name was not a valid custom element name 
 PASS "get" return the constructor of the entry with the given name when there is a matching entry. 
+PASS "whenDefined" must return a promise for a valid custom element name 
+PASS "whenDefined" must return the same promise each time invoked for a valid custom element name which has not been defined 
+PASS "whenDefined" must return an unresolved promise when the registry does not contain the entry with the given name 
+PASS "whenDefined" must return a resolved promise when the registry contains the entry with the given name 
+PASS "whenDefined" must return a new resolved promise each time invoked when the registry contains the entry with the given name 
+PASS A promise returned by "whenDefined" must be resolved by "define" 
 

Modified: trunk/LayoutTests/fast/custom-elements/CustomElementRegistry.html (205314 => 205315)


--- trunk/LayoutTests/fast/custom-elements/CustomElementRegistry.html	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/LayoutTests/fast/custom-elements/CustomElementRegistry.html	2016-09-01 21:16:40 UTC (rev 205315)
@@ -407,6 +407,115 @@
     assert_equals(customElements.get('existing-custom-element'), ExistingCustomElement);
 }, '"get" return the constructor of the entry with the given name when there is a matching entry.');
 
+test(function () {
+    assert_true(customElements.whenDefined('some-name') instanceof Promise);
+}, '"whenDefined" must return a promise for a valid custom element name');
+
+test(function () {
+    assert_equals(customElements.whenDefined('some-name'), customElements.whenDefined('some-name'));
+}, '"whenDefined" must return the same promise each time invoked for a valid custom element name which has not been defined');
+
+promise_test(function () {
+    var resolved = false;
+    var rejected = false;
+    customElements.whenDefined('a-b').then(function () { resolved = true; }, function () { rejected = true; });
+    return Promise.resolve().then(function () {
+        assert_false(resolved, 'The promise returned by "whenDefined" must not be resolved until a custom element is defined');
+        assert_false(rejected, 'The promise returned by "whenDefined" must not be rejected until a custom element is defined');
+    });    
+}, '"whenDefined" must return an unresolved promise when the registry does not contain the entry with the given name')
+/*
+promise_test(function () {
+    var promise = customElements.whenDefined('badname');
+    promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; });
+
+    assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+    assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+    return Promise.resolve().then(function () {
+        assert_false('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+        assert_true('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+    });
+}, '"whenDefined" must return a rejected promise when the given name is not a valid custom element name');
+*/
+promise_test(function () {
+    customElements.define('preexisting-custom-element', class extends HTMLElement { });
+
+    var promise = customElements.whenDefined('preexisting-custom-element');
+    promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; });
+
+    assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+    assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+    return Promise.resolve().then(function () {
+        assert_true('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+        assert_equals(promise.resolved, undefined,
+            'The promise returned by "whenDefined" must be resolved with "undefined" when a custom element is defined');
+        assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+    });
+}, '"whenDefined" must return a resolved promise when the registry contains the entry with the given name');
+
+promise_test(function () {
+    class AnotherExistingCustomElement extends HTMLElement {};
+    customElements.define('another-existing-custom-element', AnotherExistingCustomElement);
+
+    var promise1 = customElements.whenDefined('another-existing-custom-element');
+    var promise2 = customElements.whenDefined('another-existing-custom-element');
+    promise1.then(function (value) { promise1.resolved = value; }, function (value) { promise1.rejected = value; });
+    promise2.then(function (value) { promise2.resolved = value; }, function (value) { promise2.rejected = value; });
+
+    assert_not_equals(promise1, promise2);
+    assert_false('resolved' in promise1, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+    assert_false('resolved' in promise2, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+    assert_false('rejected' in promise1, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+    assert_false('rejected' in promise2, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+    return Promise.resolve().then(function () {
+        assert_true('resolved' in promise1, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+        assert_equals(promise1.resolved, undefined, 'The promise returned by "whenDefined" must be resolved with "undefined" when a custom element is defined');
+        assert_false('rejected' in promise1, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+
+        assert_true('resolved' in promise2, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+        assert_equals(promise2.resolved, undefined, 'The promise returned by "whenDefined" must be resolved with "undefined" when a custom element is defined');
+        assert_false('rejected' in promise2, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+    });
+}, '"whenDefined" must return a new resolved promise each time invoked when the registry contains the entry with the given name');
+
+promise_test(function () {
+    var promise = customElements.whenDefined('element-defined-after-whendefined');
+    promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; });
+
+    assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+    assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+    var promiseAfterDefine;
+    return Promise.resolve().then(function () {
+        assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the element is defined');
+        assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the element is defined');
+        assert_equals(customElements.whenDefined('element-defined-after-whendefined'), promise,
+            '"whenDefined" must return the same unresolved promise before the custom element is defined');
+        customElements.define('element-defined-after-whendefined', class extends HTMLElement { });
+        assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+        assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+        promiseAfterDefine = customElements.whenDefined('element-defined-after-whendefined');
+        promiseAfterDefine.then(function (value) { promiseAfterDefine.resolved = value; }, function (value) { promiseAfterDefine.rejected = value; });
+        assert_not_equals(promiseAfterDefine, promise, '"whenDefined" must return a resolved promise once the custom element is defined');
+        assert_false('resolved' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+        assert_false('rejected' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+    }).then(function () {
+        assert_true('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+        assert_equals(promise.resolved, undefined,
+            'The promise returned by "whenDefined" must be resolved with "undefined" when a custom element is defined');
+        assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+
+        assert_true('resolved' in promiseAfterDefine, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+        assert_equals(promiseAfterDefine.resolved, undefined,
+            'The promise returned by "whenDefined" must be resolved with "undefined" when a custom element is defined');
+        assert_false('rejected' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+    });
+}, 'A promise returned by "whenDefined" must be resolved by "define"');
+
 </script>
 </body>
 </html>

Modified: trunk/Source/WebCore/ChangeLog (205314 => 205315)


--- trunk/Source/WebCore/ChangeLog	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/Source/WebCore/ChangeLog	2016-09-01 21:16:40 UTC (rev 205315)
@@ -1,3 +1,31 @@
+2016-09-01  Ryosuke Niwa  <rn...@webkit.org>
+
+        Add "whenDefined" to CustomElementRegistry
+        https://bugs.webkit.org/show_bug.cgi?id=161425
+
+        Reviewed by Yusuke Suzuki.
+
+        Add the support for "whenDefined" method on CustomElementRegistry: 
+        https://html.spec.whatwg.org/#dom-customelementregistry-whendefined
+
+        Because it needs to store the newly created promise when the queried custom element has not been defined yet,
+        we need to write custom binding code instead of relying on the binding generator.
+
+        Tests: fast/custom-elements/CustomElementRegistry.html
+
+        * bindings/js/JSCustomElementRegistryCustom.cpp:
+        (WebCore::validateCustomElementNameAndThrowIfNeeded): Extracted out of JSCustomElementRegistry::define.
+        (WebCore::JSCustomElementRegistry::define): Fulfill the "whenDefined" promise when the definition succeeds.
+        (WebCore::whenDefinedPromise): Added. Return an existing promise if there is one, or create a new promise.
+        We cache the created promise only if the custom element had not been defined yet since we'll indefinitely
+        retain the resolved promise otherwise.
+        (WebCore::JSCustomElementRegistry::whenDefined): Added. Calls whenDefinedPromise and returns a rejected
+        promise when there was an exception.
+        * dom/CustomElementRegistry.cpp:
+        * dom/CustomElementRegistry.h:
+        (WebCore::CustomElementRegistry::promiseMap): Added.
+        * dom/CustomElementRegistry.idl:
+
 2016-09-01  Alex Christensen  <achristen...@webkit.org>
 
         URLParser should handle . and .. in URL paths

Modified: trunk/Source/WebCore/bindings/js/JSCustomElementRegistryCustom.cpp (205314 => 205315)


--- trunk/Source/WebCore/bindings/js/JSCustomElementRegistryCustom.cpp	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/Source/WebCore/bindings/js/JSCustomElementRegistryCustom.cpp	2016-09-01 21:16:40 UTC (rev 205315)
@@ -32,6 +32,7 @@
 #include "JSCustomElementInterface.h"
 #include "JSDOMBinding.h"
 #include "JSDOMConvert.h"
+#include "JSDOMPromise.h"
 
 using namespace JSC;
 
@@ -56,6 +57,27 @@
     return callback.getObject();
 }
 
+static bool validateCustomElementNameAndThrowIfNeeded(ExecState& state, const AtomicString& name)
+{
+    auto scope = DECLARE_THROW_SCOPE(state.vm());
+
+    switch (Document::validateCustomElementName(name)) {
+    case CustomElementNameValidationStatus::Valid:
+        return true;
+    case CustomElementNameValidationStatus::ConflictsWithBuiltinNames:
+        throwSyntaxError(&state, scope, ASCIILiteral("Custom element name cannot be same as one of the builtin elements"));
+        return false;
+    case CustomElementNameValidationStatus::NoHyphen:
+        throwSyntaxError(&state, scope, ASCIILiteral("Custom element name must contain a hyphen"));
+        return false;
+    case CustomElementNameValidationStatus::ContainsUpperCase:
+        throwSyntaxError(&state, scope, ASCIILiteral("Custom element name cannot contain an upper case letter"));
+        return false;
+    }
+    ASSERT_NOT_REACHED();
+    return false;
+}
+
 // https://html.spec.whatwg.org/#dom-customelementregistry-define
 JSValue JSCustomElementRegistry::define(ExecState& state)
 {
@@ -74,23 +96,9 @@
         return throwTypeError(&state, scope, ASCIILiteral("The second argument must be a constructor"));
     JSObject* constructor = constructorValue.getObject();
 
-    // FIXME: Throw a TypeError if constructor doesn't inherit from HTMLElement.
-    // https://github.com/w3c/webcomponents/issues/541
+    if (!validateCustomElementNameAndThrowIfNeeded(state, localName))
+        return jsUndefined();
 
-    switch (Document::validateCustomElementName(localName)) {
-    case CustomElementNameValidationStatus::Valid:
-        break;
-    case CustomElementNameValidationStatus::ConflictsWithBuiltinNames:
-        return throwSyntaxError(&state, scope, ASCIILiteral("Custom element name cannot be same as one of the builtin elements"));
-    case CustomElementNameValidationStatus::NoHyphen:
-        return throwSyntaxError(&state, scope, ASCIILiteral("Custom element name must contain a hyphen"));
-    case CustomElementNameValidationStatus::ContainsUpperCase:
-        return throwSyntaxError(&state, scope, ASCIILiteral("Custom element name cannot contain an upper case letter"));
-    }
-
-    // FIXME: Check re-entrancy here.
-    // https://github.com/w3c/webcomponents/issues/545
-
     CustomElementRegistry& registry = wrapped();
 
     if (registry.elementDefinitionIsRunning()) {
@@ -155,10 +163,58 @@
 
     // FIXME: 17. Let map be registry's upgrade candidates map.
     // FIXME: 18. Upgrade a newly-defined element given map and definition.
-    // FIXME: 19. Resolve whenDefined promise.
 
+    auto& promiseMap = registry.promiseMap();
+    auto promise = promiseMap.take(localName);
+    if (promise)
+        promise.value()->resolve(nullptr);
+
     return jsUndefined();
 }
+
+// https://html.spec.whatwg.org/#dom-customelementregistry-whendefined
+static JSValue whenDefinedPromise(ExecState& state, JSDOMGlobalObject& globalObject, CustomElementRegistry& registry)
+{
+    auto scope = DECLARE_THROW_SCOPE(state.vm());
+
+    if (UNLIKELY(state.argumentCount() < 1))
+        return throwException(&state, scope, createNotEnoughArgumentsError(&state));
+
+    AtomicString localName(state.uncheckedArgument(0).toString(&state)->toAtomicString(&state));
+    if (UNLIKELY(state.hadException()))
+        return jsUndefined();
+
+    if (!validateCustomElementNameAndThrowIfNeeded(state, localName))
+        return jsUndefined();
+
+    if (registry.findInterface(localName)) {
+        auto& jsPromise = *JSPromiseDeferred::create(&state, &globalObject);
+        DeferredWrapper::create(&state, &globalObject, &jsPromise)->resolve(nullptr);
+        return jsPromise.promise();
+    }
+
+    auto result = registry.promiseMap().ensure(localName, [&] {
+        return DeferredWrapper::create(&state, &globalObject, JSPromiseDeferred::create(&state, &globalObject));
+    });
+
+    return result.iterator->value->promise();
+}
+
+JSValue JSCustomElementRegistry::whenDefined(ExecState& state)
+{
+    JSDOMGlobalObject& globalObject = *jsCast<JSDOMGlobalObject*>(state.lexicalGlobalObject());
+    auto& promiseDeferred = *JSPromiseDeferred::create(&state, &globalObject);
+    JSValue promise = whenDefinedPromise(state, globalObject, wrapped());
+
+    if (state.hadException()) {
+        rejectPromiseWithExceptionIfAny(state, globalObject, promiseDeferred);
+        ASSERT(!state.hadException());
+        return promiseDeferred.promise();
+    }
+
+    return promise;
+}
+
 #endif
 
 }

Modified: trunk/Source/WebCore/dom/CustomElementRegistry.cpp (205314 => 205315)


--- trunk/Source/WebCore/dom/CustomElementRegistry.cpp	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/Source/WebCore/dom/CustomElementRegistry.cpp	2016-09-01 21:16:40 UTC (rev 205315)
@@ -31,6 +31,7 @@
 #include "Document.h"
 #include "Element.h"
 #include "JSCustomElementInterface.h"
+#include "JSDOMPromise.h"
 #include "MathMLNames.h"
 #include "QualifiedName.h"
 #include "SVGNames.h"

Modified: trunk/Source/WebCore/dom/CustomElementRegistry.h (205314 => 205315)


--- trunk/Source/WebCore/dom/CustomElementRegistry.h	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/Source/WebCore/dom/CustomElementRegistry.h	2016-09-01 21:16:40 UTC (rev 205315)
@@ -43,6 +43,7 @@
 namespace WebCore {
 
 class CustomElementRegistry;
+class DeferredWrapper;
 class Element;
 class JSCustomElementInterface;
 class QualifiedName;
@@ -64,6 +65,8 @@
 
     JSC::JSValue get(const AtomicString&);
 
+    HashMap<AtomicString, Ref<DeferredWrapper>>& promiseMap() { return m_promiseMap; }
+
 private:
     CustomElementRegistry();
 
@@ -70,6 +73,7 @@
     HashMap<AtomicString, Vector<RefPtr<Element>>> m_upgradeCandidatesMap;
     HashMap<AtomicString, Ref<JSCustomElementInterface>> m_nameMap;
     HashMap<const JSC::JSObject*, JSCustomElementInterface*> m_constructorMap;
+    HashMap<AtomicString, Ref<DeferredWrapper>> m_promiseMap;
 
     bool m_elementDefinitionIsRunning { false };
 

Modified: trunk/Source/WebCore/dom/CustomElementRegistry.idl (205314 => 205315)


--- trunk/Source/WebCore/dom/CustomElementRegistry.idl	2016-09-01 21:04:49 UTC (rev 205314)
+++ trunk/Source/WebCore/dom/CustomElementRegistry.idl	2016-09-01 21:16:40 UTC (rev 205315)
@@ -32,5 +32,6 @@
 
     [CEReactions, Custom] void define(DOMString name, Function constructor);
     any get(DOMString name);
+    [RaisesExceptionWithMessage, Custom] Promise whenDefined(DOMString name);
 
 };
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to