Title: [204553] trunk
Revision
204553
Author
[email protected]
Date
2016-08-16 21:42:53 -0700 (Tue, 16 Aug 2016)

Log Message

customElements.define should retrieve lifecycle callbacks
https://bugs.webkit.org/show_bug.cgi?id=160797

Reviewed by Chris Dumez.

Source/WebCore:

Updated JSCustomElementInterface to invoke Get(constructor, "prototype") and Get(prototype, callbackName)
for each lifecycle callback as required by the latest specification:
https://html.spec.whatwg.org/#dom-customelementsregistry-define

Also added the support for "observedAttributes" property on the custom elements constructor which defines
the list of attributes for which attributeChangedCallback is invoked.

Test: fast/custom-elements/CustomElementsRegistry.html

* bindings/js/JSCustomElementInterface.cpp:
(WebCore::JSCustomElementInterface::setAttributeChangedCallback): Added.
(WebCore::JSCustomElementInterface::attributeChanged): Invoke m_attributeChangedCallback instead of on the
result of Get(element, "attributeChangedCallback").
* bindings/js/JSCustomElementInterface.h:
(WebCore::JSCustomElementInterface::observesAttribute): Added.

* bindings/js/JSCustomElementsRegistryCustom.cpp:
(WebCore::getLifecycleCallback): Added.
(WebCore::JSCustomElementsRegistry::define): Invoke Get(prototype, callbackName) for each callback. Also
store attributedChangedCallback and observedAttributes to JSCustomElementInterface. Other callbacks will
be stored in the future when the support for those callbacks are added.

* dom/Element.cpp:
(WebCore::Element::attributeChanged): Moved more code into enqueueAttributeChangedCallbackIfNeeded.

* dom/LifecycleCallbackQueue.cpp:
(WebCore::LifecycleCallbackQueue::enqueueAttributeChangedCallbackIfNeeded): Added an early exit for when
the given attribute is not observed by the custom element. Also moved the logic to retrieve
JSCustomElementInterface from Element::attributeChanged and renamed it from enqueueAttributeChangedCallback.

* bindings/js/JSDOMBinding.h:
(WebCore::toNativeArray): Throw a TypeError when the argument is not an object.
* bindings/js/JSDOMConvert.h:
(WebCore::Converter<Vector<T>>::convert): Removed a FIXME comment.

LayoutTests:

Added test cases for CustomElementsRegistry.define to make sure it invokes Get(constructor, "prototype")
and Get(prototype, callbackName) for each lifecycle callback.

Also updated the tests to reflect the support for observedAttributes which specifies the list of attributes
for which attributeChangedCallback is invoked.

* fast/custom-elements/CustomElementsRegistry-expected.txt: Renamed from Document-defineElement-expected.txt.
* fast/custom-elements/CustomElementsRegistry.html: Renamed from Document-defineElement.html.
* fast/custom-elements/Document-defineElement-expected.txt: Removed.
* fast/custom-elements/Document-defineElement.html: Removed.
* fast/custom-elements/attribute-changed-callback-expected.txt:
* fast/custom-elements/attribute-changed-callback.html: Added test cases for "observedAttributes".
* fast/custom-elements/lifecycle-callback-timing.html:

Modified Paths

Added Paths

Removed Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (204552 => 204553)


--- trunk/LayoutTests/ChangeLog	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/LayoutTests/ChangeLog	2016-08-17 04:42:53 UTC (rev 204553)
@@ -1,3 +1,24 @@
+2016-08-16  Ryosuke Niwa  <[email protected]>
+
+        customElements.define should retrieve lifecycle callbacks
+        https://bugs.webkit.org/show_bug.cgi?id=160797
+
+        Reviewed by Chris Dumez.
+
+        Added test cases for CustomElementsRegistry.define to make sure it invokes Get(constructor, "prototype")
+        and Get(prototype, callbackName) for each lifecycle callback.
+
+        Also updated the tests to reflect the support for observedAttributes which specifies the list of attributes
+        for which attributeChangedCallback is invoked.
+
+        * fast/custom-elements/CustomElementsRegistry-expected.txt: Renamed from Document-defineElement-expected.txt.
+        * fast/custom-elements/CustomElementsRegistry.html: Renamed from Document-defineElement.html.
+        * fast/custom-elements/Document-defineElement-expected.txt: Removed.
+        * fast/custom-elements/Document-defineElement.html: Removed.
+        * fast/custom-elements/attribute-changed-callback-expected.txt:
+        * fast/custom-elements/attribute-changed-callback.html: Added test cases for "observedAttributes".
+        * fast/custom-elements/lifecycle-callback-timing.html:
+
 2016-08-16  Zalan Bujtas  <[email protected]>
 
         Subpixel rendering: Cleanup RenderLayerBacking::updateGeometry.

Copied: trunk/LayoutTests/fast/custom-elements/CustomElementsRegistry-expected.txt (from rev 204552, trunk/LayoutTests/fast/custom-elements/Document-defineElement-expected.txt) (0 => 204553)


