Bug 1308902 - Add l10n module and commands to Marionette. r=ato
authorHenrik Skupin <mail@hskupin.info>
Fri, 11 Nov 2016 22:49:58 +0100
changeset 438301 89e259f133ce377a67bf9fd809ee2b0815ab64a2
parent 438300 3c7202df78b4e6515085fbeb702ff82d6f8552a2
child 438302 b98fa03d8c155826643526d7a6f94ecc089a5cbf
push id35679
push userbmo:timdream@gmail.com
push dateMon, 14 Nov 2016 09:29:38 +0000
reviewersato
bugs1308902
milestone52.0a1
Bug 1308902 - Add l10n module and commands to Marionette. r=ato MozReview-Commit-ID: 7STUwSOqVsg
testing/marionette/client/marionette_driver/__init__.py
testing/marionette/client/marionette_driver/localization.py
testing/marionette/driver.js
testing/marionette/harness/docs/reference.rst
testing/marionette/harness/marionette/tests/unit/test_localization.py
testing/marionette/harness/marionette/tests/unit/unit-tests.ini
testing/marionette/jar.mn
testing/marionette/l10n.js
--- a/testing/marionette/client/marionette_driver/__init__.py
+++ b/testing/marionette/client/marionette_driver/__init__.py
@@ -9,16 +9,17 @@ from marionette_driver import (
     by,
     date_time_value,
     decorators,
     errors,
     expected,
     geckoinstance,
     gestures,
     keys,
+    localization,
     marionette,
     selection,
     wait,
 )
 from marionette_driver.by import By
 from marionette_driver.date_time_value import DateTimeValue
 from marionette_driver.gestures import smooth_scroll, pinch
 from marionette_driver.marionette import Actions
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/localization.py
@@ -0,0 +1,54 @@
+# 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/.
+
+
+class L10n(object):
+    """An API which allows Marionette to handle localized content.
+
+    The `localization`_ of UI elements in Gecko based applications is done via
+    entities and properties. For static values entities are used, which are located
+    in .dtd files. Whereby for dynamically updated content the values come from
+    .property files. Both types of elements can be identifed via a unique id,
+    and the translated content retrieved.
+
+    For example::
+
+        from marionette_driver.localization import L10n
+        l10n = L10n(marionette)
+
+        l10n.localize_entity(["chrome://global/locale/about.dtd"], "about.version")
+        l10n.localize_property(["chrome://global/locale/findbar.properties"], "FastFind"))
+
+    .. _localization: https://mzl.la/2eUMjyF
+    """
+
+    def __init__(self, marionette):
+        self._marionette = marionette
+
+    def localize_entity(self, dtd_urls, entity_id):
+        """Retrieve the localized string for the specified entity id.
+
+        :param dtd_urls: List of .dtd URLs which will be used to search for the entity.
+        :param entity_id: ID of the entity to retrieve the localized string for.
+
+        :returns: The localized string for the requested entity.
+        :raises: :exc:`NoSuchElementException`
+        """
+        body = {"urls": dtd_urls, "id": entity_id}
+        return self._marionette._send_message("localization:l10n:localizeEntity",
+                                              body, key="value")
+
+    def localize_property(self, properties_urls, property_id):
+        """Retrieve the localized string for the specified property id.
+
+        :param properties_urls: List of .properties URLs which will be used to
+                                search for the property.
+        :param property_id: ID of the property to retrieve the localized string for.
+
+        :returns: The localized string for the requested property.
+        :raises: :exc:`NoSuchElementException`
+        """
+        body = {"urls": properties_urls, "id": property_id}
+        return self._marionette._send_message("localization:l10n:localizeProperty",
+                                              body, key="value")
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -21,16 +21,17 @@ Cu.import("chrome://marionette/content/a
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/browser.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
+Cu.import("chrome://marionette/content/l10n.js");
 Cu.import("chrome://marionette/content/legacyaction.js");
 Cu.import("chrome://marionette/content/logging.js");
 Cu.import("chrome://marionette/content/modal.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/simpletest.js");
 
 this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
 
@@ -2713,16 +2714,70 @@ GeckoDriver.prototype.receiveMessage = f
 };
 
 GeckoDriver.prototype.responseCompleted = function () {
   if (this.curBrowser !== null) {
     this.curBrowser.pendingCommands = [];
   }
 };
 
