summaryrefslogtreecommitdiffstats
path: root/testing/marionette/puppeteer/firefox/firefox_puppeteer/api
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/puppeteer/firefox/firefox_puppeteer/api')
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/__init__.py0
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/appinfo.py45
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/keys.py17
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/l10n.py125
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py150
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/security.py68
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py411
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py140
8 files changed, 956 insertions, 0 deletions
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/__init__.py
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/appinfo.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/appinfo.py
new file mode 100644
index 000000000..13d32c15b
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/appinfo.py
@@ -0,0 +1,45 @@
+# 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 firefox_puppeteer.base import BaseLib
+
+
+class AppInfo(BaseLib):
+ """This class provides access to various attributes of AppInfo.
+
+ For more details on AppInfo, visit:
+ https://developer.mozilla.org/en-US/docs/Mozilla/QA/Mozmill_tests/Shared_Modules/UtilsAPI/appInfo
+ """
+
+ def __getattr__(self, attr):
+ with self.marionette.using_context('chrome'):
+ value = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ return Services.appinfo[arguments[0]];
+ """, script_args=[attr])
+
+ if value is not None:
+ return value
+ else:
+ raise AttributeError('{} has no attribute {}'.format(self.__class__.__name__,
+ attr))
+
+ @property
+ def locale(self):
+ with self.marionette.using_context('chrome'):
+ return self.marionette.execute_script("""
+ return Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIXULChromeRegistry)
+ .getSelectedLocale("global");
+ """)
+
+ @property
+ def user_agent(self):
+ with self.marionette.using_context('chrome'):
+ return self.marionette.execute_script("""
+ return Components.classes["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Components.interfaces.nsIHttpProtocolHandler)
+ .userAgent;
+ """)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/keys.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/keys.py
new file mode 100644
index 000000000..2c5a1e523
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/keys.py
@@ -0,0 +1,17 @@
+# 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 marionette_driver
+
+
+class Keys(marionette_driver.keys.Keys):
+ """Proxy to marionette's keys with an "accel" provided for convenience
+ testing across platforms."""
+
+ def __init__(self, marionette):
+ self.isDarwin = marionette.session_capabilities['platformName'] == 'darwin'
+
+ @property
+ def ACCEL(self):
+ return self.META if self.isDarwin else self.CONTROL
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/l10n.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/l10n.py
new file mode 100644
index 000000000..f7f52918c
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/l10n.py
@@ -0,0 +1,125 @@
+# 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/.
+
+# -----------------
+# DEPRECATED module
+# -----------------
+# Replace its use in tests when Firefox 45 ESR support ends with
+# marionette_driver.localization.L10n
+
+import copy
+
+from marionette_driver.errors import (
+ NoSuchElementException,
+ UnknownCommandException,
+)
+from marionette_driver.localization import L10n as L10nMarionette
+
+from firefox_puppeteer.base import BaseLib
+
+
+class L10n(BaseLib):
+ """An API which allows Marionette to handle localized content.
+
+ .. deprecated:: 52.2.0
+ Use the localization module from :py:mod:`marionette_driver` instead.
+
+ 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.
+
+ .. _localization: https://mzl.la/2eUMjyF
+ """
+
+ def __init__(self, marionette):
+ super(L10n, self).__init__(marionette)
+
+ self._l10nMarionette = L10nMarionette(self.marionette)
+
+ def localize_entity(self, dtd_urls, entity_id):
+ """Returns the localized string for the specified DTD entity id.
+
+ To find the entity all given DTD files will be searched for the id.
+
+ :param dtd_urls: A list of dtd files to search.
+ :param entity_id: The id to retrieve the value from.
+
+ :returns: The localized string for the requested entity.
+
+ :raises NoSuchElementException: When entity id is not found in dtd_urls.
+ """
+ # Add xhtml11.dtd to prevent missing entity errors with XHTML files
+ try:
+ return self._l10nMarionette.localize_entity(dtd_urls, entity_id)
+ except UnknownCommandException:
+ dtds = copy.copy(dtd_urls)
+ dtds.append("resource:///res/dtd/xhtml11.dtd")
+
+ dtd_refs = ''
+ for index, item in enumerate(dtds):
+ dtd_id = 'dtd_%s' % index
+ dtd_refs += '<!ENTITY %% %s SYSTEM "%s">%%%s;' % \
+ (dtd_id, item, dtd_id)
+
+ contents = """<?xml version="1.0"?>
+ <!DOCTYPE elem [%s]>
+
+ <elem id="entity">&%s;</elem>""" % (dtd_refs, entity_id)
+
+ with self.marionette.using_context('chrome'):
+ value = self.marionette.execute_script("""
+ var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Components.interfaces.nsIDOMParser);
+ var doc = parser.parseFromString(arguments[0], "text/xml");
+ var node = doc.querySelector("elem[id='entity']");
+
+ return node ? node.textContent : null;
+ """, script_args=[contents])
+
+ if not value:
+ raise NoSuchElementException('DTD Entity not found: %s' % entity_id)
+
+ return value
+
+ def localize_property(self, property_urls, property_id):
+ """Returns the localized string for the specified property id.
+
+ To find the property all given property files will be searched for
+ the id.
+
+ :param property_urls: A list of property files to search.
+ :param property_id: The id to retrieve the value from.
+
+ :returns: The localized string for the requested entity.
+
+ :raises NoSuchElementException: When property id is not found in
+ property_urls.
+ """
+ try:
+ return self._l10nMarionette.localize_property(property_urls, property_id)
+ except UnknownCommandException:
+ with self.marionette.using_context('chrome'):
+ value = self.marionette.execute_script("""
+ let property = null;
+ let property_id = arguments[1];
+
+ arguments[0].some(aUrl => {
+ let bundle = Services.strings.createBundle(aUrl);
+
+ try {
+ property = bundle.GetStringFromName(property_id);
+ return true;
+ }
+ catch (ex) { }
+ });
+
+ return property;
+ """, script_args=[property_urls, property_id])
+
+ if not value:
+ raise NoSuchElementException('Property not found: %s' % property_id)
+
+ return value
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py
new file mode 100644
index 000000000..fadc2c19b
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py
@@ -0,0 +1,150 @@
+# 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 collections import namedtuple
+from time import sleep
+
+from marionette_driver.errors import MarionetteException
+
+from firefox_puppeteer.base import BaseLib
+
+
+class Places(BaseLib):
+ """Low-level access to several bookmark and history related actions."""
+
+ BookmarkFolders = namedtuple('bookmark_folders',
+ ['root', 'menu', 'toolbar', 'unfiled'])
+ bookmark_folders = BookmarkFolders('root________', 'menu________',
+ 'toolbar_____', 'unfiled_____')
+
+ # Bookmark related helpers #
+
+ def is_bookmarked(self, url):
+ """Check if the given URL is bookmarked.
+
+ :param url: The URL to Check
+
+ :returns: True, if the URL is a bookmark
+ """
+ return self.marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+ PlacesUtils.bookmarks.fetch({url: arguments[0]}).then(bm => {
+ marionetteScriptFinished(bm != null);
+ });
+ """, script_args=[url])
+
+ def get_folder_ids_for_url(self, url):
+ """Retrieve the folder ids where the given URL has been bookmarked in.
+
+ :param url: URL of the bookmark
+
+ :returns: List of folder ids
+ """
+ return self.marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+ let folderGuids = []
+
+ function onResult(bm) {
+ folderGuids.push(bm.parentGuid);
+ }
+
+ PlacesUtils.bookmarks.fetch({url: arguments[0]}, onResult).then(() => {
+ marionetteScriptFinished(folderGuids);
+ });
+ """, script_args=[url])
+
+ def is_bookmark_star_button_ready(self):
+ """Check if the status of the star-button is not updating.
+
+ :returns: True, if the button is ready
+ """
+ return self.marionette.execute_script("""
+ let button = window.BookmarkingUI;
+
+ return button.status !== button.STATUS_UPDATING;
+ """)
+
+ def restore_default_bookmarks(self):
+ """Restore the default bookmarks for the current profile."""
+ retval = self.marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+
+ // Default bookmarks.html file is stored inside omni.jar,
+ // so get it via a resource URI
+ let defaultBookmarks = 'chrome://browser/locale/bookmarks.html';
+
+ // Trigger the import of the default bookmarks
+ BookmarkHTMLUtils.importFromURL(defaultBookmarks, true)
+ .then(() => marionetteScriptFinished(true))
+ .catch(() => marionetteScriptFinished(false));
+ """, script_timeout=10000)
+
+ if not retval:
+ raise MarionetteException("Restore Default Bookmarks failed")
+
+ # Browser history related helpers #
+
+ def get_all_urls_in_history(self):
+ """Retrieve any URLs which have been stored in the history."""
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(), options).root;
+ let urls = [];
+
+ root.containerOpen = true;
+ for (let i = 0; i < root.childCount; i++) {
+ urls.push(root.getChild(i).uri)
+ }
+ root.containerOpen = false;
+
+ return urls;
+ """)
+
+ def remove_all_history(self):
+ """Remove all history items."""
+ retval = self.marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+ PlacesUtils.history.clear()
+ .then(() => marionetteScriptFinished(true))
+ .catch(() => marionetteScriptFinished(false));
+ """, script_timeout=10000)
+
+ if not retval:
+ raise MarionetteException("Removing all history failed")
+
+ def wait_for_visited(self, urls, callback):
+ """Wait until all passed-in urls have been visited.
+
+ :param urls: List of URLs which need to be visited and indexed
+
+ :param callback: Method to execute which triggers loading of the URLs
+ """
+ # Bug 1121691: Needs observer handling support with callback first
+ # Until then we have to wait about 4s to ensure the page has been indexed
+ callback()
+ sleep(4)
+
+ # Plugin related helpers #
+
+ def clear_plugin_data(self):
+ """Clear any kind of locally stored data from plugins."""
+ self.marionette.execute_script("""
+ let host = Components.classes["@mozilla.org/plugin/host;1"]
+ .getService(Components.interfaces.nsIPluginHost);
+ let tags = host.getPluginTags();
+
+ tags.forEach(aTag => {
+ try {
+ host.clearSiteData(aTag, null, Components.interfaces.nsIPluginHost
+ .FLAG_CLEAR_ALL, -1);
+ } catch (ex) {
+ }
+ });
+ """)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/security.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/security.py
new file mode 100644
index 000000000..7f6532a08
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/security.py
@@ -0,0 +1,68 @@
+# 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 re
+
+from firefox_puppeteer.base import BaseLib
+from firefox_puppeteer.errors import NoCertificateError
+
+
+class Security(BaseLib):
+ """Low-level access to security (SSL, TLS) related information."""
+
+ # Security related helpers #
+
+ def get_address_from_certificate(self, certificate):
+ """Retrieves the address of the organization from the certificate information.
+
+ The returned address may be `None` in case of no address found, or a dictionary
+ with the following entries: `city`, `country`, `postal_code`, `state`, `street`.
+
+ :param certificate: A JSON object representing the certificate, which can usually be
+ retrieved via the current tab: `self.browser.tabbar.selected_tab.certificate`.
+
+ :returns: Address details as dictionary
+ """
+ regex = re.compile('.*?L=(?P<city>.+?),ST=(?P<state>.+?),C=(?P<country>.+?)'
+ ',postalCode=(?P<postal_code>.+?),STREET=(?P<street>.+?)'
+ ',serial')
+ results = regex.search(certificate['subjectName'])
+
+ return results.groupdict() if results else results
+
+ def get_certificate_for_page(self, tab_element):
+ """The SSL certificate assiciated with the loaded web page in the given tab.
+
+ :param tab_element: The inner tab DOM element.
+
+ :returns: Certificate details as JSON object.
+ """
+ cert = self.marionette.execute_script("""
+ var securityUI = arguments[0].linkedBrowser.securityUI;
+ var status = securityUI.QueryInterface(Components.interfaces.nsISSLStatusProvider)
+ .SSLStatus;
+
+ return status ? status.serverCert : null;
+ """, script_args=[tab_element])
+
+ uri = self.marionette.execute_script("""
+ return arguments[0].linkedBrowser.currentURI.spec;
+ """, script_args=[tab_element])
+
+ if cert is None:
+ raise NoCertificateError('No certificate found for "{}"'.format(uri))
+
+ return cert
+
+ def get_domain_from_common_name(self, common_name):
+ """Retrieves the domain associated with a page's security certificate from the common name.
+
+ :param certificate: A string containing the certificate's common name, which can usually
+ be retrieved like so: `certificate['commonName']`.
+
+ :returns: Domain as string
+ """
+ return self.marionette.execute_script("""
+ return Services.eTLD.getBaseDomainFromHost(arguments[0]);
+ """, script_args=[common_name])
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py
new file mode 100644
index 000000000..0b90af0d8
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py
@@ -0,0 +1,411 @@
+# 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 ConfigParser
+import os
+import re
+
+import mozinfo
+
+from firefox_puppeteer.base import BaseLib
+from firefox_puppeteer.api.appinfo import AppInfo
+
+
+class ActiveUpdate(BaseLib):
+
+ def __getattr__(self, attr):
+ value = self.marionette.execute_script("""
+ let ums = Components.classes['@mozilla.org/updates/update-manager;1']
+ .getService(Components.interfaces.nsIUpdateManager);
+ return ums.activeUpdate[arguments[0]];
+ """, script_args=[attr])
+
+ if value:
+ return value
+ else:
+ raise AttributeError('{} has no attribute {}'.format(self.__class__.__name__,
+ attr))
+
+ @property
+ def exists(self):
+ """Checks if there is an active update.
+
+ :returns: True if there is an active update
+ """
+ active_update = self.marionette.execute_script("""
+ let ums = Components.classes['@mozilla.org/updates/update-manager;1']
+ .getService(Components.interfaces.nsIUpdateManager);
+ return ums.activeUpdate;
+ """)
+
+ return bool(active_update)
+
+ def get_patch_at(self, patch_index):
+ """Use nsIUpdate.getPatchAt to return a patch from an update.
+
+ :returns: JSON data for an nsIUpdatePatch object
+ """
+ return self.marionette.execute_script("""
+ let ums = Components.classes['@mozilla.org/updates/update-manager;1']
+ .getService(Components.interfaces.nsIUpdateManager);
+ return ums.activeUpdate.getPatchAt(arguments[0]);
+ """, script_args=[patch_index])
+
+ @property
+ def patch_count(self):
+ """Get the patchCount from the active update.
+
+ :returns: The patch count
+ """
+ return self.marionette.execute_script("""
+ let ums = Components.classes['@mozilla.org/updates/update-manager;1']
+ .getService(Components.interfaces.nsIUpdateManager);
+ return ums.activeUpdate.patchCount;
+ """)
+
+ @property
+ def selected_patch(self):
+ """Get the selected patch for the active update.
+
+ :returns: JSON data for the selected patch
+ """
+ return self.marionette.execute_script("""
+ let ums = Components.classes['@mozilla.org/updates/update-manager;1']
+ .getService(Components.interfaces.nsIUpdateManager);
+ return ums.activeUpdate.selectedPatch;
+ """)
+
+
+class MARChannels(BaseLib):
+ """Class to handle the allowed MAR channels as listed in update-settings.ini."""
+ INI_SECTION = 'Settings'
+ INI_OPTION = 'ACCEPTED_MAR_CHANNEL_IDS'
+
+ class MARConfigParser(ConfigParser.ConfigParser):
+ """INI parser which allows to read and write MAR config files.
+
+ Virtually identical to the original method, but delimit keys and values
+ with '=' instead of ' = '
+ """
+
+ def write(self, fp):
+ """Write an .ini-format representation of the configuration state."""
+ if self._defaults:
+ fp.write("[%s]\n" % ConfigParser.DEFAULTSECT)
+ for (key, value) in self._defaults.items():
+ fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t')))
+ fp.write("\n")
+ for section in self._sections:
+ fp.write("[%s]\n" % section)
+ for (key, value) in self._sections[section].items():
+ if key == "__name__":
+ continue
+ if (value is not None) or (self._optcre == self.OPTCRE):
+ key = "=".join((key, str(value).replace('\n', '\n\t')))
+ fp.write("%s\n" % (key))
+ fp.write("\n")
+
+ def __init__(self, marionette):
+ BaseLib.__init__(self, marionette)
+
+ self.config_file_path = self.marionette.execute_script("""
+ Components.utils.import('resource://gre/modules/Services.jsm');
+
+ let file = Services.dirsvc.get('GreD', Components.interfaces.nsIFile);
+ file.append('update-settings.ini');
+
+ return file.path;
+ """)
+
+ self.config = self.MARConfigParser()
+ self.config.optionxform = str
+
+ @property
+ def channels(self):
+ """The currently accepted MAR channels.
+
+ :returns: A set of channel names
+ """
+ # Make sure to always read the current file contents
+ self.config.read(self.config_file_path)
+
+ return set(self.config.get(self.INI_SECTION, self.INI_OPTION).split(','))
+
+ @channels.setter
+ def channels(self, channels):
+ """Set the accepted MAR channels.
+
+ :param channels: A set of channel names
+ """
+ self.config.set(self.INI_SECTION, self.INI_OPTION, ','.join(channels))
+ with open(self.config_file_path, 'wb') as configfile:
+ self.config.write(configfile)
+
+ def add_channels(self, channels):
+ """Add additional MAR channels.
+
+ :param channels: A set of channel names to add
+ """
+ self.channels = self.channels | set(channels)
+
+ def remove_channels(self, channels):
+ """Remove MAR channels.
+
+ :param channels: A set of channel names to remove
+ """
+ self.channels = self.channels - set(channels)
+
+
+class SoftwareUpdate(BaseLib):
+ """The SoftwareUpdate API adds support for an easy access to the update process."""
+ PREF_APP_DISTRIBUTION = 'distribution.id'
+ PREF_APP_DISTRIBUTION_VERSION = 'distribution.version'
+ PREF_APP_UPDATE_CHANNEL = 'app.update.channel'
+ PREF_APP_UPDATE_URL = 'app.update.url'
+ PREF_APP_UPDATE_URL_OVERRIDE = 'app.update.url.override'
+ PREF_DISABLED_ADDONS = 'extensions.disabledAddons'
+
+ def __init__(self, marionette):
+ BaseLib.__init__(self, marionette)
+
+ self.app_info = AppInfo(marionette)
+
+ self._mar_channels = MARChannels(marionette)
+ self._active_update = ActiveUpdate(marionette)
+
+ @property
+ def ABI(self):
+ """Get the customized ABI for the update service.
+
+ :returns: ABI version
+ """
+ abi = self.app_info.XPCOMABI
+ if mozinfo.isMac:
+ abi += self.marionette.execute_script("""
+ let macutils = Components.classes['@mozilla.org/xpcom/mac-utils;1']
+ .getService(Components.interfaces.nsIMacUtils);
+ if (macutils.isUniversalBinary) {
+ return '-u-' + macutils.architecturesInBinary;
+ }
+ return '';
+ """)
+
+ return abi
+
+ @property
+ def active_update(self):
+ """ Holds a reference to an :class:`ActiveUpdate` object."""
+ return self._active_update
+
+ @property
+ def allowed(self):
+ """Check if the user has permissions to run the software update
+
+ :returns: Status if the user has the permissions
+ """
+ return self.marionette.execute_script("""
+ let aus = Components.classes['@mozilla.org/updates/update-service;1']
+ .getService(Components.interfaces.nsIApplicationUpdateService);
+ return aus.canCheckForUpdates && aus.canApplyUpdates;
+ """)
+
+ @property
+ def build_info(self):
+ """Return information of the current build version
+
+ :returns: A dictionary of build information
+ """
+ update_url = self.get_update_url(True)
+
+ return {
+ 'buildid': self.app_info.appBuildID,
+ 'channel': self.update_channel,
+ 'disabled_addons': self.marionette.get_pref(self.PREF_DISABLED_ADDONS),
+ 'locale': self.app_info.locale,
+ 'mar_channels': self.mar_channels.channels,
+ 'update_url': update_url,
+ 'update_snippet': self.get_update_snippet(update_url),
+ 'user_agent': self.app_info.user_agent,
+ 'version': self.app_info.version
+ }
+
+ @property
+ def is_complete_update(self):
+ """Return true if the offered update is a complete update
+
+ :returns: True if the offered update is a complete update
+ """
+ # Throw when isCompleteUpdate is called without an update. This should
+ # never happen except if the test is incorrectly written.
+ assert self.active_update.exists, 'An active update has been found'
+
+ patch_count = self.active_update.patch_count
+ assert patch_count == 1 or patch_count == 2,\
+ 'An update must have one or two patches included'
+
+ # Ensure Partial and Complete patches produced have unique urls
+ if patch_count == 2:
+ patch0_url = self.active_update.get_patch_at(0)['URL']
+ patch1_url = self.active_update.get_patch_at(1)['URL']
+ assert patch0_url != patch1_url,\
+ 'Partial and Complete download URLs are different'
+
+ return self.active_update.selected_patch['type'] == 'complete'
+
+ @property
+ def mar_channels(self):
+ """ Holds a reference to a :class:`MARChannels` object."""
+ return self._mar_channels
+
+ @property
+ def os_version(self):
+ """Returns information about the OS version
+
+ :returns: The OS version
+ """
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ let osVersion;
+ try {
+ osVersion = Services.sysinfo.getProperty("name") + " " +
+ Services.sysinfo.getProperty("version");
+ }
+ catch (ex) {
+ }
+
+ if (osVersion) {
+ try {
+ osVersion += " (" + Services.sysinfo.getProperty("secondaryLibrary") + ")";
+ }
+ catch (e) {
+ // Not all platforms have a secondary widget library,
+ // so an error is nothing to worry about.
+ }
+ osVersion = encodeURIComponent(osVersion);
+ }
+ return osVersion;
+ """)
+
+ @property
+ def patch_info(self):
+ """ Returns information of the active update in the queue."""
+ info = {'channel': self.update_channel}
+
+ if (self.active_update.exists):
+ info['buildid'] = self.active_update.buildID
+ info['is_complete'] = self.is_complete_update
+ info['size'] = self.active_update.selected_patch['size']
+ info['type'] = self.update_type
+ info['url_mirror'] = \
+ self.active_update.selected_patch['finalURL'] or 'n/a'
+ info['version'] = self.active_update.appVersion
+
+ return info
+
+ @property
+ def staging_directory(self):
+ """ Returns the path to the updates staging directory."""
+ return self.marionette.execute_script("""
+ let aus = Components.classes['@mozilla.org/updates/update-service;1']
+ .getService(Components.interfaces.nsIApplicationUpdateService);
+ return aus.getUpdatesDirectory().path;
+ """)
+
+ @property
+ def update_channel(self):
+ """Return the currently used update channel."""
+ return self.marionette.get_pref(self.PREF_APP_UPDATE_CHANNEL,
+ default_branch=True)
+
+ @update_channel.setter
+ def update_channel(self, channel):
+ """Set the update channel to be used for update checks.
+
+ :param channel: New update channel to use
+
+ """
+ writer = UpdateChannelWriter(self.marionette)
+ writer.set_channel(channel)
+
+ @property
+ def update_type(self):
+ """Returns the type of the active update."""
+ return self.active_update.type
+
+ def force_fallback(self):
+ """Update the update.status file and set the status to 'failed:6'"""
+ with open(os.path.join(self.staging_directory, 'update.status'), 'w') as f:
+ f.write('failed: 6\n')
+
+ def get_update_snippet(self, update_url):
+ """Retrieve contents of the update snippet.
+
+ :param update_url: URL to the update snippet
+ """
+ snippet = None
+ try:
+ import urllib2
+ response = urllib2.urlopen(update_url)
+ snippet = response.read()
+ except Exception:
+ pass
+
+ return snippet
+
+ def get_update_url(self, force=False):
+ """Retrieve the AUS update URL the update snippet is retrieved from.
+
+ :param force: Boolean flag to force an update check
+
+ :returns: The URL of the update snippet
+ """
+ url = self.marionette.get_pref(self.PREF_APP_UPDATE_URL_OVERRIDE)
+ if not url:
+ url = self.marionette.get_pref(self.PREF_APP_UPDATE_URL)
+
+ # Format the URL by replacing placeholders
+ url = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/UpdateUtils.jsm")
+ return UpdateUtils.formatUpdateURL(arguments[0]);
+ """, script_args=[url])
+
+ if force:
+ if '?' in url:
+ url += '&'
+ else:
+ url += '?'
+ url += 'force=1'
+
+ return url
+
+
+class UpdateChannelWriter(BaseLib):
+ """Class to handle the update channel as listed in channel-prefs.js"""
+ REGEX_UPDATE_CHANNEL = re.compile(r'("app\.update\.channel", ")([^"].*)(?=")')
+
+ def __init__(self, *args, **kwargs):
+ BaseLib.__init__(self, *args, **kwargs)
+
+ self.file_path = self.marionette.execute_script("""
+ Components.utils.import('resource://gre/modules/Services.jsm');
+
+ let file = Services.dirsvc.get('PrfDef', Components.interfaces.nsIFile);
+ file.append('channel-prefs.js');
+
+ return file.path;
+ """)
+
+ def set_channel(self, channel):
+ """Set default update channel.
+
+ :param channel: New default update channel
+ """
+ with open(self.file_path) as f:
+ file_contents = f.read()
+
+ new_content = re.sub(
+ self.REGEX_UPDATE_CHANNEL, r'\g<1>' + channel, file_contents)
+ with open(self.file_path, 'w') as f:
+ f.write(new_content)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py
new file mode 100644
index 000000000..2b4ef0766
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py
@@ -0,0 +1,140 @@
+# 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_driver.errors import MarionetteException
+
+from firefox_puppeteer.base import BaseLib
+
+
+class Utils(BaseLib):
+ """Low-level access to utility actions."""
+
+ def __init__(self, *args, **kwargs):
+ super(Utils, self).__init__(*args, **kwargs)
+
+ self._permissions = Permissions(self.marionette)
+
+ @property
+ def permissions(self):
+ """Handling of various permissions for hosts.
+
+ :returns: Instance of the Permissions class.
+ """
+ return self._permissions
+
+ def compare_version(self, a, b):
+ """Compare two version strings.
+
+ :param a: The first version.
+ :param b: The second version.
+
+ :returns: -1 if a is smaller than b, 0 if equal, and 1 if larger.
+ """
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ return Services.vc.compare(arguments[0], arguments[1]);
+ """, script_args=[a, b])
+
+ def sanitize(self, data_type):
+ """Sanitize user data, including cache, cookies, offlineApps, history, formdata,
+ downloads, passwords, sessions, siteSettings.
+
+ Usage:
+ sanitize(): Clears all user data.
+ sanitize({ "sessions": True }): Clears only session user data.
+
+ more: https://dxr.mozilla.org/mozilla-central/source/browser/base/content/sanitize.js
+
+ :param data_type: optional, Information specifying data to be sanitized
+ """
+
+ with self.marionette.using_context('chrome'):
+ result = self.marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ var data_type = arguments[0];
+
+ var data_type = (typeof data_type === "undefined") ? {} : {
+ cache: data_type.cache || false,
+ cookies: data_type.cookies || false,
+ downloads: data_type.downloads || false,
+ formdata: data_type.formdata || false,
+ history: data_type.history || false,
+ offlineApps: data_type.offlineApps || false,
+ passwords: data_type.passwords || false,
+ sessions: data_type.sessions || false,
+ siteSettings: data_type.siteSettings || false
+ };
+
+ // Load the sanitize script
+ var tempScope = {};
+ Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+
+ // Instantiate the Sanitizer
+ var s = new tempScope.Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+ var itemPrefs = Services.prefs.getBranch(s.prefDomain);
+
+ // Apply options for what to sanitize
+ for (var pref in data_type) {
+ itemPrefs.setBoolPref(pref, data_type[pref]);
+ };
+
+ // Sanitize and wait for the promise to resolve
+ var finished = false;
+ s.sanitize().then(() => {
+ for (let pref in data_type) {
+ itemPrefs.clearUserPref(pref);
+ };
+ marionetteScriptFinished(true);
+ }, aError => {
+ for (let pref in data_type) {
+ itemPrefs.clearUserPref(pref);
+ };
+ marionetteScriptFinished(false);
+ });
+ """, script_args=[data_type])
+
+ if not result:
+ raise MarionetteException('Sanitizing of profile data failed.')
+
+
+class Permissions(BaseLib):
+
+ def add(self, host, permission):
+ """Add a permission for web host.
+
+ Permissions include safe-browsing, install, geolocation, and others described here:
+ https://dxr.mozilla.org/mozilla-central/source/browser/modules/SitePermissions.jsm#144
+ and elsewhere.
+
+ :param host: The web host whose permission will be added.
+ :param permission: The type of permission to be added.
+ """
+ with self.marionette.using_context('chrome'):
+ self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let uri = Services.io.newURI(arguments[0], null, null);
+ Services.perms.add(uri, arguments[1],
+ Components.interfaces.nsIPermissionManager.ALLOW_ACTION);
+ """, script_args=[host, permission])
+
+ def remove(self, host, permission):
+ """Remove a permission for web host.
+
+ Permissions include safe-browsing, install, geolocation, and others described here:
+ https://dxr.mozilla.org/mozilla-central/source/browser/modules/SitePermissions.jsm#144
+ and elsewhere.
+
+ :param host: The web host whose permission will be removed.
+ :param permission: The type of permission to be removed.
+ """
+ with self.marionette.using_context('chrome'):
+ self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let uri = Services.io.newURI(arguments[0], null, null);
+ Services.perms.remove(uri, arguments[1]);
+ """, script_args=[host, permission])