Bug 1081039 - cloneNode on a custom element should call createdCallback if cloned in a document with a custom element definition. r=smaug
authorWilliam Chen <wchen@mozilla.com>
Mon, 22 Dec 2014 18:19:08 -0800
changeset 237755 d3eee7f05cbb0e1de181566b791ffd63f3d29986
parent 237754 2b933356daf722d7f8f170d1f2726419f48548af
child 237756 39853e6515eb6078f2068bf6138f18f0a2d5c2c7
push id7472
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 20:36:27 +0000
treeherdermozilla-aurora@300ca104f8fb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1081039
milestone37.0a1
Bug 1081039 - cloneNode on a custom element should call createdCallback if cloned in a document with a custom element definition. r=smaug
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsIDocument.h
dom/base/nsNodeUtils.cpp
dom/html/nsHTMLContentSink.cpp
dom/tests/mochitest/webcomponents/mochitest.ini
dom/tests/mochitest/webcomponents/test_custom_element_adopt_callbacks.html
dom/tests/mochitest/webcomponents/test_custom_element_clone_callbacks.html
dom/tests/mochitest/webcomponents/test_custom_element_clone_callbacks_extended.html
parser/html/nsHtml5TreeOperation.cpp
testing/web-platform/meta/custom-elements/instantiating-custom-elements/extensions-to-document-interface/create-element-is-attribute.html.ini
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -5419,68 +5419,69 @@ nsIDocument::CreateElement(const nsAStri
                   nullptr, mDefaultElementType, getter_AddRefs(content));
   if (rv.Failed()) {
     return nullptr;
   }
   return dont_AddRef(content.forget().take()->AsElement());
 }
 
 void