+/**
+ * Retrieve the localized string for the specified entity id.
+ *
+ * Example:
+ *     localizeEntity(["chrome://global/locale/about.dtd"], "about.version")
+ *
+ * @param {Array.<string>} urls
+ *     Array of .dtd URLs.
+ * @param {string} id
+ *     The ID of the entity to retrieve the localized string for.
+ *
+ * @return {string}
+ *     The localized string for the requested entity.
+ */
+GeckoDriver.prototype.localizeEntity = function(cmd, resp) {
+  let {urls, id} = cmd.parameters;
+
+  if (!Array.isArray(urls)) {
+    throw new InvalidArgumentError("Value of `urls` should be of type 'Array'");
+  }
+  if (typeof id != "string") {
+    throw new InvalidArgumentError("Value of `id` should be of type 'string'");
+  }
+
+  resp.body.value = l10n.localizeEntity(urls, id);
+}
+
+/**
+ * Retrieve the localized string for the specified property id.
+ *
+ * Example:
+ *     localizeProperty(["chrome://global/locale/findbar.properties"], "FastFind")
+ *
+ * @param {Array.<string>} urls
+ *     Array of .properties URLs.
+ * @param {string} id
+ *     The ID of the property to retrieve the localized string for.
+ *
+ * @return {string}
+ *     The localized string for the requested property.
+ */
+GeckoDriver.prototype.localizeProperty = function(cmd, resp) {
+  let {urls, id} = cmd.parameters;
+
+  if (!Array.isArray(urls)) {
+    throw new InvalidArgumentError("Value of `urls` should be of type 'Array'");
+  }
+  if (typeof id != "string") {
+    throw new InvalidArgumentError("Value of `id` should be of type 'string'");
+  }
+
+  resp.body.value = l10n.localizeProperty(urls, id);
+}
+
 GeckoDriver.prototype.commands = {
   "getMarionetteID": GeckoDriver.prototype.getMarionetteID,
   "sayHello": GeckoDriver.prototype.sayHello,
   "newSession": GeckoDriver.prototype.newSession,
   "getSessionCapabilities": GeckoDriver.prototype.getSessionCapabilities,
   "log": GeckoDriver.prototype.log,
   "getLogs": GeckoDriver.prototype.getLogs,
   "setContext": GeckoDriver.prototype.setContext,
@@ -2797,9 +2852,12 @@ GeckoDriver.prototype.commands = {
   "setWindowSize": GeckoDriver.prototype.setWindowSize,
   "maximizeWindow": GeckoDriver.prototype.maximizeWindow,
   "dismissDialog": GeckoDriver.prototype.dismissDialog,
   "acceptDialog": GeckoDriver.prototype.acceptDialog,
   "getTextFromDialog": GeckoDriver.prototype.getTextFromDialog,
   "sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog,
   "acceptConnections": GeckoDriver.prototype.acceptConnections,
   "quitApplication": GeckoDriver.prototype.quitApplication,
+
+  "localization:l10n:localizeEntity": GeckoDriver.prototype.localizeEntity,
+  "localization:l10n:localizeProperty": GeckoDriver.prototype.localizeProperty,
 };
--- a/testing/marionette/harness/docs/reference.rst
+++ b/testing/marionette/harness/docs/reference.rst
@@ -39,8 +39,13 @@ Built-in Conditions
 ^^^^^^^^^^^^^^^^^^^
 .. automodule:: marionette_driver.expected
    :members:
 
 Addons
 ------
 .. autoclass:: marionette_driver.addons.Addons
    :members:
+
+Localization
+------------
+.. autoclass:: marionette_driver.localization.L10n
+   :members:
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/unit/test_localization.py
@@ -0,0 +1,65 @@
+# 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/.
+
+from marionette import MarionetteTestCase
+from marionette_driver import By
+from marionette_driver.errors import InvalidArgumentException, NoSuchElementException
+from marionette_driver.localization import L10n
+
+
+class TestL10n(MarionetteTestCase):
+
+    def setUp(self):
+        super(TestL10n, self).setUp()
+
+        self.l10n = L10n(self.marionette)
+
+    def test_localize_entity_chrome(self):
+        dtds = ['chrome://global/locale/about.dtd',
+                'chrome://browser/locale/baseMenuOverlay.dtd']
+
+        with self.marionette.using_context('chrome'):
+            value = self.l10n.localize_entity(dtds, 'helpSafeMode.label')
+            element = self.marionette.find_element(By.ID, 'helpSafeMode')
+            self.assertEqual(value, element.get_attribute('label'))
+
+    def test_localize_entity_content(self):
+        dtds = ['chrome://global/locale/about.dtd',
+                'chrome://global/locale/aboutSupport.dtd']
+
+        value = self.l10n.localize_entity(dtds, 'aboutSupport.pageTitle')
+        self.marionette.navigate('about:support')
+        element = self.marionette.find_element(By.TAG_NAME, 'title')
+        self.assertEqual(value, element.text)
+
+    def test_localize_entity_invalid_arguments(self):
+        dtds = ['chrome://global/locale/about.dtd']
+
+        self.assertRaises(NoSuchElementException,
+                          self.l10n.localize_entity, dtds, 'notExistent')
+        self.assertRaises(InvalidArgumentException,
+                          self.l10n.localize_entity, dtds[0], 'notExistent')
+        self.assertRaises(InvalidArgumentException,
+                          self.l10n.localize_entity, dtds, True)
+
+    def test_localize_property(self):
+        properties = ['chrome://global/locale/filepicker.properties',
+                      'chrome://global/locale/findbar.properties']
+
+        # TODO: Find a way to verify the retrieved localized value
+        value = self.l10n.localize_property(properties, 'CaseSensitive')
+        self.assertNotEqual(value, '')
+
+        self.assertRaises(NoSuchElementException,
+                          self.l10n.localize_property, properties, 'notExistent')
+
+    def test_localize_property_invalid_arguments(self):
+        properties = ['chrome://global/locale/filepicker.properties']
+
+        self.assertRaises(NoSuchElementException,
+                          self.l10n.localize_property, properties, 'notExistent')
+        self.assertRaises(InvalidArgumentException,
+                          self.l10n.localize_property, properties[0], 'notExistent')
+        self.assertRaises(InvalidArgumentException,
+                          self.l10n.localize_property, properties, True)
--- a/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
@@ -137,8 +137,9 @@ skip-if = buildapp == 'b2g' || appname =
 [test_chrome.py]
 skip-if = buildapp == 'b2g' || appname == 'fennec'
 
 [test_addons.py]
 
 [test_select.py]
 [test_crash.py]
 [test_httpd.py]
+[test_localization.py]
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -22,16 +22,17 @@ marionette.jar:
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)
   content/atom.js (atom.js)
   content/evaluate.js (evaluate.js)
   content/logging.js (logging.js)
   content/navigate.js (navigate.js)