--- trunk/LayoutTests/fast/custom-elements/CustomElementsRegistry-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/fast/custom-elements/CustomElementsRegistry-expected.txt	2016-08-17 04:42:53 UTC (rev 204553)
@@ -0,0 +1,20 @@
+
+PASS CustomElementsRegistry interface must have define as a method 
+PASS customElements.define must throw with an invalid name 
+PASS customElements.define must throw when there is already a custom element of the same name 
+PASS customElements.define must throw when there is already a custom element with the same class 
+PASS customElements.define must throw when the element interface is not a constructor 
+PASS customElements.define must get "prototype" property of the constructor 
+PASS customElements.define must rethrow an exception thrown while getting "prototype" property of the constructor 
+PASS customElements.define must throw when "prototype" property of the constructor is not an object 
+PASS customElements.define must get callbacks of the constructor prototype 
+PASS customElements.define must rethrow an exception thrown while getting callbacks on the constructor prototype 
+PASS customElements.define must rethrow an exception thrown while converting a callback value to Function callback type 
+PASS customElements.define must get "observedAttributes" property on the constructor prototype when "attributeChangedCallback" is present 
+PASS customElements.define must rethrow an exception thrown while getting observedAttributes on the constructor prototype 
+PASS customElements.define must rethrow an exception thrown while converting the value of observedAttributes to sequence<DOMString> 
+PASS customElements.define must rethrow an exception thrown while iterating over observedAttributes to sequence<DOMString> 
+PASS customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on observedAttributes 
+PASS customElements.define must not throw even if "observedAttributes" fails to convert if "attributeChangedCallback" is not defined 
+PASS customElements.define must define an instantiatable custom element 
+

Copied: trunk/LayoutTests/fast/custom-elements/CustomElementsRegistry.html (from rev 204552, trunk/LayoutTests/fast/custom-elements/Document-defineElement.html) (0 => 204553)


