Bug 1272653 - Make Get Element Attribute spec compatible; r=automatedtester
authorAndreas Tolfsen <ato@mozilla.com>
Mon, 16 May 2016 21:24:57 +0100
changeset 337744 a8d2a36786aa5f8416cd3daf6c15d9d038e2fa35
parent 337743 2a6a5e53e03f80f4c38df19bd59e682c038ecc58
child 337745 ee21562d144c762f0c087c75f4dfb22ddacc7607
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester
bugs1272653
milestone49.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 1272653 - Make Get Element Attribute spec compatible; r=automatedtester MozReview-Commit-ID: K5PsnmRrDJk
testing/marionette/element.js
testing/marionette/harness/marionette/tests/unit/test_element_state.py
testing/marionette/listener.js
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -31,16 +31,17 @@ const logger = Log.repository.getLogger(
  */
 
 this.EXPORTED_SYMBOLS = [
   "element",
   "ElementManager",
 ];
 
 const DOCUMENT_POSITION_DISCONNECTED = 1;
+const XMLNS = "http://www.w3.org/1999/xhtml";
 
 const uuidGen = Cc["@mozilla.org/uuid-generator;1"]
     .getService(Ci.nsIUUIDGenerator);
 
 this.element = {};
 
 element.LegacyKey = "ELEMENT";
 element.Key = "element-6066-11e4-a52e-4f735466cecf";
@@ -906,8 +907,59 @@ element.getInteractableElementTree = fun
 element.isKeyboardInteractable = function(el) {
   return true;
 };
 
 element.isXULElement = function(el) {
   let ns = atom.getElementAttribute(el, "namespaceURI");
   return ns.indexOf("there.is.only.xul") >= 0;
 };
+
+const boolEls = {
+  audio: ["autoplay", "controls", "loop", "muted"],
+  button: ["autofocus", "disabled", "formnovalidate"],
+  details: ["open"],
+  dialog: ["open"],
+  fieldset: ["disabled"],
+  form: ["novalidate"],
+  iframe: ["allowfullscreen"],
+  img: ["ismap"],
+  input: ["autofocus", "checked", "disabled", "formnovalidate", "multiple", "readonly", "required"],
+  keygen: ["autofocus", "disabled"],
+  menuitem: ["checked", "default", "disabled"],
+  object: ["typemustmatch"],
+  ol: ["reversed"],
+  optgroup: ["disabled"],
+  option: ["disabled", "selected"],
+  script: ["async", "defer"],
+  select: ["autofocus", "disabled", "multiple", "required"],
+  textarea: ["autofocus", "disabled", "readonly", "required"],
+  track: ["default"],
+  video: ["autoplay", "controls", "loop", "muted"],
+};
+
+/**
+ * Tests if the attribute is a boolean attribute on element.
+ *
+ * @param {DOMElement} el
+ *     Element to test if |attr| is a boolean attribute on.
+ * @param {string} attr
+ *     Attribute to test is a boolean attribute.
+ *
+ * @return {boolean}
+ *     True if the attribute is boolean, false otherwise.
+ */
+element.isBooleanAttribute = function(el, attr) {
+  if (el.namespaceURI !== XMLNS) {
+    return false;
+  }
+
+  // global boolean attributes that apply to all HTML elements,
+  // except for custom elements
+  if ((attr == "hidden" || attr == "itemscope") && !el.localName.includes("-")) {
+    return true;
+  }
+
+  if (!boolEls.hasOwnProperty(el.localName)) {
+    return false;
+  }
+  return boolEls[el.localName].includes(attr)
+};
--- a/testing/marionette/harness/marionette/tests/unit/test_element_state.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_element_state.py
@@ -1,20 +1,58 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+import types
 import urllib
 
 from marionette import MarionetteTestCase
 from marionette_driver.by import By
 
 
-def inline(doc):
-    return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
+boolean_attributes = {
+  "audio": ["autoplay", "controls", "loop", "muted"],
+  "button": ["autofocus", "disabled", "formnovalidate"],
+  "details": ["open"],
+  "dialog": ["open"],
+  "fieldset": ["disabled"],
+  "form": ["novalidate"],
+  "iframe": ["allowfullscreen"],
+  "img": ["ismap"],
+  "input": ["autofocus", "checked", "disabled", "formnovalidate", "multiple", "readonly", "required"],
+  "menuitem": ["checked", "default", "disabled"],
+  "object": ["typemustmatch"],
+  "ol": ["reversed"],
+  "optgroup": ["disabled"],
+  "option": ["disabled", "selected"],
+  "script": ["async", "defer"],
+  "select": ["autofocus", "disabled", "multiple", "required"],
+  "textarea": ["autofocus", "disabled", "readonly", "required"],
+  "track": ["default"],
+  "video": ["autoplay", "controls", "loop", "muted"],
+}
+
+
+def inline(doc, doctype="html"):
+    if doctype == "html":
+        return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
+    elif doctype == "xhtml":
+        return "data:application/xhtml+xml,%s" % urllib.quote(
+r"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <title>XHTML might be the future</title>
+  </head>
+
+  <body>
+    %s
+  </body>
+</html>""" % doc)
 
 
 attribute = inline("<input foo=bar>")
 input = inline("<input>")
 disabled = inline("<input disabled=baz>")
 check = inline("<input type=checkbox>")
 
 
@@ -34,27 +72,66 @@ class TestIsElementDisplayed(MarionetteT
         self.marionette.navigate(test_html)
         l = self.marionette.find_element(By.NAME, "myCheckBox")
         self.assertTrue(l.is_displayed())
         self.marionette.execute_script("arguments[0].hidden = true;", [l])
         self.assertFalse(l.is_displayed())
 
 
 class TestGetElementAttribute(MarionetteTestCase):
-    def test_get(self):
-        test_html = self.marionette.absolute_url("test.html")
-        self.marionette.navigate(test_html)
-        l = self.marionette.find_element(By.ID, "mozLink")
-        self.assertEqual("mozLink", l.get_attribute("id"))
+    def test_normal_attribute(self):
+        self.marionette.navigate(inline("<p style=foo>"))
+        el = self.marionette.find_element(By.TAG_NAME, "p")
+        attr = el.get_attribute("style")
+        self.assertIsInstance(attr, types.StringTypes)
+        self.assertEqual("foo", attr)
+
+    def test_boolean_attributes(self):
+        for tag, attrs in boolean_attributes.iteritems():
+            for attr in attrs:
+                print("testing boolean attribute <%s %s>" % (tag, attr))
+                doc = inline("<%s %s>" % (tag, attr))
+                self.marionette.navigate(doc)
+                el = self.marionette.find_element(By.TAG_NAME, tag)
+                res = el.get_attribute(attr)
+                self.assertIsInstance(res, types.StringTypes)
+                self.assertEqual("true", res)
+
+    def test_global_boolean_attributes(self):
+        self.marionette.navigate(inline("<p hidden>foo"))
+        el = self.marionette.find_element(By.TAG_NAME, "p")
+        attr = el.get_attribute("hidden")
+        self.assertIsInstance(attr, types.StringTypes)
+        self.assertEqual("true", attr)
 
-    def test_boolean(self):
-        test_html = self.marionette.absolute_url("html5/boolean_attributes.html")
-        self.marionette.navigate(test_html)
-        disabled = self.marionette.find_element(By.ID, "disabled")
-        self.assertEqual('true', disabled.get_attribute("disabled"))
+        self.marionette.navigate(inline("<p>foo"))
+        el = self.marionette.find_element(By.TAG_NAME, "p")
+        attr = el.get_attribute("hidden")
+        self.assertIsNone(attr)
+
+        self.marionette.navigate(inline("<p itemscope>foo"))
+        el = self.marionette.find_element(By.TAG_NAME, "p")
+        attr = el.get_attribute("itemscope")
+        self.assertIsInstance(attr, types.StringTypes)
+        self.assertEqual("true", attr)
+
+        self.marionette.navigate(inline("<p>foo"))
+        el = self.marionette.find_element(By.TAG_NAME, "p")
+        attr = el.get_attribute("itemscope")
+        self.assertIsNone(attr)
+
+    # TODO(ato): Test for custom elements
+
+    def test_xhtml(self):
+        doc = inline("<p hidden=\"true\">foo</p>", doctype="xhtml")
+        self.marionette.navigate(doc)
+        el = self.marionette.find_element(By.TAG_NAME, "p")
+        attr = el.get_attribute("hidden")
+        self.assertIsInstance(attr, types.StringTypes)
+        self.assertEqual("true", attr)
 
 
 class TestGetElementProperty(MarionetteTestCase):
     def test_get(self):
         self.marionette.navigate(disabled)
         el = self.marionette.find_element(By.TAG_NAME, "input")
         prop = el.get_property("disabled")
         self.assertIsInstance(prop, bool)
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -1056,17 +1056,25 @@ function clickElement(id) {
   return interaction.clickElement(
       el,
       !!capabilities.raisesAccessibilityExceptions,
       capabilities.specificationLevel >= 1);
 }
 
 function getElementAttribute(id, name) {
   let el = elementManager.getKnownElement(id, curContainer);
-  return atom.getElementAttribute(el, name, curContainer.frame);
+  if (element.isBooleanAttribute(el, name)) {
+    if (el.hasAttribute(name)) {
+      return "true";
+    } else {
+      return null;
+    }
+  } else {
+    return el.getAttribute(name);
+  }
 }
 
 function getElementProperty(id, name) {
   let el = elementManager.getKnownElement(id, curContainer);
   return el[name];
 }
 
 /**