+  content/l10n.js (l10n.js)
   content/assert.js (assert.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (harness/marionette/chrome/test.xul)
   content/test2.xul  (harness/marionette/chrome/test2.xul)
   content/test_dialog.xul  (harness/marionette/chrome/test_dialog.xul)
   content/test_nested_iframe.xul  (harness/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (harness/marionette/chrome/test_anonymous_content.xul)
 #endif
new file mode 100644
--- /dev/null
+++ b/testing/marionette/l10n.js
@@ -0,0 +1,98 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * An API which allows Marionette to handle localized content.
+ *
+ * The localization (https://mzl.la/2eUMjyF) of UI elements in Gecko based
+ * applications is done via entities and properties. For static values entities
+ * are used, which are located in .dtd files. Whereby for dynamically updated
+ * content the values come from .property files. Both types of elements can be
+ * identifed via a unique id, and the translated content retrieved.
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+    this, "domParser", "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
+
+Cu.import("chrome://marionette/content/error.js");
+
+this.EXPORTED_SYMBOLS = ["l10n"];
+
+this.l10n = {};
+
+/**
+ * Retrieve the localized string for the specified entity id.
+ *
+ * Example:
+ *     localizeEntity(["chrome://global/locale/about.dtd"], "about.version")
+ *
+ * @param {Array.<string>} urls
+ *     Array of .dtd URLs.
+ * @param {string} id
+ *     The ID of the entity to retrieve the localized string for.
+ *
+ * @return {string}
+ *     The localized string for the requested entity.
+ */
+l10n.localizeEntity = function(urls, id) {
+  // Add xhtml11.dtd to prevent missing entity errors with XHTML files
+  urls.push("resource:///res/dtd/xhtml11.dtd");
+
+  // Build a string which contains all possible entity locations
+  let locations = [];
+  urls.forEach((url, index) => {
+    locations.push(`<!ENTITY % dtd_${index} SYSTEM "${url}">%dtd_${index};`);
+  })
+
+  // Use the DOM parser to resolve the entity and extract its real value
+  let header = `<?xml version="1.0"?><!DOCTYPE elem [${locations.join("")}]>`;
+  let elem = `<elem id="elementID">&${id};</elem>`;
+  let doc = domParser.parseFromString(header + elem, "text/xml");
+  let element = doc.querySelector("elem[id='elementID']");
+
+  if (element === null) {
+    throw new NoSuchElementError(`Entity with id='${id}' hasn't been found`);
+  }
+
+  return element.textContent;
+};
+
+/**
+ * Retrieve the localized string for the specified property id.
+ *
+ * Example:
+ *     localizeProperty(["chrome://global/locale/findbar.properties"], "FastFind")
+ *
+ * @param {Array.<string>} urls
+ *     Array of .properties URLs.
+ * @param {string} id
+ *     The ID of the property to retrieve the localized string for.
+ *
+ * @return {string}
+ *     The localized string for the requested property.
+ */
+l10n.localizeProperty = function(urls, id) {
+  let property = null;
+
+  for (let url of urls) {
+    let bundle = Services.strings.createBundle(url);
+    try {
+      property = bundle.GetStringFromName(id);
+      break;
+    } catch (e) {}
+  };
+
+  if (property === null) {
+    throw new NoSuchElementError(`Property with id='${id}' hasn't been found`);
+  }
+
+  return property;
+};