--- trunk/LayoutTests/fast/custom-elements/CustomElementsRegistry.html	                        (rev 0)
+++ trunk/LayoutTests/fast/custom-elements/CustomElementsRegistry.html	2016-08-17 04:42:53 UTC (rev 204553)
@@ -0,0 +1,272 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CustomElementsRegistry interface</title>
+<meta name="author" title="Ryosuke Niwa" href=""
+<meta name="assert" content="CustomElementsRegistry interface must exist">
+<script src=""
+<script src=""
+<link rel='stylesheet' href=''>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test(function () {
+    assert_true('define' in CustomElementsRegistry.prototype, '"define" exists on CustomElementsRegistry.prototype');
+    assert_true('define' in customElements, '"define" exists on window.customElements');
+}, 'CustomElementsRegistry interface must have define as a method');
+
+test(function () {
+    class MyCustomElement extends HTMLElement {};
+
+    assert_throws({'name': 'SyntaxError'}, function () { customElements.define(null, MyCustomElement); },
+        'customElements.define must throw a SyntaxError if the tag name is null');
+    assert_throws({'name': 'SyntaxError'}, function () { customElements.define('', MyCustomElement); },
+        'customElements.define must throw a SyntaxError if the tag name is empty');
+    assert_throws({'name': 'SyntaxError'}, function () { customElements.define('abc', MyCustomElement); },
+        'customElements.define must throw a SyntaxError if the tag name does not contain "-"');
+    assert_throws({'name': 'SyntaxError'}, function () { customElements.define('a-Bc', MyCustomElement); },
+        'customElements.define must throw a SyntaxError if the tag name contains an upper case letter');
+
+    var builtinTagNames = [
+        'annotation-xml',
+        'color-profile',
+        'font-face',
+        'font-face-src',
+        'font-face-uri',
+        'font-face-format',
+        'font-face-name',
+        'missing-glyph'
+    ];
+
+    for (var tagName of builtinTagNames) {
+        assert_throws({'name': 'SyntaxError'}, function () { customElements.define(tagName, MyCustomElement); },
+            'customElements.define must throw a SyntaxError if the tag name is "' + tagName + '"');
+    }
+
+}, 'customElements.define must throw with an invalid name');
+
+test(function () {
+    class SomeCustomElement extends HTMLElement {};
+    class OtherCustomElement extends HTMLElement {};
+
+    customElements.define('some-custom-element', SomeCustomElement);
+    assert_throws({'name': 'NotSupportedError'}, function () { customElements.define('some-custom-element', OtherCustomElement); },
+        'customElements.define must throw a NotSupportedError if the specified tag name is already used');
+
+}, 'customElements.define must throw when there is already a custom element of the same name');
+
+test(function () {
+    class AnotherCustomElement extends HTMLElement {};
+
+    customElements.define('another-custom-element', AnotherCustomElement);
+    assert_throws({'name': 'NotSupportedError'}, function () { customElements.define('some-other-element', AnotherCustomElement); },
+        'customElements.define must throw a NotSupportedError if the specified class already defines an element');
+
+}, 'customElements.define must throw when there is already a custom element with the same class');
+
+test(function () {
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', 1); },
+        'customElements.define must throw a TypeError when the element interface is a number');
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', '123'); },
+        'customElements.define must throw a TypeError when the element interface is a string');
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', {}); },
+        'customElements.define must throw a TypeError when the element interface is an object');
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', []); },
+        'customElements.define must throw a TypeError when the element interface is an array');
+}, 'customElements.define must throw when the element interface is not a constructor');
+
+test(function () {
+    var calls = [];
+    var proxy = new Proxy(class extends HTMLElement { }, {
+        get: function (target, name) {
+            calls.push(name);
+            return target[name];
+        }
+    });
+    customElements.define('proxy-element', proxy);
+    assert_array_equals(calls, ['prototype']);
+}, 'customElements.define must get "prototype" property of the constructor');
+
+test(function () {
+    var proxy = new Proxy(class extends HTMLElement { }, {
+        get: function (target, name) {
+            throw {name: 'expectedError'};
+        }
+    });
+    assert_throws({'name': 'expectedError'}, function () { customElements.define('element-with-string-prototype', proxy); });
+}, 'customElements.define must rethrow an exception thrown while getting "prototype" property of the constructor');
+
+test(function () {
+    var returnedValue;
+    var proxy = new Proxy(class extends HTMLElement { }, {
+        get: function (target, name) { return returnedValue; }
+    });
+
+    returnedValue = null;
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('element-with-string-prototype', proxy); },
+        'customElements.define must throw when "prototype" property of the constructor is null');
+    returnedValue = undefined;
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('element-with-string-prototype', proxy); },
+        'customElements.define must throw when "prototype" property of the constructor is undefined');
+    returnedValue = 'hello';
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('element-with-string-prototype', proxy); },
+        'customElements.define must throw when "prototype" property of the constructor is a string');
+    returnedValue = 1;
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('element-with-string-prototype', proxy); },
+        'customElements.define must throw when "prototype" property of the constructor is a number');
+
+}, 'customElements.define must throw when "prototype" property of the constructor is not an object');
+
+test(function () {
+    var constructor = function () {}
+    var calls = [];
+    constructor.prototype = new Proxy(constructor.prototype, {
+        get: function (target, name) {
+            calls.push(name);
+            return target[name];
+        }
+    });
+    customElements.define('element-with-proxy-prototype', constructor);
+    assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback', 'adoptedCallback', 'attributeChangedCallback']);
+}, 'customElements.define must get callbacks of the constructor prototype');
+
+test(function () {
+    var constructor = function () {}
+    var calls = [];
+    constructor.prototype = new Proxy(constructor.prototype, {
+        get: function (target, name) {
+            calls.push(name);
+            if (name == 'disconnectedCallback')
+                throw {name: 'expectedError'};
+            return target[name];
+        }
+    });
+    assert_throws({'name': 'expectedError'}, function () { customElements.define('element-with-throwing-callback', constructor); });
+    assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback'],
+        'customElements.define must not get callbacks after one of the get throws');
+}, 'customElements.define must rethrow an exception thrown while getting callbacks on the constructor prototype');
+
+test(function () {
+    var constructor = function () {}
+    var calls = [];
+    constructor.prototype = new Proxy(constructor.prototype, {
+        get: function (target, name) {
+            calls.push(name);
+            if (name == 'adoptedCallback')
+                return 1;
+            return target[name];
+        }
+    });
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('element-with-throwing-callback', constructor); });
+    assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback', 'adoptedCallback'],
+        'customElements.define must not get callbacks after one of the conversion throws');
+}, 'customElements.define must rethrow an exception thrown while converting a callback value to Function callback type');
+
+test(function () {
+    var constructor = function () {}
+    constructor.prototype.attributeChangedCallback = function () { };
+    var prototypeCalls = [];
+    var callOrder = 0;
+    constructor.prototype = new Proxy(constructor.prototype, {
+        get: function (target, name) {
+            if (name == 'prototype' || name == 'observedAttributes')
+                throw 'Unexpected access to observedAttributes';
+            prototypeCalls.push(callOrder++);    
+            prototypeCalls.push(name);
+            return target[name];
+        }
+    });
+    var constructorCalls = [];
+    var proxy = new Proxy(constructor, {
+        get: function (target, name) {
+            constructorCalls.push(callOrder++);    
+            constructorCalls.push(name);
+            return target[name];
+        }
+    });
+    customElements.define('element-with-attribute-changed-callback', proxy);
+    assert_array_equals(prototypeCalls, [1, 'connectedCallback', 2, 'disconnectedCallback', 3, 'adoptedCallback', 4, 'attributeChangedCallback']);
+    assert_array_equals(constructorCalls, [0, 'prototype', 5, 'observedAttributes']);
+}, 'customElements.define must get "observedAttributes" property on the constructor prototype when "attributeChangedCallback" is present');
+
+test(function () {
+    var constructor = function () {}
+    constructor.prototype.attributeChangedCallback = function () { };
+    var calls = [];
+    var proxy = new Proxy(constructor, {
+        get: function (target, name) {
+            calls.push(name);
+            if (name == 'observedAttributes')
+                throw {name: 'expectedError'};
+            return target[name];
+        }
+    });
+    assert_throws({'name': 'expectedError'}, function () { customElements.define('element-with-throwing-observed-attributes', proxy); });
+    assert_array_equals(calls, ['prototype', 'observedAttributes'],
+        'customElements.define must get "prototype" and "observedAttributes" on the constructor');
+}, 'customElements.define must rethrow an exception thrown while getting observedAttributes on the constructor prototype');
+
+test(function () {
+    var constructor = function () {}
+    constructor.prototype.attributeChangedCallback = function () { };
+    var calls = [];
+    var proxy = new Proxy(constructor, {
+        get: function (target, name) {
+            calls.push(name);
+            if (name == 'observedAttributes')
+                return 1;
+            return target[name];
+        }
+    });
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('element-with-invalid-observed-attributes', proxy); });
+    assert_array_equals(calls, ['prototype', 'observedAttributes'],
+        'customElements.define must get "prototype" and "observedAttributes" on the constructor');
+}, 'customElements.define must rethrow an exception thrown while converting the value of observedAttributes to sequence<DOMString>');
+
+test(function () {
+    var constructor = function () {}
+    constructor.prototype.attributeChangedCallback = function () { };
+    constructor.observedAttributes = {[Symbol.iterator]: function *() {
+        yield 'foo';
+        throw {name: 'SomeError'};
+    }};
+    assert_throws({'name': 'SomeError'}, function () { customElements.define('element-with-generator-observed-attributes', constructor); });
+}, 'customElements.define must rethrow an exception thrown while iterating over observedAttributes to sequence<DOMString>');
+
+test(function () {
+    var constructor = function () {}
+    constructor.prototype.attributeChangedCallback = function () { };
+    constructor.observedAttributes = {[Symbol.iterator]: 1};
+    assert_throws({'name': 'TypeError'}, function () { customElements.define('element-with-observed-attributes-with-uncallable-iterator', constructor); });
+}, 'customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on observedAttributes');
+
+test(function () {
+    var constructor = function () {}
+    constructor.observedAttributes = 1;
+    customElements.define('element-without-callback-with-invalid-observed-attributes', constructor);
+}, 'customElements.define must not throw even if "observedAttributes" fails to convert if "attributeChangedCallback" is not defined');
+
+test(function () {
+    class MyCustomElement extends HTMLElement {};
+    customElements.define('my-custom-element', MyCustomElement);
+
+    var instance = new MyCustomElement;
+    assert_true(instance instanceof MyCustomElement,
+        'An instance of a custom HTML element be an instance of the associated interface');
+
+    assert_true(instance instanceof HTMLElement,
+        'An instance of a custom HTML element must inherit from HTMLElement');
+
+    assert_equals(instance.localName, 'my-custom-element',
+        'An instance of a custom element must use the associated tag name');
+
+    assert_equals(instance.namespaceURI, 'http://www.w3.org/1999/xhtml',
+        'A custom element HTML must use HTML namespace');
+
+}, 'customElements.define must define an instantiatable custom element');
+
+</script>
+</body>
+</html>

