Bug 1460962 - Support customized built-in element in XUL r=smaug
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 11 May 2018 12:44:46 -0700
changeset 419003 87368ba3ff8bfff560e974ef8b5d1af2a8a1d313
parent 419002 d52c1a18a259f8bc0be20aa246fc9f08f07d2d7c
child 419004 ea2c202529c904f976681e046e2c29777084e0cd
push id103434
push userebalazs@mozilla.com
push dateSat, 19 May 2018 09:46:20 +0000
treeherdermozilla-inbound@7f19d8ef14c4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1460962, 1446247
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1460962 - Support customized built-in element in XUL r=smaug This patch enables us to specify a custom element type with |is| attribute or property when creating a XUL element. Because non-dashed names are valid custom element names in XUL (bug 1446247), other checks has to modified accordingly. The checks I am settling with are 1) Forbids the custom built-in element names to be a non-dashed name. 2) Forbids the custom built-in element to extend a dashed built-in element name. This also ensures the custom built-in element types don't take on the same name as the element name it extends. MozReview-Commit-ID: GCQ9RnfvvrC
dom/base/CustomElementRegistry.cpp
dom/base/Element.cpp
dom/base/nsContentCreatorFunctions.h
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/base/nsNameSpaceManager.cpp
dom/bindings/BindingUtils.cpp
dom/tests/mochitest/webcomponents/test_xul_custom_element.xul
dom/xul/nsXULElement.cpp
dom/xul/nsXULElement.h
--- a/dom/base/CustomElementRegistry.cpp
+++ b/dom/base/CustomElementRegistry.cpp
@@ -761,32 +761,52 @@ CustomElementRegistry::Define(const nsAS
    * 7. If extends is not null, then:
    *    1. If extends is a valid custom element name, then throw a
    *       "NotSupportedError" DOMException.
    *    2. If the element interface for extends and the HTML namespace is
    *       HTMLUnknownElement (e.g., if extends does not indicate an element
    *       definition in this specification), then throw a "NotSupportedError"
    *       DOMException.
    *    3. Set localName to extends.
+   *
+   * Special note for XUL elements:
+   *
+   * For step 7.1, we'll subject XUL to the same rules as HTML, so that a
+   * custom built-in element will not be extending from a dashed name.
+   * Step 7.2 is disregarded. But, we do check if the name is a dashed name
+   * (i.e. step 2) given that there is no reason for a custom built-in element
+   * type to take on a non-dashed name.
+   * This also ensures the name of the built-in custom element type can never
+   * be the same as the built-in element name, so we don't break the assumption
+   * elsewhere.
    */
   nsAutoString localName(aName);
   if (aOptions.mExtends.WasPassed()) {
     RefPtr<nsAtom> extendsAtom(NS_Atomize(aOptions.mExtends.Value()));
-    if (nsContentUtils::IsCustomElementName(extendsAtom, nameSpaceID)) {
+    if (nsContentUtils::IsCustomElementName(extendsAtom, kNameSpaceID_XHTML)) {
       aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
       return;
     }
 
-    // bgsound and multicol are unknown html element.
-    int32_t tag = nsHTMLTags::CaseSensitiveAtomTagToId(extendsAtom);
-    if (tag == eHTMLTag_userdefined ||
-        tag == eHTMLTag_bgsound ||
-        tag == eHTMLTag_multicol) {
-      aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
-      return;
+    if (nameSpaceID == kNameSpaceID_XHTML) {
+      // bgsound and multicol are unknown html element.
+      int32_t tag = nsHTMLTags::CaseSensitiveAtomTagToId(extendsAtom);
+      if (tag == eHTMLTag_userdefined ||
+          tag == eHTMLTag_bgsound ||
+          tag == eHTMLTag_multicol) {
+        aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+        return;
+      }
+    } else { // kNameSpaceID_XUL
+      // As stated above, ensure the name of the customized built-in element
+      // (the one that goes to the |is| attribute) is a dashed name.
+      if (!nsContentUtils::IsNameWithDash(nameAtom)) {
+        aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+        return;
+      }
     }
 
     localName.Assign(aOptions.mExtends.Value());
   }
 
   /**
    * 8. If this CustomElementRegistry's element definition is running flag is set,
    *    then throw a "NotSupportedError" DOMException and abort these steps.
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -4273,20 +4273,37 @@ Element::ClearServoData(nsIDocument* aDo
 void
 Element::SetCustomElementData(CustomElementData* aData)
 {
   nsExtendedDOMSlots *slots = ExtendedDOMSlots();
   MOZ_ASSERT(!slots->mCustomElementData, "Custom element data may not be changed once set.");
   #if DEBUG
     nsAtom* name = NodeInfo()->NameAtom();
     nsAtom* type = aData->GetCustomElementType();
-    if (nsContentUtils::IsCustomElementName(name, NodeInfo()->NamespaceID())) {
-      MOZ_ASSERT(type == name);
-    } else {
-      MOZ_ASSERT(type != name);
+    if (NodeInfo()->NamespaceID() == kNameSpaceID_XHTML) {
+      if (nsContentUtils::IsCustomElementName(name, kNameSpaceID_XHTML)) {
+        MOZ_ASSERT(type == name);
+      } else {
+        MOZ_ASSERT(type != name);
+      }
+    } else { // kNameSpaceID_XUL
+      // Check to see if the tag name is a dashed name.
+      if (nsContentUtils::IsNameWithDash(name)) {
+        // Assert that a tag name with dashes is always an autonomous custom
+        // element.
+        MOZ_ASSERT(type == name);
+      } else {
+        // Could still be an autonomous custom element with a non-dashed tag name.
+        // Need the check below for sure.
+        if (type != name) {
+          // Assert that the name of the built-in custom element type is always
+          // a dashed name.
+          MOZ_ASSERT(nsContentUtils::IsNameWithDash(type));
+        }
+      }
     }
   #endif
   slots->mCustomElementData = aData;
 }
 
 CustomElementDefinition*
 Element::GetCustomElementDefinition() const
 {
--- a/dom/base/nsContentCreatorFunctions.h
+++ b/dom/base/nsContentCreatorFunctions.h
@@ -57,17 +57,19 @@ CreateHTMLElement(uint32_t aNodeType,
 nsresult
 NS_NewMathMLElement(mozilla::dom::Element** aResult,
                     already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
 
 #ifdef MOZ_XUL
 nsresult
 NS_NewXULElement(mozilla::dom::Element** aResult,
                  already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
-                 mozilla::dom::FromParser aFromParser);
+                 mozilla::dom::FromParser aFromParser,
+                 nsAtom* aIsAtom = nullptr,
+                 mozilla::dom::CustomElementDefinition* aDefinition = nullptr);
 
 void
 NS_TrustedNewXULElement(mozilla::dom::Element** aResult,
                         already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
 #endif
 
 nsresult
 NS_NewSVGElement(mozilla::dom::Element** aResult,
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -3178,23 +3178,18 @@ nsContentUtils::NewURIWithDocumentCharse
                      aDocument->GetDocumentCharacterSet(),
                      aBaseURI, sIOService);
   }
   return NS_NewURI(aResult, aSpec, nullptr, aBaseURI, sIOService);
 }
 
 // static
 bool
-nsContentUtils::IsCustomElementName(nsAtom* aName, uint32_t aNameSpaceID)
-{
-  // Allow non-dashed names in XUL for XBL to Custom Element migrations.
-  if (aNameSpaceID == kNameSpaceID_XUL) {
-    return true;
-  }
-
+nsContentUtils::IsNameWithDash(nsAtom* aName)
+{
   // A valid custom element name is a sequence of characters name which
   // must match the PotentialCustomElementName production:
   // PotentialCustomElementName ::= [a-z] (PCENChar)* '-' (PCENChar)*
   const char16_t* name = aName->GetUTF16String();
   uint32_t len = aName->GetLength();
   bool hasDash = false;
 
   if (!len || name[0] < 'a' || name[0] > 'z') {
@@ -3234,16 +3229,29 @@ nsContentUtils::IsCustomElementName(nsAt
          (name[i] < 0xFDF0 || name[i] > 0xFFFD)) {
         return false;
       }
 
       i++;
     }
   }
 
+  return hasDash;
+}
+
+// static
+bool
+nsContentUtils::IsCustomElementName(nsAtom* aName, uint32_t aNameSpaceID)
+{
+  // Allow non-dashed names in XUL for XBL to Custom Element migrations.
+  if (aNameSpaceID == kNameSpaceID_XUL) {
+    return true;
+  }
+
+  bool hasDash = IsNameWithDash(aName);
   if (!hasDash) {
     return false;
   }
 
   // The custom element name must not be one of the following values:
   //  annotation-xml
   //  color-profile
   //  font-face
@@ -9881,18 +9889,32 @@ nsContentUtils::NewXULOrHTMLElement(Elem
 
   nsAtom *name = nodeInfo->NameAtom();
   int32_t tag = eHTMLTag_unknown;
   bool isCustomElementName = false;
   if (nodeInfo->NamespaceEquals(kNameSpaceID_XHTML)) {
     tag = nsHTMLTags::CaseSensitiveAtomTagToId(name);
     isCustomElementName = (tag == eHTMLTag_userdefined &&
                            nsContentUtils::IsCustomElementName(name, kNameSpaceID_XHTML));
-  } else {
-    isCustomElementName = nsContentUtils::IsCustomElementName(name, kNameSpaceID_XUL);
+  } else { // kNameSpaceID_XUL
+    if (aIsAtom) {
+      // Make sure the customized built-in element to be constructed confirms
+      // to our naming requirement, i.e. [is] must be a dashed name and
+      // the tag name must not.
+      // if so, set isCustomElementName to false to kick off all the logics
+      // that pick up aIsAtom.
+      if (nsContentUtils::IsNameWithDash(aIsAtom) &&
+          !nsContentUtils::IsNameWithDash(name)) {
+        isCustomElementName = false;
+      } else {
+        isCustomElementName = nsContentUtils::IsCustomElementName(name, kNameSpaceID_XUL);
+      }
+    } else {
+      isCustomElementName = nsContentUtils::IsCustomElementName(name, kNameSpaceID_XUL);
+    }
   }
 
   RefPtr<nsAtom> tagAtom = nodeInfo->NameAtom();
   RefPtr<nsAtom> typeAtom;
   bool isCustomElement = isCustomElementName || aIsAtom;
   if (isCustomElement) {
     typeAtom = isCustomElementName ? tagAtom.get() : aIsAtom;
   }
@@ -9951,19 +9973,21 @@ nsContentUtils::NewXULOrHTMLElement(Elem
     ErrorResult rv;
 
     // Step 5.
     if (definition->IsCustomBuiltIn()) {
       // SetupCustomElement() should be called with an element that don't have
       // CustomElementData setup, if not we will hit the assertion in
       // SetCustomElementData().
       // Built-in element
-      MOZ_ASSERT(nodeInfo->NamespaceEquals(kNameSpaceID_XHTML),
-                 "Custom built-in XUL elements are not supported yet.");
-      *aResult = CreateHTMLElement(tag, nodeInfo.forget(), aFromParser).take();
+      if (nodeInfo->NamespaceEquals(kNameSpaceID_XHTML)) {
+        *aResult = CreateHTMLElement(tag, nodeInfo.forget(), aFromParser).take();
+      } else {
+        NS_IF_ADDREF(*aResult = nsXULElement::Construct(nodeInfo.forget()));
+      }
       (*aResult)->SetCustomElementData(new CustomElementData(typeAtom));
       if (synchronousCustomElements) {
         CustomElementRegistry::Upgrade(*aResult, definition, rv);
         if (rv.MaybeSetPendingException(cx)) {
           aes.ReportException();
         }
       } else {
         nsContentUtils::EnqueueUpgradeReaction(*aResult, definition);
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -699,16 +699,21 @@ public:
    * aDocument.
    */
   static nsresult NewURIWithDocumentCharset(nsIURI** aResult,
                                             const nsAString& aSpec,
                                             nsIDocument* aDocument,
                                             nsIURI* aBaseURI);
 
   /**
+   * Returns true if |aName| is a name with dashes.
+   */
+  static bool IsNameWithDash(nsAtom* aName);
+
+  /**
    * Returns true if |aName| is a valid name to be registered via
    * customElements.define.
    */
   static bool IsCustomElementName(nsAtom* aName, uint32_t aNameSpaceID);
 
   static nsresult CheckQName(const nsAString& aQualifiedName,
                              bool aNamespaceAware = true,
                              const char16_t** aColon = nullptr);
--- a/dom/base/nsNameSpaceManager.cpp
+++ b/dom/base/nsNameSpaceManager.cpp
@@ -181,23 +181,23 @@ nsNameSpaceManager::GetNameSpaceID(nsAto
 nsresult
 NS_NewElement(Element** aResult,
               already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
               FromParser aFromParser,
               const nsAString* aIs)
 {
   RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo;
   int32_t ns = ni->NamespaceID();
+  RefPtr<nsAtom> isAtom = aIs ? NS_Atomize(*aIs) : nullptr;
   if (ns == kNameSpaceID_XHTML) {
-    RefPtr<nsAtom> isAtom = aIs ? NS_Atomize(*aIs) : nullptr;
     return NS_NewHTMLElement(aResult, ni.forget(), aFromParser, isAtom);
   }
 #ifdef MOZ_XUL
   if (ns == kNameSpaceID_XUL) {
-    return NS_NewXULElement(aResult, ni.forget(), aFromParser);
+    return NS_NewXULElement(aResult, ni.forget(), aFromParser, isAtom);
   }
 #endif
   if (ns == kNameSpaceID_MathML) {
     // If the mathml.disabled pref. is true, convert all MathML nodes into
     // disabled MathML nodes by swapping the namespace.
     if (ni->NodeInfoManager()->MathMLEnabled()) {
       return NS_NewMathMLElement(aResult, ni.forget());
     }
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -43,16 +43,17 @@
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/HTMLObjectElement.h"
 #include "mozilla/dom/HTMLObjectElementBinding.h"
 #include "mozilla/dom/HTMLEmbedElement.h"
 #include "mozilla/dom/HTMLElementBinding.h"
 #include "mozilla/dom/HTMLEmbedElementBinding.h"
 #include "mozilla/dom/XULElementBinding.h"
+#include "mozilla/dom/XULPopupElementBinding.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/ResolveSystemBinding.h"
 #include "mozilla/dom/WebIDLGlobalNameHash.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerScope.h"
 #include "mozilla/dom/XrayExpandoClass.h"
 #include "mozilla/jsipc/CrossProcessObjectWrappers.h"
 #include "ipc/ErrorIPCUtils.h"
@@ -3802,35 +3803,42 @@ HTMLConstructor(JSContext* aCx, unsigned
     if (!constructor) {
       return false;
     }
 
     if (constructor != js::CheckedUnwrap(callee)) {
       return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
     }
   } else {
-    // Step 5.
-    // If the definition is for a customized built-in element, the localName
-    // should be one of the ones defined in the specification for this interface.
-
-    // Customized built-in elements are not supported for XUL yet.
-    if (ns == kNameSpaceID_XUL) {
-      return Throw(aCx, NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    constructorGetterCallback cb;
+    if (ns == kNameSpaceID_XHTML) {
+      // Step 5.
+      // If the definition is for a customized built-in element, the localName
+      // should be one of the ones defined in the specification for this interface.
+      tag = nsHTMLTags::CaseSensitiveAtomTagToId(definition->mLocalName);
+      if (tag == eHTMLTag_userdefined) {
+        return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
+      }
+
+      MOZ_ASSERT(tag <= NS_HTML_TAG_MAX, "tag is out of bounds");
+
+      // If the definition is for a customized built-in element, the active
+      // function should be the localname's element interface.
+      cb = sConstructorGetterCallback[tag];
+    } else { // kNameSpaceID_XUL
+      if (definition->mLocalName == nsGkAtoms::menupopup ||
+          definition->mLocalName == nsGkAtoms::popup ||
+          definition->mLocalName == nsGkAtoms::panel ||
+          definition->mLocalName == nsGkAtoms::tooltip) {
+        cb = XULPopupElementBinding::GetConstructorObject;
+      } else {
+        cb = XULElementBinding::GetConstructorObject;
+      }
     }
 
-    tag = nsHTMLTags::CaseSensitiveAtomTagToId(definition->mLocalName);
-    if (tag == eHTMLTag_userdefined) {
-      return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
-    }
-
-    MOZ_ASSERT(tag <= NS_HTML_TAG_MAX, "tag is out of bounds");
-
-    // If the definition is for a customized built-in element, the active
-    // function should be the localname's element interface.
-    constructorGetterCallback cb = sConstructorGetterCallback[tag];
     if (!cb) {
       return ThrowErrorMessage(aCx, MSG_ILLEGAL_CONSTRUCTOR);
     }
 
     // We want to get the constructor from our global's realm, not the
     // caller realm.
     JSAutoRealm ar(aCx, global.Get());
     JS::Rooted<JSObject*> constructor(aCx, cb(aCx));
--- a/dom/tests/mochitest/webcomponents/test_xul_custom_element.xul
+++ b/dom/tests/mochitest/webcomponents/test_xul_custom_element.xul
@@ -24,19 +24,54 @@
       }
     }
 
     customElements.define("test-custom-element", TestCustomElement);
 
     class TestWithoutDash extends XULElement { }
     customElements.define("testwithoutdash", TestWithoutDash);
 
-    function runTest() {
-      const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+    class TestWithoutDashExtended extends TestWithoutDash {
+      constructor() {
+        super();
+      }
+
+      connectedCallback() {
+        this.textContent = "quux";
+      }
+    }
+    customElements.define("testwithoutdash-extended", TestWithoutDashExtended, { extends: "testwithoutdash" });
+
+    class TestCustomBuiltInElement extends XULElement {
+      constructor() {
+        super();
+      }
 
+      connectedCallback() {
+        this.textContent = "baz";
+      }
+    }
+    customElements.define("test-built-in-element",
+      TestCustomBuiltInElement, { extends: "axulelement" });
+
+    class TestPopupExtendElement extends XULPopupElement {
+      constructor() {
+        super();
+      }
+
+      connectedCallback() {
+        this.textContent = "quuz";
+      }
+    }
+    customElements.define("test-popup-extend",
+      TestPopupExtendElement, { extends: "popup" });
+
+    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+    function basicCustomElementCreate() {
       let element = document.createElementNS(XUL_NS, "test-custom-element");
       document.querySelector("#content").appendChild(element);
       is(element.textContent, "foo", "Should have set the textContent");
       ok(element instanceof TestCustomElement, "Should be an instance of TestCustomElement");
 
       let element2 = element.cloneNode(false);
       is(element2.textContent, "", "Shouldn't have cloned the textContent");
       document.querySelector("#content").appendChild(element2);
@@ -45,31 +80,228 @@
 
       let element3 = new TestCustomElement();
       is(element3.localName, "test-custom-element", "Should see the right tag");
       is(element3.textContent, "", "Shouldn't have been inserted yet");
       is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
       document.querySelector("#content").appendChild(element3);
       is(element3.textContent, "foo", "Should have set the textContent");
       ok(element3 instanceof TestCustomElement, "Should be an instance of TestCustomElement");
+    }
 
+    function parserBasicElementUpgrade() {
       let element4 = document.getElementById("element4");
       is(element4.textContent, "foo",
          "Parser should have instantiated the custom element.");
       ok(element4 instanceof TestCustomElement, "Should be an instance of TestCustomElement");
+    }
 
+    function tagNameWithoutDash() {
       let element5 = document.getElementById("element5");
       ok(element5 instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+    }
+
+    function upgradeAfterDefine() {
+      class TestCustomElement1 extends XULElement {
+        constructor() {
+          super();
+        }
+
+        connectedCallback() {
+          this.textContent = "bar";
+        }
+      }
+
+      let element = document.createElementNS(XUL_NS, "test-custom-element-1");
+      ok(!(element instanceof TestCustomElement1), "Should not be an instance of TestCustomElement1");
+      customElements.define("test-custom-element-1", TestCustomElement1);
+      ok(!(element instanceof TestCustomElement1), "Should not be an instance of TestCustomElement1");
+      document.querySelector("#content").appendChild(element);
+      ok(element instanceof TestCustomElement1, "Should be upgraded to an instance of TestCustomElement1");
+      is(element.textContent, "bar", "Should have set the textContent");
+    }
+
+    function basicElementCreateBuiltIn() {
+      let element = document.createElementNS(XUL_NS, "axulelement", { is: "test-built-in-element" });
+      ok(element instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+      is(element.getAttribute("is"), "test-built-in-element", "The |is| attribute of the created element should be the extended type.");
+      document.querySelector("#content").appendChild(element);
+      is(element.textContent, "baz", "Should have set the textContent");
+
+      let element2 = element.cloneNode(false);
+      is(element2.localName, "axulelement", "Should see the right tag");
+      is(element2.getAttribute("is"), "test-built-in-element", "The |is| attribute of the created element should be the extended type.");
+      is(element2.textContent, "", "Shouldn't have cloned the textContent");
+      document.querySelector("#content").appendChild(element2);
+      is(element2.textContent, "baz", "Should have set the textContent");
+      ok(element2 instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+
+      let element3 = new TestCustomBuiltInElement();
+      is(element3.localName, "axulelement", "Should see the right tag");
+      is(element3.textContent, "", "Shouldn't have been inserted yet");
+      is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
+      document.querySelector("#content").appendChild(element3);
+      is(element3.textContent, "baz", "Should have set the textContent");
+      ok(element3 instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+    }
+
+    function parserBasicElementUpgradeBuiltIn() {
+      let element = document.getElementById("element6");
+      is(element.textContent, "baz",
+         "Parser should have instantiated the custom element.");
+      ok(element instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement");
+    }
+
+    function subclassElementCreateBuiltIn() {
+      let element = document.createElementNS(XUL_NS, "popup", { is: "test-popup-extend" });
+      ok(element instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+      is(element.getAttribute("is"), "test-popup-extend", "The |is| attribute of the created element should be the extended type.");
+      document.querySelector("#content").appendChild(element);
+      is(element.textContent, "quuz", "Should have set the textContent");
+
+      let element2 = element.cloneNode(false);
+      is(element2.localName, "popup", "Should see the right tag");
+      is(element2.getAttribute("is"), "test-popup-extend", "The |is| attribute of the created element should be the extended type.");
+      is(element2.textContent, "", "Shouldn't have cloned the textContent");
+      document.querySelector("#content").appendChild(element2);
+      is(element2.textContent, "quuz", "Should have set the textContent");
+      ok(element2 instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+
+      let element3 = new TestPopupExtendElement();
+      is(element3.localName, "popup", "Should see the right tag");
+      is(element3.textContent, "", "Shouldn't have been inserted yet");
+      is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
+      document.querySelector("#content").appendChild(element3);
+      is(element3.textContent, "quuz", "Should have set the textContent");
+      ok(element3 instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+    }
+
+    function parserSubclassElementUpgradeBuiltIn() {
+      let element = document.getElementById("element7");
+      is(element.textContent, "quuz",
+         "Parser should have instantiated the custom element.");
+      ok(element instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement");
+    }
+
+    function upgradeAfterDefineBuiltIn() {
+      class TestCustomBuiltInElement1 extends XULElement {
+        constructor() {
+          super();
+        }
+
+        connectedCallback() {
+          this.textContent = "qux";
+        }
+      }
+      let element = document.createElementNS(XUL_NS, "axulelement", { is: "test-built-in-element-1" });
+      ok(!(element instanceof TestCustomBuiltInElement1), "Should not be an instance of TestCustomBuiltInElement1");
+      customElements.define("test-built-in-element-1",
+        TestCustomBuiltInElement1, { extends: "axulelement" });
+      ok(!(element instanceof TestCustomBuiltInElement1), "Should not be an instance of TestCustomBuiltInElement1");
+      document.querySelector("#content").appendChild(element);
+      ok(element instanceof TestCustomBuiltInElement1, "Should be upgraded to an instance of TestCustomBuiltInElement1");
+      is(element.textContent, "qux", "Should have set the textContent");
+    }
+
+    function throwForInvalidBuiltInName() {
+      try {
+        // <axulelement is="testwithoutdashbuiltin" /> is not allowed;
+        // built-in type names need dashes.
+        customElements.define(
+          "testwithoutdashbuiltin", class extends XULElement {}, { extends: "axulelement" });
+        ok(false, "Built-in type name without dash should be rejected.");
+      } catch (e) {
+        ok(true, "Built-in type name without dash is rejected.");
+      }
+      try {
+        // <test-built-in-element-2 is="test-custom-element-2" /> is not allowed;
+        // built-in type tag names forbid dashes
+        customElements.define(
+          "test-built-in-element-2", class extends XULElement {}, { extends: "test-custom-element-2" });
+        ok(false, "Extending from a name with dash should be rejected.");
+      } catch (e) {
+        ok(true, "Extending from a name with dash is rejected.");
+      }
+    }
+
+    function extendingWithoutDashCustomElement() {
+      let element = document.createElementNS(XUL_NS, "testwithoutdash", { is: "testwithoutdash-extended" });
+      ok(element instanceof TestWithoutDashExtended, "Should be an instance of TestWithoutDashExtended");
+      ok(element instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+      is(element.getAttribute("is"), "testwithoutdash-extended", "The |is| attribute of the created element should be the extended type.");
+      document.querySelector("#content").appendChild(element);
+      is(element.textContent, "quux", "Should have set the textContent");
+
+      let element2 = element.cloneNode(false);
+      is(element2.localName, "testwithoutdash", "Should see the right tag");
+      is(element2.getAttribute("is"), "testwithoutdash-extended", "The |is| attribute of the created element should be the extended type.");
+      is(element2.textContent, "", "Shouldn't have cloned the textContent");
+      document.querySelector("#content").appendChild(element2);
+      is(element2.textContent, "quux", "Should have set the textContent");
+      ok(element2 instanceof TestWithoutDashExtended, "Should be an instance of TestWithoutDashExtended");
+      ok(element2 instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+
+      let element3 = new TestWithoutDashExtended();
+      is(element3.localName, "testwithoutdash", "Should see the right tag");
+      is(element3.textContent, "", "Shouldn't have been inserted yet");
+      is(element3.namespaceURI, XUL_NS, "Should have set the right namespace");
+      document.querySelector("#content").appendChild(element3);
+      is(element3.textContent, "quux", "Should have set the textContent");
+      ok(element3 instanceof TestWithoutDashExtended, "Should be an instance of TestWithoutDashExtended");
+      ok(element3 instanceof TestWithoutDash, "Should be an instance of TestWithoutDash");
+    }
+
+    function nonCustomElementCreate() {
+      // All of these should be created as plain XUL elements without hitting
+      // any assertions.
+      let elements = [
+        document.createElementNS(XUL_NS, "axulelement", { is: "test-custom-element" }),
+        document.createElementNS(XUL_NS, "axulelement", { is: "testwithoutdash" }),
+        document.createElementNS(XUL_NS, "axulelement", { is: "test-custom-element-1" }),
+        document.createElementNS(XUL_NS, "name-with-dash", { is: "name-with-dash" }),
+        document.createElementNS(XUL_NS, "name-with-dash", { is: "another-name-with-dash" }),
+        document.createElementNS(XUL_NS, "testwithoutdash-extended"),
+        document.createElementNS(XUL_NS, "test-built-in-element"),
+        document.createElementNS(XUL_NS, "test-popup-extend"),
+        document.createElementNS(XUL_NS, "test-built-in-element-1")];
+
+      for (let element of elements) {
+        is(Object.getPrototypeOf(element), XULElement.prototype,
+          `<${element.localName} is="${element.getAttribute("is")}" /> should not be a custom element.`);
+      }
+    }
+
+    function runTest() {
+      basicCustomElementCreate();
+      parserBasicElementUpgrade();
+
+      tagNameWithoutDash();
+      upgradeAfterDefine();
+
+      basicElementCreateBuiltIn();
+      parserBasicElementUpgradeBuiltIn();
+
+      subclassElementCreateBuiltIn();
+      parserSubclassElementUpgradeBuiltIn();
+
+      upgradeAfterDefineBuiltIn();
+
+      throwForInvalidBuiltInName();
+      extendingWithoutDashCustomElement();
+
+      nonCustomElementCreate();
 
       SimpleTest.finish();
     }
   ]]>
   </script>
 
   <body xmlns="http://www.w3.org/1999/xhtml">
     <p id="display"></p>
     <div id="content" style="display: none">
       <test-custom-element id="element4" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
       <testwithoutdash id="element5" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
+      <axulelement id="element6" is="test-built-in-element" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
+      <popup id="element7" is="test-popup-extend" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
     </div>
     <pre id="test"></pre>
   </body>
 </window>
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -185,17 +185,20 @@ nsXULElement* nsXULElement::Construct(al
 already_AddRefed<nsXULElement>
 nsXULElement::CreateFromPrototype(nsXULPrototypeElement* aPrototype,
                                   mozilla::dom::NodeInfo *aNodeInfo,
                                   bool aIsScriptable,
                                   bool aIsRoot)
 {
     RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo;
     nsCOMPtr<Element> baseElement;
-    NS_NewXULElement(getter_AddRefs(baseElement), ni.forget(), dom::FROM_PARSER_NETWORK);
+    NS_NewXULElement(getter_AddRefs(baseElement),
+                     ni.forget(),
+                     dom::FROM_PARSER_NETWORK,
+                     aPrototype->mIsAtom);
 
     if (baseElement) {
         nsXULElement* element = FromNode(baseElement);
 
         if (aPrototype->mHasIdAttribute) {
             element->SetHasID();
         }
         if (aPrototype->mHasClassAttribute) {
@@ -260,31 +263,32 @@ nsXULElement::CreateFromPrototype(nsXULP
                                                        aIsScriptable, aIsRoot);
     element.forget(aResult);
 
     return NS_OK;
 }
 
 nsresult
 NS_NewXULElement(Element** aResult, already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
-                 FromParser aFromParser)
+                 FromParser aFromParser, nsAtom* aIsAtom,
+                 mozilla::dom::CustomElementDefinition* aDefinition)
 {
     RefPtr<mozilla::dom::NodeInfo> nodeInfo = aNodeInfo;
 
     MOZ_ASSERT(nodeInfo, "need nodeinfo for non-proto Create");
 
     NS_ASSERTION(nodeInfo->NamespaceEquals(kNameSpaceID_XUL),
                  "Trying to create XUL elements that don't have the XUL namespace");
 
     nsIDocument* doc = nodeInfo->GetDocument();
     if (doc && !doc->AllowXULXBL()) {
         return NS_ERROR_NOT_AVAILABLE;
     }
 
-    return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser, nullptr, nullptr);
+    return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser, aIsAtom, aDefinition);
 }
 
 void
 NS_TrustedNewXULElement(Element** aResult,
                         already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
 {
     RefPtr<mozilla::dom::NodeInfo> ni = aNodeInfo;
     MOZ_ASSERT(ni, "need nodeinfo for non-proto Create");
@@ -2287,16 +2291,22 @@ nsXULPrototypeElement::SetAttrAt(uint32_
         !aValue.IsEmpty()) {
         mHasIdAttribute = true;
         // Store id as atom.
         // id="" means that the element has no id. Not that it has
         // emptystring as id.
         mAttributes[aPos].mValue.ParseAtom(aValue);
 
         return NS_OK;
+    } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::is)) {
+        // Store is as atom.
+        mAttributes[aPos].mValue.ParseAtom(aValue);
+        mIsAtom = mAttributes[aPos].mValue.GetAtomValue();
+
+        return NS_OK;
     } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::_class)) {
         mHasClassAttribute = true;
         // Compute the element's class list
         mAttributes[aPos].mValue.ParseAtomArray(aValue);
 
         return NS_OK;
     } else if (mAttributes[aPos].mName.Equals(nsGkAtoms::style)) {
         mHasStyleAttribute = true;
--- a/dom/xul/nsXULElement.h
+++ b/dom/xul/nsXULElement.h
@@ -145,17 +145,18 @@ class nsXULPrototypeElement : public nsX
 {
 public:
     nsXULPrototypeElement()
         : nsXULPrototypeNode(eType_Element),
           mNumAttributes(0),
           mHasIdAttribute(false),
           mHasClassAttribute(false),
           mHasStyleAttribute(false),
-          mAttributes(nullptr)
+          mAttributes(nullptr),
+          mIsAtom(nullptr)
     {
     }
 
     virtual ~nsXULPrototypeElement()
     {
         Unlink();
     }
 
@@ -188,16 +189,17 @@ public:
 
     RefPtr<mozilla::dom::NodeInfo> mNodeInfo;
 
     uint32_t                 mNumAttributes:29;
     uint32_t                 mHasIdAttribute:1;
     uint32_t                 mHasClassAttribute:1;
     uint32_t                 mHasStyleAttribute:1;
     nsXULPrototypeAttribute* mAttributes;         // [OWNER]
+    RefPtr<nsAtom>           mIsAtom;
 };
 
 namespace mozilla {
 namespace dom {
 class XULDocument;
 } // namespace dom
 } // namespace mozilla