-nsDocument::SwizzleCustomElement(Element* aElement,
-                                 const nsAString& aTypeExtension,
-                                 uint32_t aNamespaceID,
-                                 ErrorResult& rv)
-{
-  nsCOMPtr<nsIAtom> typeAtom(do_GetAtom(aTypeExtension));
+nsDocument::SetupCustomElement(Element* aElement,
+                               uint32_t aNamespaceID,
+                               const nsAString* aTypeExtension)
+{
+  if (!mRegistry) {
+    return;
+  }
+
   nsCOMPtr<nsIAtom> tagAtom = aElement->Tag();
-  if (!mRegistry || tagAtom == typeAtom) {
-    return;
+  nsCOMPtr<nsIAtom> typeAtom = aTypeExtension ?
+    do_GetAtom(*aTypeExtension) : tagAtom;
+
+  if (aTypeExtension && !aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::is)) {
+    // Custom element setup in the parser happens after the "is"
+    // attribute is added.
+    aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::is, *aTypeExtension, true);
   }
 
   CustomElementDefinition* data;
   CustomElementHashKey key(aNamespaceID, typeAtom);
   if (!mRegistry->mCustomDefinitions.Get(&key, &data)) {
     // The type extension doesn't exist in the registry,
-    // thus we don't need to swizzle, but it is possibly
-    // an upgrade candidate.
+    // thus we don't need to enqueue callback or adjust
+    // the "is" attribute, but it is possibly an upgrade candidate.
     RegisterUnresolvedElement(aElement, typeAtom);
     return;
   }
 
   if (data->mLocalName != tagAtom) {
     // The element doesn't match the local name for the
     // definition, thus the element isn't a custom element
     // and we don't need to do anything more.
     return;
   }
 
-  if (!aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::is)) {
-    // Swizzling in the parser happens after the "is" attribute is added.
-    aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::is, aTypeExtension, true);
-  }
-
   // Enqueuing the created callback will set the CustomElementData on the
   // element, causing prototype swizzling to occur in Element::WrapObject.
   EnqueueLifecycleCallback(nsIDocument::eCreated, aElement, nullptr, data);
 }
 
 already_AddRefed<Element>
 nsDocument::CreateElement(const nsAString& aTagName,
                           const nsAString& aTypeExtension,
                           ErrorResult& rv)
 {
   nsRefPtr<Element> elem = nsIDocument::CreateElement(aTagName, rv);
   if (rv.Failed()) {
     return nullptr;
   }
 
-  SwizzleCustomElement(elem, aTypeExtension,
-                       GetDefaultNamespaceID(), rv);
-  if (rv.Failed()) {
-    return nullptr;
+  if (!aTagName.Equals(aTypeExtension)) {
+    // Custom element type can not extend itself.
+    SetupCustomElement(elem, GetDefaultNamespaceID(), &aTypeExtension);
   }
 
   return elem.forget();
 }
 
 NS_IMETHODIMP
 nsDocument::CreateElementNS(const nsAString& aNamespaceURI,
                             const nsAString& aQualifiedName,
@@ -5535,19 +5536,19 @@ nsDocument::CreateElementNS(const nsAStr
   if (!aNamespaceURI.EqualsLiteral("*")) {
     rv = nsContentUtils::NameSpaceManager()->RegisterNameSpace(aNamespaceURI,
                                                                nameSpaceId);
     if (rv.Failed()) {
       return nullptr;
     }
   }
 
-  SwizzleCustomElement(elem, aTypeExtension, nameSpaceId, rv);
-  if (rv.Failed()) {
-    return nullptr;
+  if (!aQualifiedName.Equals(aTypeExtension)) {
+    // A custom element type can not extend itself.
+    SetupCustomElement(elem, nameSpaceId, &aTypeExtension);
   }
 
   return elem.forget();
 }
 
 NS_IMETHODIMP
 nsDocument::CreateTextNode(const nsAString& aData, nsIDOMText** aReturn)
 {
@@ -5764,22 +5765,22 @@ nsDocument::CustomElementConstructor(JSC
   nsDependentAtomString localName(definition->mLocalName);
 
   nsCOMPtr<nsIContent> newElement;
   nsresult rv = document->CreateElem(localName, nullptr,
                                      definition->mNamespaceID,
                                      getter_AddRefs(newElement));
   NS_ENSURE_SUCCESS(rv, true);
 
-  ErrorResult errorResult;
   nsCOMPtr<Element> element = do_QueryInterface(newElement);
-  document->SwizzleCustomElement(element, elemName, definition->mNamespaceID,
-                                 errorResult);
-  if (errorResult.Failed()) {
-    return true;
+  if (definition->mLocalName != typeAtom) {
+    // This element is a custom element by extension, thus we need to
+    // do some special setup. For non-extended custom elements, this happens
+    // when the element is created.
+    document->SetupCustomElement(element, definition->mNamespaceID, &elemName);
   }
 
   rv = nsContentUtils::WrapNative(aCx, newElement, newElement, args.rval());
   NS_ENSURE_SUCCESS(rv, true);
 
   return true;
 }
 
@@ -6090,17 +6091,17 @@ nsDocument::RegisterElement(JSContext* a
 
   nsIGlobalObject* sgo = GetScopeObject();
   if (!sgo) {
     rv.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
 
   JS::Rooted<JSObject*> global(aCx, sgo->GetGlobalJSObject());
-  nsCOMPtr<nsIAtom> nameAtom;;
+  nsCOMPtr<nsIAtom> nameAtom;
   int32_t namespaceID = kNameSpaceID_XHTML;
   JS::Rooted<JSObject*> protoObject(aCx);
   {
     JSAutoCompartment ac(aCx, global);
 
     JS::Handle<JSObject*> htmlProto(
       HTMLElementBinding::GetProtoObjectHandle(aCx, global));
     if (!htmlProto) {
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -1552,21 +1552,22 @@ private:
   // custom elements speicification.
   static bool sProcessingBaseElementQueue;
 
   static bool CustomElementConstructor(JSContext* aCx, unsigned aArgc, JS::Value* aVp);
 
 public:
   static void ProcessBaseElementQueue();
 
-  // Modify the prototype and "is" attribute of newly created custom elements.
-  virtual void SwizzleCustomElement(Element* aElement,
-                                    const nsAString& aTypeExtension,
-                                    uint32_t aNamespaceID,
-                                    mozilla::ErrorResult& rv);
+  // Enqueue created callback or register upgrade candidate for
+  // newly created custom elements, possibly extending an existing type.
+  // ex. <x-button>, <button is="x-button> (type extension)
+  virtual void SetupCustomElement(Element* aElement,
+                                  uint32_t aNamespaceID,
+                                  const nsAString* aTypeExtension);
 
   static bool IsWebComponentsEnabled(JSContext* aCx, JSObject* aObject);
 
   // The "registry" from the web components spec.
   nsRefPtr<mozilla::dom::Registry> mRegistry;
 
   nsRefPtr<mozilla::EventListenerManager> mListenerManager;
   nsRefPtr<mozilla::dom::StyleSheetList> mDOMStyleSheets;
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -2232,20 +2232,19 @@ public:
    * element. e.g. <button is="x-button">.
    */
   virtual nsresult RegisterUnresolvedElement(Element* aElement,
                                              nsIAtom* aTypeName = nullptr) = 0;
   virtual void EnqueueLifecycleCallback(ElementCallbackType aType,
                                         Element* aCustomElement,
                                         mozilla::dom::LifecycleCallbackArgs* aArgs = nullptr,
                                         mozilla::dom::CustomElementDefinition* aDefinition = nullptr) = 0;
-  virtual void SwizzleCustomElement(Element* aElement,
-                                    const nsAString& aTypeExtension,
-                                    uint32_t aNamespaceID,
-                                    mozilla::ErrorResult& rv) = 0;
+  virtual void SetupCustomElement(Element* aElement,
+                                  uint32_t aNamespaceID,
+                                  const nsAString* aTypeExtension = nullptr) = 0;
   virtual void
     RegisterElement(JSContext* aCx, const nsAString& aName,
                     const mozilla::dom::ElementRegistrationOptions& aOptions,
                     JS::MutableHandle<JSObject*> aRetval,
                     mozilla::ErrorResult& rv) = 0;
 
   /**
    * In some cases, new document instances must be associated with
--- a/dom/base/nsNodeUtils.cpp
+++ b/dom/base/nsNodeUtils.cpp
@@ -358,16 +358,34 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNod
 
   Element *elem = aNode->IsElement() ? aNode->AsElement() : nullptr;
 
   nsCOMPtr<nsINode> clone;
   if (aClone) {
     rv = aNode->Clone(nodeInfo, getter_AddRefs(clone));
     NS_ENSURE_SUCCESS(rv, rv);
 
+    if (clone->IsElement()) {
+      // The cloned node may be a custom element that may require
+      // enqueing created callback and prototype swizzling.
+      Element* elem = clone->AsElement();
+      if (nsContentUtils::IsCustomElementName(nodeInfo->NameAtom())) {
+        elem->OwnerDoc()->SetupCustomElement(elem, nodeInfo->NamespaceID());
+      } else {
+        // Check if node may be custom element by type extension.
+        // ex. <button is="x-button">
+        nsAutoString extension;
+        if (elem->GetAttr(kNameSpaceID_None, nsGkAtoms::is, extension) &&
+            !extension.IsEmpty()) {
+          elem->OwnerDoc()->SetupCustomElement(elem, nodeInfo->NamespaceID(),
+                                               &extension);
+        }
+      }
+    }
+
     if (aParent) {
       // If we're cloning we need to insert the cloned children into the cloned
       // parent.
       rv = aParent->AppendChildTo(static_cast<nsIContent*>(clone.get()),
                                   false);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     else if (aDeep && clone->IsNodeOfType(nsINode::eDOCUMENT)) {
--- a/dom/html/nsHTMLContentSink.cpp
+++ b/dom/html/nsHTMLContentSink.cpp
@@ -261,23 +261,17 @@ NS_NewHTMLElement(Element** aResult, alr
       nsContentUtils::IsCustomElementName(name)) {
     nsIDocument* doc = nodeInfo->GetDocument();
 
     NS_IF_ADDREF(*aResult = NS_NewHTMLElement(nodeInfo.forget(), aFromParser));
     if (!*aResult) {
       return NS_ERROR_OUT_OF_MEMORY;
     }
 
-    // Element may be unresolved at this point.
-    doc->RegisterUnresolvedElement(*aResult);
-
-    // Try to enqueue a created callback. The custom element data will be set
-    // and created callback will be enqueued if the custom element type
-    // has already been registered.
-    doc->EnqueueLifecycleCallback(nsIDocument::eCreated, *aResult);
+    doc->SetupCustomElement(*aResult, kNameSpaceID_XHTML);
 
     return NS_OK;
   }
 
   *aResult = CreateHTMLElement(tag,
                                nodeInfo.forget(), aFromParser).take();
   return *aResult ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
 }
--- a/dom/tests/mochitest/webcomponents/mochitest.ini
+++ b/dom/tests/mochitest/webcomponents/mochitest.ini
@@ -1,15 +1,18 @@
 [DEFAULT]
 support-files =
   inert_style.css
 
 [test_bug900724.html]
 [test_bug1017896.html]
 [test_content_element.html]
+[test_custom_element_adopt_callbacks.html]
+[test_custom_element_clone_callbacks.html]
+[test_custom_element_clone_callbacks_extended.html]
 [test_nested_content_element.html]
 [test_dest_insertion_points.html]
 [test_dest_insertion_points_shadow.html]
 [test_fallback_dest_insertion_points.html]
 [test_detached_style.html]
 [test_dynamic_content_element_matching.html]
 [test_document_register.html]
 [test_document_register_base_queue.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/webcomponents/test_custom_element_adopt_callbacks.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1081039
+-->
+<head>
+  <title>Test callbacks for adopted custom elements.</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<template id="template"><x-foo></x-foo></template>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1081039">Bug 1081039</a>
+<script>
+
+var p = Object.create(HTMLElement.prototype);
+p.createdCallback = function() {
+  ok(false, "Created callback should not be called for adopted node.");
+};
+
+document.registerElement("x-foo", { prototype: p });
+
+var template = document.getElementById("template");
+var adoptedFoo = document.adoptNode(template.content.firstChild);
+is(adoptedFoo.nodeName, "X-FOO");
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/webcomponents/test_custom_element_clone_callbacks.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1081039
+-->
+<head>
+  <title>Test callbacks for cloned custom elements.</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1081039">Bug 1081039</a>
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to make sure created callback is called on clones that are upgraded and clones
+// created after registering the custom element.
+
+var callbackCalledOnUpgrade = false;
+var callbackCalledOnClone = false;
+
+var foo = document.createElement("x-foo");
+var fooClone = foo.cloneNode(true);
+
+var p = Object.create(HTMLElement.prototype);
+p.createdCallback = function() {
+  is(this.__proto__, p, "Correct prototype should be set on custom elements.");
+
+  if (this == fooClone) {
+    // Callback called for the element created before registering the custom element.
+    // Should be called on element upgrade.
+    is(callbackCalledOnUpgrade, false, "Upgrade should only be called once per clone.");
+    callbackCalledOnUpgrade = true;
+  } else if (this != foo) {
+    // Callback called for the element created after registering the custom element.
+    is(callbackCalledOnClone, false, "Upgrade should only be called once per clone.");
+    callbackCalledOnClone = true;
+  }
+
+  if (callbackCalledOnUpgrade && callbackCalledOnClone) {
+    SimpleTest.finish();
+  }
+};
+
+document.registerElement("x-foo", { prototype: p });
+
+var anotherFooClone = foo.cloneNode(true);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/webcomponents/test_custom_element_clone_callbacks_extended.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1081039
+-->
+<head>
+  <title>Test callbacks for cloned extended custom elements.</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1081039">Bug 1081039</a>
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to make sure created callback is called on clones that are upgraded and clones
+// created after registering the custom element.
+
+var callbackCalledOnUpgrade = false;
+var callbackCalledOnClone = false;
+
+var foo = document.createElement("button", "x-foo");
+is(foo.getAttribute("is"), "x-foo");
+
+var fooClone = foo.cloneNode(true);
+
+var p = Object.create(HTMLButtonElement.prototype);
+p.createdCallback = function() {
+  is(this.__proto__, p, "Correct prototype should be set on custom elements.");
+
+  if (this == fooClone) {
+    // Callback called for the element created before registering the custom element.
+    // Should be called on element upgrade.
+    is(callbackCalledOnUpgrade, false, "Upgrade should only be called once per clone.");
+    callbackCalledOnUpgrade = true;
+  } else if (this != foo) {
+    // Callback called for the element created after registering the custom element.
+    is(callbackCalledOnClone, false, "Upgrade should only be called once per clone.");
+    callbackCalledOnClone = true;
+  }
+
+  if (callbackCalledOnUpgrade && callbackCalledOnClone) {
+    SimpleTest.finish();
+  }
+};
+
+document.registerElement("x-foo", { prototype: p, extends: "button" });
+
+var anotherFooClone = foo.cloneNode(true);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
--- a/parser/html/nsHtml5TreeOperation.cpp
+++ b/parser/html/nsHtml5TreeOperation.cpp
@@ -434,24 +434,21 @@ nsHtml5TreeOperation::CreateElement(int3
     } else {
       nsString& value = *(aAttributes->getValueNoBoundsCheck(i));
       newContent->SetAttr(nsuri,
                           localName,
                           prefix,
                           value,
                           false);
 
-      // Custom element prototype swizzling may be needed if there is an
-      // "is" attribute.
+      // Custom element setup may be needed if there is an "is" attribute.
       if (kNameSpaceID_None == nsuri && !prefix && nsGkAtoms::is == localName) {
-        ErrorResult errorResult;
-        newContent->OwnerDoc()->SwizzleCustomElement(newContent,
-                                                     value,
-                                                     newContent->GetNameSpaceID(),
-                                                     errorResult);
+        newContent->OwnerDoc()->SetupCustomElement(newContent,
+                                                   newContent->GetNameSpaceID(),
+                                                   &value);
       }
     }
   }
   return newContent;
 }
 
 void
 nsHtml5TreeOperation::SetFormElement(nsIContent* aNode, nsIContent* aParent)
--- a/testing/web-platform/meta/custom-elements/instantiating-custom-elements/extensions-to-document-interface/create-element-is-attribute.html.ini
+++ b/testing/web-platform/meta/custom-elements/instantiating-custom-elements/extensions-to-document-interface/create-element-is-attribute.html.ini
@@ -1,14 +1,8 @@
 [create-element-is-attribute.html]
   type: testharness
   [Test Document.createElement() sets the element\'s IS attribute value to type, if type is not the same as localName]
     expected: FAIL
 
-  [Test Document.createElement() sets the element\'s IS attribute value to type, if type is not the same as localName and an element definition with matching localName, namespace, and type is not registered]
-    expected: FAIL
-
   [Test Document.createElementNS() sets the element\'s IS attribute value to type, if type is not the same as localName]
     expected: FAIL
 
-  [Test Document.createElementNS() sets the element\'s IS attribute value to type, if type is not the same as localNameand and an element definition with matching localName, namespace, and type is not registered ]
-    expected: FAIL
-