Deleted: trunk/LayoutTests/fast/custom-elements/Document-defineElement-expected.txt (204552 => 204553)


--- trunk/LayoutTests/fast/custom-elements/Document-defineElement-expected.txt	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/LayoutTests/fast/custom-elements/Document-defineElement-expected.txt	2016-08-17 04:42:53 UTC (rev 204553)
@@ -1,8 +0,0 @@
-
-PASS Check the existence of CustomElementsRegistry.prototype.define on CustomElementsRegistry interface 
-PASS customElements.define should throw with an invalid name 
-PASS customElements.define should throw when there is already a custom element of the same name 
-PASS customElements.define should throw when there is already a custom element with the same class 
-PASS customElements.define should throw when the element interface is not a constructor 
-PASS customElements.define should define an instantiatable custom element 
-

Deleted: trunk/LayoutTests/fast/custom-elements/Document-defineElement.html (204552 => 204553)


--- trunk/LayoutTests/fast/custom-elements/Document-defineElement.html	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/LayoutTests/fast/custom-elements/Document-defineElement.html	2016-08-17 04:42:53 UTC (rev 204553)
@@ -1,101 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<title>Custom Elements: Extensions to Document interface</title>
-<meta name="author" title="Ryosuke Niwa" href=""
-<meta name="assert" content="customElements.define should define a custom element">
-<script src=""
-<script src=""
-<link rel='stylesheet' href=''>
-</head>
-<body>
-<div id="log"></div>
-<script>
-
-test(function () {
-    assert_true('define' in CustomElementsRegistry.prototype, '"define" exists on CustomElementsRegistry.prototype');
-    assert_true('define' in customElements, '"define" exists on window.customElements');
-}, 'Check the existence of CustomElementsRegistry.prototype.define on CustomElementsRegistry interface');
-
-test(function () {
-    class MyCustomElement extends HTMLElement {};
-
-    assert_throws({'name': 'SyntaxError'}, function () { customElements.define(null, MyCustomElement); },
-        'customElements.define must throw a SyntaxError if the tag name is null');
-    assert_throws({'name': 'SyntaxError'}, function () { customElements.define('', MyCustomElement); },
-        'customElements.define must throw a SyntaxError if the tag name is empty');
-    assert_throws({'name': 'SyntaxError'}, function () { customElements.define('abc', MyCustomElement); },
-        'customElements.define must throw a SyntaxError if the tag name does not contain "-"');
-    assert_throws({'name': 'SyntaxError'}, function () { customElements.define('a-Bc', MyCustomElement); },
-        'customElements.define must throw a SyntaxError if the tag name contains an upper case letter');
-
-    var builtinTagNames = [
-        'annotation-xml',
-        'color-profile',
-        'font-face',
-        'font-face-src',
-        'font-face-uri',
-        'font-face-format',
-        'font-face-name',
-        'missing-glyph'
-    ];
-
-    for (var tagName of builtinTagNames) {
-        assert_throws({'name': 'SyntaxError'}, function () { customElements.define(tagName, MyCustomElement); },
-            'customElements.define must throw a SyntaxError if the tag name is "' + tagName + '"');
-    }
-
-}, 'customElements.define should throw with an invalid name');
-
-test(function () {
-    class SomeCustomElement extends HTMLElement {};
-    class OtherCustomElement extends HTMLElement {};
-
-    customElements.define('some-custom-element', SomeCustomElement);
-    assert_throws({'name': 'NotSupportedError'}, function () { customElements.define('some-custom-element', OtherCustomElement); },
-        'customElements.define must throw a NotSupportedError if the specified tag name is already used');
-
-}, 'customElements.define should throw when there is already a custom element of the same name');
-
-test(function () {
-    class AnotherCustomElement extends HTMLElement {};
-
-    customElements.define('another-custom-element', AnotherCustomElement);
-    assert_throws({'name': 'NotSupportedError'}, function () { customElements.define('some-other-element', AnotherCustomElement); },
-        'customElements.define must throw a NotSupportedError if the specified class already defines an element');
-
-}, 'customElements.define should throw when there is already a custom element with the same class');
-
-test(function () {
-    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', 1); },
-        'customElements.define must throw a TypeError when the element interface is a number');
-    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', '123'); },
-        'customElements.define must throw a TypeError when the element interface is a string');
-    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', {}); },
-        'customElements.define must throw a TypeError when the element interface is an object');
-    assert_throws({'name': 'TypeError'}, function () { customElements.define('invalid-element', []); },
-        'customElements.define must throw a TypeError when the element interface is an array');
-}, 'customElements.define should throw when the element interface is not a constructor');
-
-test(function () {
-    class MyCustomElement extends HTMLElement {};
-    customElements.define('my-custom-element', MyCustomElement);
-
-    var instance = new MyCustomElement;
-    assert_true(instance instanceof MyCustomElement,
-        'An instance of a custom HTML element be an instance of the associated interface');
-
-    assert_true(instance instanceof HTMLElement,
-        'An instance of a custom HTML element must inherit from HTMLElement');
-
-    assert_equals(instance.localName, 'my-custom-element',
-        'An instance of a custom element must use the associated tag name');
-
-    assert_equals(instance.namespaceURI, 'http://www.w3.org/1999/xhtml',
-        'A custom element HTML must use HTML namespace');
-
-}, 'customElements.define should define an instantiatable custom element');
-
-</script>
-</body>
-</html>

Modified: trunk/LayoutTests/fast/custom-elements/attribute-changed-callback-expected.txt (204552 => 204553)


--- trunk/LayoutTests/fast/custom-elements/attribute-changed-callback-expected.txt	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/LayoutTests/fast/custom-elements/attribute-changed-callback-expected.txt	2016-08-17 04:42:53 UTC (rev 204553)
@@ -3,4 +3,8 @@
 PASS setAttributeNS and removeAttributeNS must enqueue and invoke attributeChangedCallback 
 PASS setAttributeNode and removeAttributeNS must enqueue and invoke attributeChangedCallback 
 PASS setAttributeNode and removeAttributeNS must enqueue and invoke attributeChangedCallback 
+PASS Mutating attributeChangedCallback after calling customElements.define must not affect the callback being invoked 
+PASS attributedChangedCallback must not be invoked when the observed attributes does not contain the attribute. 
+PASS Mutating observedAttributes after calling customElements.define must not affect the set of attributes for which attributedChangedCallback is invoked 
+PASS attributedChangedCallback must be enqueued for attributes specified in a non-Array iterable observedAttributes 
 

Modified: trunk/LayoutTests/fast/custom-elements/attribute-changed-callback.html (204552 => 204553)


--- trunk/LayoutTests/fast/custom-elements/attribute-changed-callback.html	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/LayoutTests/fast/custom-elements/attribute-changed-callback.html	2016-08-17 04:42:53 UTC (rev 204553)
@@ -19,6 +19,7 @@
         argumentList.push({arguments: arguments, value: this.getAttributeNS(namespace, name)});
     }
 }
+MyCustomElement.observedAttributes = ['title', 'id', 'r'];
 customElements.define('my-custom-element', MyCustomElement);
 
 test(function () {
@@ -96,9 +97,97 @@
     assert_equals(argumentList.length, 2);
     assert_equals(argumentList[1].value, null);
     assert_array_equals(argumentList[1].arguments, ['r', '100', null, 'http://www.w3.org/2000/svg']);
-
 }, 'setAttributeNode and removeAttributeNS must enqueue and invoke attributeChangedCallback');
 
+test(function () {
+    var callsToOld = [];
+    var callsToNew = [];
+    class CustomElement extends HTMLElement { }
+    CustomElement.prototype.attributeChangedCallback = function () {
+        callsToOld.push(Array.from(arguments));
+    }
+    CustomElement.observedAttributes = ['title'];
+    customElements.define('element-with-mutated-attribute-changed-callback', CustomElement);
+    CustomElement.prototype.attributeChangedCallback = function () {
+        callsToNew.push(Array.from(arguments));
+    }
+
+    var instance = document.createElement('element-with-mutated-attribute-changed-callback');
+    instance.setAttribute('title', 'hi');
+    assert_equals(instance.getAttribute('title'), 'hi');
+    assert_array_equals(callsToNew, []);
+    assert_equals(callsToOld.length, 1);
+    assert_array_equals(callsToOld[0], ['title', null, 'hi', null]);
+}, 'Mutating attributeChangedCallback after calling customElements.define must not affect the callback being invoked');
+
+test(function () {
+    var calls = [];
+    class CustomElement extends HTMLElement {
+        attributeChangedCallback() {
+            calls.push(Array.from(arguments));
+        }
+    }
+    CustomElement.observedAttributes = ['title'];
+    customElements.define('element-not-observing-id-attribute', CustomElement);
+
+    var instance = document.createElement('element-not-observing-id-attribute');
+    instance.setAttribute('title', 'hi');
+    assert_equals(calls.length, 1);
+    assert_array_equals(calls[0], ['title', null, 'hi', null]);
+    instance.setAttribute('id', 'some');
+    assert_equals(calls.length, 1);
+}, 'attributedChangedCallback must not be invoked when the observed attributes does not contain the attribute.');
+
+test(function () {
+    var calls = [];
+    class CustomElement extends HTMLElement { }
+    CustomElement.prototype.attributeChangedCallback = function () {
+        calls.push(Array.from(arguments));
+    }
+    CustomElement.observedAttributes = ['title', 'lang'];
+    customElements.define('element-with-mutated-observed-attributes', CustomElement);
+    CustomElement.observedAttributes = ['title', 'id'];
+
+    var instance = document.createElement('element-with-mutated-observed-attributes');
+    instance.setAttribute('title', 'hi');
+    assert_equals(calls.length, 1);
+    assert_array_equals(calls[0], ['title', null, 'hi', null]);
+
+    instance.setAttribute('id', 'some');
+    assert_equals(calls.length, 1);
+
+    instance.setAttribute('lang', 'en');
+    assert_equals(calls.length, 2);
+    assert_array_equals(calls[0], ['title', null, 'hi', null]);
+    assert_array_equals(calls[1], ['lang', null, 'en', null]);
+}, 'Mutating observedAttributes after calling customElements.define must not affect the set of attributes for which attributedChangedCallback is invoked');
+
+test(function () {
+    var calls = [];
+    class CustomElement extends HTMLElement { }
+    CustomElement.prototype.attributeChangedCallback = function () {
+        calls.push(Array.from(arguments));
+    }
+    CustomElement.observedAttributes = { [Symbol.iterator]: function *() { yield 'lang'; yield 'style'; } };
+    customElements.define('element-with-generator-observed-attributes', CustomElement);
+
+    var instance = document.createElement('element-with-generator-observed-attributes');
+    instance.setAttribute('lang', 'en');
+    assert_equals(calls.length, 1);
+    assert_array_equals(calls[0], ['lang', null, 'en', null]);
+
+    instance.setAttribute('lang', 'ja');
+    assert_equals(calls.length, 2);
+    assert_array_equals(calls[1], ['lang', 'en', 'ja', null]);
+
+    instance.setAttribute('title', 'hello');
+    assert_equals(calls.length, 2);
+
+    instance.setAttribute('style', 'font-size: 2rem');
+    assert_equals(calls.length, 3);
+    assert_array_equals(calls[2], ['style', null, 'font-size: 2rem', null]);
+}, 'attributedChangedCallback must be enqueued for attributes specified in a non-Array iterable observedAttributes');
+
 </script>
 </body>
 </html>

Modified: trunk/LayoutTests/fast/custom-elements/lifecycle-callback-timing.html (204552 => 204553)


--- trunk/LayoutTests/fast/custom-elements/lifecycle-callback-timing.html	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/LayoutTests/fast/custom-elements/lifecycle-callback-timing.html	2016-08-17 04:42:53 UTC (rev 204553)
@@ -20,6 +20,7 @@
 
     handler() { }
 }
+MyCustomElement.observedAttributes = ['data-title', 'title'];
 customElements.define('my-custom-element', MyCustomElement);
 
 test(function () {

Modified: trunk/Source/WebCore/ChangeLog (204552 => 204553)


--- trunk/Source/WebCore/ChangeLog	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/Source/WebCore/ChangeLog	2016-08-17 04:42:53 UTC (rev 204553)
@@ -1,3 +1,45 @@
+2016-08-16  Ryosuke Niwa  <[email protected]>
+
+        customElements.define should retrieve lifecycle callbacks
+        https://bugs.webkit.org/show_bug.cgi?id=160797
+
+        Reviewed by Chris Dumez.
+
+        Updated JSCustomElementInterface to invoke Get(constructor, "prototype") and Get(prototype, callbackName)
+        for each lifecycle callback as required by the latest specification:
+        https://html.spec.whatwg.org/#dom-customelementsregistry-define
+
+        Also added the support for "observedAttributes" property on the custom elements constructor which defines
+        the list of attributes for which attributeChangedCallback is invoked.
+
+        Test: fast/custom-elements/CustomElementsRegistry.html
+
+        * bindings/js/JSCustomElementInterface.cpp:
+        (WebCore::JSCustomElementInterface::setAttributeChangedCallback): Added.
+        (WebCore::JSCustomElementInterface::attributeChanged): Invoke m_attributeChangedCallback instead of on the
+        result of Get(element, "attributeChangedCallback").
+        * bindings/js/JSCustomElementInterface.h:
+        (WebCore::JSCustomElementInterface::observesAttribute): Added.
+
+        * bindings/js/JSCustomElementsRegistryCustom.cpp:
+        (WebCore::getLifecycleCallback): Added.
+        (WebCore::JSCustomElementsRegistry::define): Invoke Get(prototype, callbackName) for each callback. Also
+        store attributedChangedCallback and observedAttributes to JSCustomElementInterface. Other callbacks will
+        be stored in the future when the support for those callbacks are added.
+
+        * dom/Element.cpp:
+        (WebCore::Element::attributeChanged): Moved more code into enqueueAttributeChangedCallbackIfNeeded.
+
+        * dom/LifecycleCallbackQueue.cpp:
+        (WebCore::LifecycleCallbackQueue::enqueueAttributeChangedCallbackIfNeeded): Added an early exit for when
+        the given attribute is not observed by the custom element. Also moved the logic to retrieve
+        JSCustomElementInterface from Element::attributeChanged and renamed it from enqueueAttributeChangedCallback.
+
+        * bindings/js/JSDOMBinding.h:
+        (WebCore::toNativeArray): Throw a TypeError when the argument is not an object.
+        * bindings/js/JSDOMConvert.h:
+        (WebCore::Converter<Vector<T>>::convert): Removed a FIXME comment.
+
 2016-08-16  Zalan Bujtas  <[email protected]>
 
         Subpixel rendering: Cleanup RenderLayerBacking::updateGeometry.

Modified: trunk/Source/WebCore/bindings/js/JSCustomElementInterface.cpp (204552 => 204553)


--- trunk/Source/WebCore/bindings/js/JSCustomElementInterface.cpp	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/Source/WebCore/bindings/js/JSCustomElementInterface.cpp	2016-08-17 04:42:53 UTC (rev 204553)
@@ -151,6 +151,14 @@
     ASSERT(wrappedElement->isCustomElement());
 }
 
+void JSCustomElementInterface::setAttributeChangedCallback(JSC::JSObject* callback, const Vector<String>& observedAttributes)
+{
+    m_attributeChangedCallback = callback;
+    m_observedAttributes.clear();
+    for (auto& name : observedAttributes)
+        m_observedAttributes.add(name);
+}
+
 void JSCustomElementInterface::attributeChanged(Element& element, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
 {
     if (!canInvokeCallback())
@@ -170,12 +178,9 @@
 
     JSObject* jsElement = asObject(toJS(state, globalObject, element));
 
-    PropertyName attributeChanged(Identifier::fromString(state, "attributeChangedCallback"));
-    JSValue callback = jsElement->get(state, attributeChanged);
     CallData callData;
-    CallType callType = getCallData(callback, callData);
-    if (callType == CallType::None)
-        return;
+    CallType callType = m_attributeChangedCallback->methodTable()->getCallData(m_attributeChangedCallback.get(), callData);
+    ASSERT(callType != CallType::None);
 
     const AtomicString& namespaceURI = attributeName.namespaceURI();
     MarkedArgumentBuffer args;
@@ -187,7 +192,7 @@
     InspectorInstrumentationCookie cookie = JSMainThreadExecState::instrumentFunctionCall(context, callType, callData);
 
     NakedPtr<Exception> exception;
-    JSMainThreadExecState::call(state, callback, callType, callData, jsElement, args, exception);
+    JSMainThreadExecState::call(state, m_attributeChangedCallback.get(), callType, callData, jsElement, args, exception);
 
     InspectorInstrumentation::didCallFunction(cookie, context);
 

Modified: trunk/Source/WebCore/bindings/js/JSCustomElementInterface.h (204552 => 204553)


--- trunk/Source/WebCore/bindings/js/JSCustomElementInterface.h	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/Source/WebCore/bindings/js/JSCustomElementInterface.h	2016-08-17 04:42:53 UTC (rev 204553)
@@ -37,6 +37,7 @@
 #include <wtf/Forward.h>
 #include <wtf/RefCounted.h>
 #include <wtf/RefPtr.h>
+#include <wtf/text/AtomicStringHash.h>
 
 namespace JSC {
 
@@ -65,6 +66,8 @@
 
     void upgradeElement(Element&);
 
+    void setAttributeChangedCallback(JSC::JSObject* callback, const Vector<String>& observedAttributes);
+    bool observesAttribute(const AtomicString& name) const { return m_observedAttributes.contains(name); }
     void attributeChanged(Element&, const QualifiedName&, const AtomicString& oldValue, const AtomicString& newValue);
 
     ScriptExecutionContext* scriptExecutionContext() const { return ContextDestructionObserver::scriptExecutionContext(); }
@@ -83,8 +86,10 @@
 
     QualifiedName m_name;
     mutable JSC::Weak<JSC::JSObject> m_constructor;
+    mutable JSC::Weak<JSC::JSObject> m_attributeChangedCallback;
     RefPtr<DOMWrapperWorld> m_isolatedWorld;
     Vector<RefPtr<Element>, 1> m_constructionStack;
+    HashSet<AtomicString> m_observedAttributes;
 };
 
 } // namespace WebCore

Modified: trunk/Source/WebCore/bindings/js/JSCustomElementsRegistryCustom.cpp (204552 => 204553)


--- trunk/Source/WebCore/bindings/js/JSCustomElementsRegistryCustom.cpp	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/Source/WebCore/bindings/js/JSCustomElementsRegistryCustom.cpp	2016-08-17 04:42:53 UTC (rev 204553)
@@ -31,13 +31,29 @@
 #include "HTMLNames.h"
 #include "JSCustomElementInterface.h"
 #include "JSDOMBinding.h"
+#include "JSDOMConvert.h"
 
 using namespace JSC;
 
 namespace WebCore {
 
-    
 #if ENABLE(CUSTOM_ELEMENTS)
+
+static JSObject* getLifecycleCallback(ExecState& state, JSObject& prototype, const Identifier& id)
+{
+    JSValue callback = prototype.get(&state, id);
+    if (state.hadException())
+        return nullptr;
+    if (callback.isUndefined())
+        return nullptr;
+    if (!callback.isFunction()) {
+        throwTypeError(&state, ASCIILiteral("A lifecycle callback must be a function"));
+        return nullptr;
+    }
+    return callback.getObject();
+}
+
+// https://html.spec.whatwg.org/#dom-customelementsregistry-define
 JSValue JSCustomElementsRegistry::define(ExecState& state)
 {
     if (UNLIKELY(state.argumentCount() < 2))
@@ -53,6 +69,7 @@
     JSObject* constructor = constructorValue.getObject();
 
     // FIXME: Throw a TypeError if constructor doesn't inherit from HTMLElement.
+    // https://github.com/w3c/webcomponents/issues/541
 
     switch (Document::validateCustomElementName(localName)) {
     case CustomElementNameValidationStatus::Valid:
@@ -65,6 +82,9 @@
         return throwSyntaxError(&state, ASCIILiteral("Custom element name cannot contain an upper case letter"));
     }
 
+    // FIXME: Check re-entrancy here.
+    // https://github.com/w3c/webcomponents/issues/545
+
     CustomElementsRegistry& registry = wrapped();
     if (registry.findInterface(localName)) {
         throwNotSupportedError(state, ASCIILiteral("Cannot define multiple custom elements with the same tag name"));
@@ -76,20 +96,51 @@
         return jsUndefined();
     }
 
-    // FIXME: 10. Let prototype be Get(constructor, "prototype"). Rethrow any exceptions.
-    // FIXME: 11. If Type(prototype) is not Object, throw a TypeError exception.
-    // FIXME: 12. Let attachedCallback be Get(prototype, "attachedCallback"). Rethrow any exceptions.
-    // FIXME: 13. Let detachedCallback be Get(prototype, "detachedCallback"). Rethrow any exceptions.
-    // FIXME: 14. Let attributeChangedCallback be Get(prototype, "attributeChangedCallback"). Rethrow any exceptions.
+    auto& vm = globalObject()->vm();
+    JSValue prototypeValue = constructor->get(&state, vm.propertyNames->prototype);
+    if (state.hadException())
+        return jsUndefined();
+    if (!prototypeValue.isObject())
+        return throwTypeError(&state, ASCIILiteral("Custom element constructor's prototype must be an object"));
+    JSObject& prototypeObject = *asObject(prototypeValue);
 
-    PrivateName uniquePrivateName;
-    globalObject()->putDirect(globalObject()->vm(), uniquePrivateName, constructor);
+    // FIXME: Add the support for connectedCallback.
+    getLifecycleCallback(state, prototypeObject, Identifier::fromString(&vm, "connectedCallback"));
+    if (state.hadException())
+        return jsUndefined();
 
+    // FIXME: Add the support for disconnectedCallback.
+    getLifecycleCallback(state, prototypeObject, Identifier::fromString(&vm, "disconnectedCallback"));
+    if (state.hadException())
+        return jsUndefined();
+
+    // FIXME: Add the support for adoptedCallback.
+    getLifecycleCallback(state, prototypeObject, Identifier::fromString(&vm, "adoptedCallback"));
+    if (state.hadException())
+        return jsUndefined();
+
     QualifiedName name(nullAtom, localName, HTMLNames::xhtmlNamespaceURI);
-    registry.addElementDefinition(JSCustomElementInterface::create(name, constructor, globalObject()));
+    auto elementInterface = JSCustomElementInterface::create(name, constructor, globalObject());
 
+    auto* attributeChangedCallback = getLifecycleCallback(state, prototypeObject, Identifier::fromString(&vm, "attributeChangedCallback"));
+    if (state.hadException())
+        return jsUndefined();
+    if (attributeChangedCallback) {
+        auto value = convertOptional<Vector<String>>(state, constructor->get(&state, Identifier::fromString(&state, "observedAttributes")));
+        if (state.hadException())
+            return jsUndefined();
+        if (value)
+            elementInterface->setAttributeChangedCallback(attributeChangedCallback, *value);
+    }
+
+    PrivateName uniquePrivateName;
+    globalObject()->putDirect(vm, uniquePrivateName, constructor);
+
+    registry.addElementDefinition(WTFMove(elementInterface));
+
     // 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.
 
     return jsUndefined();
 }

Modified: trunk/Source/WebCore/dom/Element.cpp (204552 => 204553)


--- trunk/Source/WebCore/dom/Element.cpp	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/Source/WebCore/dom/Element.cpp	2016-08-17 04:42:53 UTC (rev 204553)
@@ -1290,15 +1290,8 @@
     document().incDOMTreeVersion();
 
 #if ENABLE(CUSTOM_ELEMENTS)
-    if (UNLIKELY(isCustomElement())) {
-        if (auto* window = document().domWindow()) {
-            if (auto* registry = window->customElementsRegistry()) {
-                auto* elementInterface = registry->findInterface(tagQName());
-                RELEASE_ASSERT(elementInterface);
-                LifecycleCallbackQueue::enqueueAttributeChangedCallback(*this, *elementInterface, name, oldValue, newValue);
-            }
-        }
-    }
+    if (UNLIKELY(isCustomElement()))
+        LifecycleCallbackQueue::enqueueAttributeChangedCallbackIfNeeded(*this, name, oldValue, newValue);
 #endif
 
     if (valueIsSameAsBefore)

Modified: trunk/Source/WebCore/dom/LifecycleCallbackQueue.cpp (204552 => 204553)


--- trunk/Source/WebCore/dom/LifecycleCallbackQueue.cpp	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/Source/WebCore/dom/LifecycleCallbackQueue.cpp	2016-08-17 04:42:53 UTC (rev 204553)
@@ -28,6 +28,8 @@
 
 #if ENABLE(CUSTOM_ELEMENTS)
 
+#include "CustomElementsRegistry.h"
+#include "DOMWindow.h"
 #include "Document.h"
 #include "Element.h"
 #include "JSCustomElementInterface.h"
@@ -96,11 +98,23 @@
         queue->m_items.append(LifecycleQueueItem(LifecycleQueueItem::Type::ElementUpgrade, element, elementInterface));
 }
 
-void LifecycleCallbackQueue::enqueueAttributeChangedCallback(Element& element, JSCustomElementInterface& elementInterface,
-    const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
+void LifecycleCallbackQueue::enqueueAttributeChangedCallbackIfNeeded(Element& element, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
 {
+    ASSERT(element.isCustomElement());
+    auto* window = element.document().domWindow();
+    if (!window)
+        return;
+
+    auto* registry = window->customElementsRegistry();
+    if (!registry)
+        return;
+
+    auto* elementInterface = registry->findInterface(element.tagQName());
+    if (!elementInterface->observesAttribute(attributeName.localName()))
+        return;
+
     if (auto* queue = CustomElementLifecycleProcessingStack::ensureCurrentQueue())
-        queue->m_items.append(LifecycleQueueItem(element, elementInterface, attributeName, oldValue, newValue));
+        queue->m_items.append(LifecycleQueueItem(element, *elementInterface, attributeName, oldValue, newValue));
 }
 
 void LifecycleCallbackQueue::invokeAll()

Modified: trunk/Source/WebCore/dom/LifecycleCallbackQueue.h (204552 => 204553)


--- trunk/Source/WebCore/dom/LifecycleCallbackQueue.h	2016-08-17 03:18:21 UTC (rev 204552)
+++ trunk/Source/WebCore/dom/LifecycleCallbackQueue.h	2016-08-17 04:42:53 UTC (rev 204553)
@@ -23,8 +23,7 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef LifecycleCallbackQueue_h
-#define LifecycleCallbackQueue_h
+#pragma once
 
 #if ENABLE(CUSTOM_ELEMENTS)
 
@@ -47,10 +46,8 @@
     ~LifecycleCallbackQueue();
 
     static void enqueueElementUpgrade(Element&, JSCustomElementInterface&);
+    static void enqueueAttributeChangedCallbackIfNeeded(Element&, const QualifiedName&, const AtomicString& oldValue, const AtomicString& newValue);
 
-    static void enqueueAttributeChangedCallback(Element&, JSCustomElementInterface&,
-        const QualifiedName&, const AtomicString& oldValue, const AtomicString& newValue);
-
     void invokeAll();
 
 private:
@@ -89,5 +86,3 @@
 }
 
 #endif
-
-#endif // LifecycleCallbackQueue_h
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to