summaryrefslogtreecommitdiffstats
path: root/testing/marionette/puppeteer/firefox/firefox_puppeteer
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/marionette/puppeteer/firefox/firefox_puppeteer
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/marionette/puppeteer/firefox/firefox_puppeteer')
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/__init__.py9
-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
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/base.py14
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/decorators.py35
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/errors.py21
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/mixins.py101
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/puppeteer.py84
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/__init__.py0
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py0
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py174
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py32
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/base.py54
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py3
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py116
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py388
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py641
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py260
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/deck.py17
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/menu.py110
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py0
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py204
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py61
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py5
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py46
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py291
-rw-r--r--testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/windows.py435
33 files changed, 4057 insertions, 0 deletions
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/__init__.py
new file mode 100644
index 000000000..bc6605102
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/__init__.py
@@ -0,0 +1,9 @@
+# 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.mixins import PuppeteerMixin
+from firefox_puppeteer.puppeteer import Puppeteer
+
+
+__version__ = '52.1.0'
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])
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/base.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/base.py
new file mode 100644
index 000000000..8c805e2b5
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/base.py
@@ -0,0 +1,14 @@
+# 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 BaseLib(object):
+ """A base class that handles lazily setting the "client" class attribute."""
+
+ def __init__(self, marionette):
+ self._marionette = marionette
+
+ @property
+ def marionette(self):
+ return self._marionette
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/decorators.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/decorators.py
new file mode 100644
index 000000000..1cdb64b00
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/decorators.py
@@ -0,0 +1,35 @@
+# 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 functools import wraps
+from importlib import import_module
+
+
+class use_class_as_property(object):
+ """
+ This decorator imports a library module and sets an instance
+ of the associated class as an attribute on the Puppeteer
+ object and returns it.
+
+ Note: return value of the wrapped function is ignored.
+ """
+ def __init__(self, lib):
+ self.lib = lib
+ self.mod_name, self.cls_name = self.lib.rsplit('.', 1)
+
+ def __call__(self, func):
+ @property
+ @wraps(func)
+ def _(cls, *args, **kwargs):
+ tag = '_{}_{}'.format(self.mod_name, self.cls_name)
+ prop = getattr(cls, tag, None)
+
+ if not prop:
+ module = import_module('.{}'.format(self.mod_name),
+ 'firefox_puppeteer')
+ prop = getattr(module, self.cls_name)(cls.marionette)
+ setattr(cls, tag, prop)
+ func(cls, *args, **kwargs)
+ return prop
+ return _
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/errors.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/errors.py
new file mode 100644
index 000000000..a518422e9
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/errors.py
@@ -0,0 +1,21 @@
+# 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
+
+
+class NoCertificateError(MarionetteException):
+ pass
+
+
+class UnexpectedWindowTypeError(MarionetteException):
+ pass
+
+
+class UnknownTabError(MarionetteException):
+ pass
+
+
+class UnknownWindowError(MarionetteException):
+ pass
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/mixins.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/mixins.py
new file mode 100644
index 000000000..645aa40a3
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/mixins.py
@@ -0,0 +1,101 @@
+# 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.puppeteer import Puppeteer
+from firefox_puppeteer.ui.browser.window import BrowserWindow
+
+
+class PuppeteerMixin(object):
+ """Mix-in class for Firefox specific API modules exposed to test scope.
+
+ It also provides common set-up and tear-down code for Firefox tests.
+
+ Child test case classes are expected to also subclass MarionetteTestCase such
+ that PuppeteerMixin is followed by MarionetteTestCase. This will insert the
+ Puppeteer mixin before the MarionetteTestCase into the MRO.
+
+ example:
+ `class MyTestCase(PuppeteerMixin, MarionetteTestCase)`
+
+ The key role of MarionetteTestCase is to set self.marionette appropriately
+ in `setUp()`. Any TestCase class that satisfies this requirement is
+ compatible with this class.
+
+ If you're extending the inheritance tree further to make specialized
+ TestCases, favour the use of super() as opposed to explicit calls to a
+ parent class.
+
+ """
+ def _check_and_fix_leaked_handles(self):
+ handle_count = len(self.marionette.window_handles)
+ url = []
+
+ try:
+ # Verify the existence of leaked tabs and print their URLs.
+ if self._start_handle_count < handle_count:
+ message = ('A test must not leak window handles. This test started with '
+ '%s open top level browsing contexts, but ended with %s.'
+ ' Remaining Tabs URLs:') % (self._start_handle_count, handle_count)
+ with self.marionette.using_context('content'):
+ for tab in self.marionette.window_handles:
+ if tab not in self._init_tab_handles:
+ url.append(' %s' % self.marionette.get_url())
+ self.assertListEqual(self._init_tab_handles, self.marionette.window_handles,
+ message + ','.join(url))
+ finally:
+ # For clean-up make sure we work on a proper browser window
+ if not self.browser or self.browser.closed:
+ # Find a proper replacement browser window
+ # TODO: We have to make this less error prone in case no browser is open.
+ self.browser = self.puppeteer.windows.switch_to(
+ lambda win: type(win) is BrowserWindow)
+
+ # Ensure to close all the remaining chrome windows to give following
+ # tests a proper start condition and make them not fail.
+ self.puppeteer.windows.close_all([self.browser])
+ self.browser.focus()
+
+ # Also close all remaining tabs
+ self.browser.tabbar.close_all_tabs([self.browser.tabbar.tabs[0]])
+ self.browser.tabbar.tabs[0].switch_to()
+
+ def restart(self, **kwargs):
+ """Restart Firefox and re-initialize data.
+
+ :param flags: Specific restart flags for Firefox
+ """
+ if kwargs.get('clean'):
+ self.marionette.restart(clean=True)
+ else:
+ self.marionette.restart(in_app=True)
+
+ # Ensure that we always have a valid browser instance available
+ self.browser = self.puppeteer.windows.switch_to(lambda win: type(win) is BrowserWindow)
+
+ def setUp(self, *args, **kwargs):
+ super(PuppeteerMixin, self).setUp(*args, **kwargs)
+
+ self._start_handle_count = len(self.marionette.window_handles)
+ self._init_tab_handles = self.marionette.window_handles
+ self.marionette.set_context('chrome')
+
+ self.puppeteer = Puppeteer(self.marionette)
+ self.browser = self.puppeteer.windows.current
+ self.browser.focus()
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ # Bug 1312674 - Navigating to about:blank twice can cause a hang in
+ # Marionette. So try to always have a known default page loaded.
+ self.marionette.navigate('about:')
+
+ def tearDown(self, *args, **kwargs):
+ self.marionette.set_context('chrome')
+
+ try:
+ # This code should be run after all other tearDown code
+ # so that in case of a failure, further tests will not run
+ # in a state that is more inconsistent than necessary.
+ self._check_and_fix_leaked_handles()
+ finally:
+ super(PuppeteerMixin, self).tearDown(*args, **kwargs)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/puppeteer.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/puppeteer.py
new file mode 100644
index 000000000..1894a414b
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/puppeteer.py
@@ -0,0 +1,84 @@
+# 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 decorators import use_class_as_property
+
+
+class Puppeteer(object):
+ """The puppeteer class is used to expose additional API and UI libraries.
+
+ Each library can be referenced by its puppeteer name as a member of a
+ Puppeteer instance. For example, the `current_window` member of the
+ `Browser` class can be accessed via `puppeteer.browser.current_window`.
+ """
+
+ def __init__(self, marionette):
+ self._marionette = marionette
+
+ @property
+ def marionette(self):
+ return self._marionette
+
+ @use_class_as_property('api.appinfo.AppInfo')
+ def appinfo(self):
+ """
+ Provides access to members of the appinfo api.
+
+ See the :class:`~api.appinfo.AppInfo` reference.
+ """
+
+ @use_class_as_property('api.keys.Keys')
+ def keys(self):
+ """
+ Provides a definition of control keys to use with keyboard shortcuts.
+ For example, keys.CONTROL or keys.ALT.
+ See the :class:`~api.keys.Keys` reference.
+ """
+
+ @use_class_as_property('api.places.Places')
+ def places(self):
+ """Provides low-level access to several bookmark and history related actions.
+
+ See the :class:`~api.places.Places` reference.
+ """
+
+ @use_class_as_property('api.utils.Utils')
+ def utils(self):
+ """Provides an api for interacting with utility actions.
+
+ See the :class:`~api.utils.Utils` reference.
+ """
+
+ @property
+ def platform(self):
+ """Returns the lowercased platform name.
+
+ :returns: Platform name
+ """
+ return self.marionette.session_capabilities['platformName']
+
+ @use_class_as_property('api.prefs.Preferences')
+ def prefs(self):
+ """
+ Provides an api for setting and inspecting preferences, as see in
+ about:config.
+
+ See the :class:`~api.prefs.Preferences` reference.
+ """
+
+ @use_class_as_property('api.security.Security')
+ def security(self):
+ """
+ Provides an api for accessing security related properties and helpers.
+
+ See the :class:`~api.security.Security` reference.
+ """
+
+ @use_class_as_property('ui.windows.Windows')
+ def windows(self):
+ """
+ Provides shortcuts to the top-level windows.
+
+ See the :class:`~ui.window.Windows` reference.
+ """
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/__init__.py
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py
new file mode 100644
index 000000000..9d8d90603
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py
@@ -0,0 +1,174 @@
+# 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 import By
+
+from firefox_puppeteer.ui.base import UIBaseLib
+from firefox_puppeteer.ui.deck import Panel
+
+
+class Deck(UIBaseLib):
+
+ def _create_panel_for_id(self, panel_id):
+ """Creates an instance of :class:`Panel` for the specified panel id.
+
+ :param panel_id: The ID of the panel to create an instance of.
+
+ :returns: :class:`Panel` instance
+ """
+ mapping = {'apply': ApplyPanel,
+ 'checkForUpdates': CheckForUpdatesPanel,
+ 'checkingForUpdates': CheckingForUpdatesPanel,
+ 'downloadAndInstall': DownloadAndInstallPanel,
+ 'downloadFailed': DownloadFailedPanel,
+ 'downloading': DownloadingPanel,
+ 'noUpdatesFound': NoUpdatesFoundPanel,
+ }
+
+ panel = self.element.find_element(By.ID, panel_id)
+ return mapping.get(panel_id, Panel)(self.marionette, self.window, panel)
+
+ # Properties for visual elements of the deck #
+
+ @property
+ def apply(self):
+ """The :class:`ApplyPanel` instance for the apply panel.
+
+ :returns: :class:`ApplyPanel` instance.
+ """
+ return self._create_panel_for_id('apply')
+
+ @property
+ def check_for_updates(self):
+ """The :class:`CheckForUpdatesPanel` instance for the check for updates panel.
+
+ :returns: :class:`CheckForUpdatesPanel` instance.
+ """
+ return self._create_panel_for_id('checkForUpdates')
+
+ @property
+ def checking_for_updates(self):
+ """The :class:`CheckingForUpdatesPanel` instance for the checking for updates panel.
+
+ :returns: :class:`CheckingForUpdatesPanel` instance.
+ """
+ return self._create_panel_for_id('checkingForUpdates')
+
+ @property
+ def download_and_install(self):
+ """The :class:`DownloadAndInstallPanel` instance for the download and install panel.
+
+ :returns: :class:`DownloadAndInstallPanel` instance.
+ """
+ return self._create_panel_for_id('downloadAndInstall')
+
+ @property
+ def download_failed(self):
+ """The :class:`DownloadFailedPanel` instance for the download failed panel.
+
+ :returns: :class:`DownloadFailedPanel` instance.
+ """
+ return self._create_panel_for_id('downloadFailed')
+
+ @property
+ def downloading(self):
+ """The :class:`DownloadingPanel` instance for the downloading panel.
+
+ :returns: :class:`DownloadingPanel` instance.
+ """
+ return self._create_panel_for_id('downloading')
+
+ @property
+ def no_updates_found(self):
+ """The :class:`NoUpdatesFoundPanel` instance for the no updates found panel.
+
+ :returns: :class:`NoUpdatesFoundPanel` instance.
+ """
+ return self._create_panel_for_id('noUpdatesFound')
+
+ @property
+ def panels(self):
+ """List of all the :class:`Panel` instances of the current deck.
+
+ :returns: List of :class:`Panel` instances.
+ """
+ panels = self.marionette.execute_script("""
+ let deck = arguments[0];
+ let panels = [];
+
+ for (let index = 0; index < deck.children.length; index++) {
+ if (deck.children[index].id) {
+ panels.push(deck.children[index].id);
+ }
+ }
+
+ return panels;
+ """, script_args=[self.element])
+
+ return [self._create_panel_for_id(panel) for panel in panels]
+
+ @property
+ def selected_index(self):
+ """The index of the currently selected panel.
+
+ :return: Index of the selected panel.
+ """
+ return int(self.element.get_property('selectedIndex'))
+
+ @property
+ def selected_panel(self):
+ """A :class:`Panel` instance of the currently selected panel.
+
+ :returns: :class:`Panel` instance.
+ """
+ return self.panels[self.selected_index]
+
+
+class ApplyPanel(Panel):
+
+ @property
+ def button(self):
+ """The DOM element which represents the Update button.
+
+ :returns: Reference to the button element.
+ """
+ return self.element.find_element(By.ID, 'updateButton')
+
+
+class CheckForUpdatesPanel(Panel):
+
+ @property
+ def button(self):
+ """The DOM element which represents the Check for Updates button.
+
+ :returns: Reference to the button element.
+ """
+ return self.element.find_element(By.ID, 'checkForUpdatesButton')
+
+
+class CheckingForUpdatesPanel(Panel):
+ pass
+
+
+class DownloadAndInstallPanel(Panel):
+
+ @property
+ def button(self):
+ """The DOM element which represents the Download button.
+
+ :returns: Reference to the button element.
+ """
+ return self.element.find_element(By.ID, 'downloadAndInstallButton')
+
+
+class DownloadFailedPanel(Panel):
+ pass
+
+
+class DownloadingPanel(Panel):
+ pass
+
+
+class NoUpdatesFoundPanel(Panel):
+ pass
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py
new file mode 100644
index 000000000..25037a471
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py
@@ -0,0 +1,32 @@
+# 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 import By
+
+from firefox_puppeteer.ui.about_window.deck import Deck
+from firefox_puppeteer.ui.windows import BaseWindow, Windows
+
+
+class AboutWindow(BaseWindow):
+ """Representation of the About window."""
+ window_type = 'Browser:About'
+
+ dtds = [
+ 'chrome://branding/locale/brand.dtd',
+ 'chrome://browser/locale/aboutDialog.dtd',
+ ]
+
+ @property
+ def deck(self):
+ """The :class:`Deck` instance which represents the deck.
+
+ :returns: Reference to the deck.
+ """
+ self.switch_to()
+
+ deck = self.window_element.find_element(By.ID, 'updateDeck')
+ return Deck(self.marionette, self, deck)
+
+
+Windows.register_window(AboutWindow.window_type, AboutWindow)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/base.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/base.py
new file mode 100644
index 000000000..622568df9
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/base.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/.
+
+from marionette_driver.marionette import HTMLElement
+
+from firefox_puppeteer.base import BaseLib
+from firefox_puppeteer.ui.windows import BaseWindow
+
+
+class UIBaseLib(BaseLib):
+ """A base class for all UI element wrapper classes inside a chrome window."""
+
+ def __init__(self, marionette, window, element):
+ super(UIBaseLib, self).__init__(marionette)
+
+ assert isinstance(window, BaseWindow)
+ assert isinstance(element, HTMLElement)
+
+ self._window = window
+ self._element = element
+
+ @property
+ def element(self):
+ """Returns the reference to the underlying DOM element.
+
+ :returns: Reference to the DOM element
+ """
+ return self._element
+
+ @property
+ def window(self):
+ """Returns the reference to the chrome window.
+
+ :returns: :class:`BaseWindow` instance of the chrome window.
+ """
+ return self._window
+
+
+class DOMElement(HTMLElement):
+ """
+ Class that inherits from HTMLElement and provides a way for subclasses to
+ expose new api's.
+ """
+
+ def __new__(cls, element):
+ instance = object.__new__(cls)
+ instance.__dict__ = element.__dict__.copy()
+ setattr(instance, 'inner', element)
+
+ return instance
+
+ def __init__(self, element):
+ pass
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py
new file mode 100644
index 000000000..c580d191c
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py
@@ -0,0 +1,3 @@
+# 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/.
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py
new file mode 100644
index 000000000..2cf67ce7f
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py
@@ -0,0 +1,116 @@
+# 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 abc import ABCMeta
+
+from marionette_driver import By
+
+from firefox_puppeteer.ui.base import UIBaseLib
+
+
+class BaseNotification(UIBaseLib):
+ """Abstract base class for any kind of notification."""
+
+ __metaclass__ = ABCMeta
+
+ @property
+ def close_button(self):
+ """Provide access to the close button.
+
+ :returns: The close button.
+ """
+ return self.element.find_element(By.ANON_ATTRIBUTE,
+ {'anonid': 'closebutton'})
+
+ @property
+ def label(self):
+ """Provide access to the notification label.
+
+ :returns: The notification label.
+ """
+ return self.element.get_attribute('label')
+
+ @property
+ def origin(self):
+ """Provide access to the notification origin.
+
+ :returns: The notification origin.
+ """
+ return self.element.get_attribute('origin')
+
+ def close(self, force=False):
+ """Close the notification.
+
+ :param force: Optional, if True force close the notification.
+ Defaults to False.
+ """
+ if force:
+ self.marionette.execute_script('arguments[0].click()',
+ script_args=[self.close_button])
+ else:
+ self.close_button.click()
+
+ self.window.wait_for_notification(None)
+
+
+class AddOnInstallBlockedNotification(BaseNotification):
+ """Add-on install blocked notification."""
+
+ @property
+ def allow_button(self):
+ """Provide access to the allow button.
+
+ :returns: The allow button.
+ """
+ return self.element.find_element(
+ By.ANON_ATTRIBUTE, {'anonid': 'button'}).find_element(
+ By.ANON_ATTRIBUTE, {'anonid': 'button'})
+
+
+class AddOnInstallConfirmationNotification(BaseNotification):
+ """Add-on install confirmation notification."""
+
+ @property
+ def addon_name(self):
+ """Provide access to the add-on name.
+
+ :returns: The add-on name.
+ """
+ label = self.element.find_element(
+ By.CSS_SELECTOR, '#addon-install-confirmation-content label')
+ return label.get_attribute('value')
+
+ def cancel_button(self):
+ """Provide access to the cancel button.
+
+ :returns: The cancel button.
+ """
+ return self.element.find_element(
+ By.ID, 'addon-install-confirmation-cancel')
+
+ def install_button(self):
+ """Provide access to the install button.
+
+ :returns: The install button.
+ """
+ return self.element.find_element(
+ By.ID, 'addon-install-confirmation-accept')
+
+
+class AddOnInstallCompleteNotification(BaseNotification):
+ """Add-on install complete notification."""
+
+ pass
+
+
+class AddOnInstallFailedNotification(BaseNotification):
+ """Add-on install failed notification."""
+
+ pass
+
+
+class AddOnProgressNotification(BaseNotification):
+ """Add-on progress notification."""
+
+ pass
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py
new file mode 100644
index 000000000..4fec98d99
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py
@@ -0,0 +1,388 @@
+# 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 import (
+ By, Wait
+)
+
+from marionette_driver.errors import NoSuchElementException
+
+import firefox_puppeteer.errors as errors
+
+from firefox_puppeteer.api.security import Security
+from firefox_puppeteer.ui.base import UIBaseLib, DOMElement
+
+
+class TabBar(UIBaseLib):
+ """Wraps the tabs toolbar DOM element inside a browser window."""
+
+ # Properties for visual elements of the tabs toolbar #
+
+ @property
+ def menupanel(self):
+ """A :class:`MenuPanel` instance which represents the menu panel
+ at the far right side of the tabs toolbar.
+
+ :returns: :class:`MenuPanel` instance.
+ """
+ return MenuPanel(self.marionette, self.window)
+
+ @property
+ def newtab_button(self):
+ """The DOM element which represents the new tab button.
+
+ :returns: Reference to the new tab button.
+ """
+ return self.toolbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'tabs-newtab-button'})
+
+ @property
+ def tabs(self):
+ """List of all the :class:`Tab` instances of the current browser window.
+
+ :returns: List of :class:`Tab` instances.
+ """
+ tabs = self.toolbar.find_elements(By.TAG_NAME, 'tab')
+
+ return [Tab(self.marionette, self.window, tab) for tab in tabs]
+
+ @property
+ def toolbar(self):
+ """The DOM element which represents the tab toolbar.
+
+ :returns: Reference to the tabs toolbar.
+ """
+ return self.element
+
+ # Properties for helpers when working with the tabs toolbar #
+
+ @property
+ def selected_index(self):
+ """The index of the currently selected tab.
+
+ :return: Index of the selected tab.
+ """
+ return int(self.toolbar.get_property('selectedIndex'))
+
+ @property
+ def selected_tab(self):
+ """A :class:`Tab` instance of the currently selected tab.
+
+ :returns: :class:`Tab` instance.
+ """
+ return self.tabs[self.selected_index]
+
+ # Methods for helpers when working with the tabs toolbar #
+
+ def close_all_tabs(self, exceptions=None):
+ """Forces closing of all open tabs.
+
+ There is an optional `exceptions` list, which can be used to exclude
+ specific tabs from being closed.
+
+ :param exceptions: Optional, list of :class:`Tab` instances not to close.
+ """
+ # Get handles from tab exceptions, and find those which can be closed
+ for tab in self.tabs:
+ if tab not in exceptions:
+ tab.close(force=True)
+
+ def close_tab(self, tab=None, trigger='menu', force=False):
+ """Closes the tab by using the specified trigger.
+
+ By default the currently selected tab will be closed. If another :class:`Tab`
+ is specified, that one will be closed instead. Also when the tab is closed, a
+ :func:`switch_to` call is automatically performed, so that the new selected
+ tab becomes active.
+
+ :param tab: Optional, the :class:`Tab` instance to close. Defaults to
+ the currently selected tab.
+
+ :param trigger: Optional, method to close the current tab. This can
+ be a string with one of `menu` or `shortcut`, or a callback which gets triggered
+ with the :class:`Tab` as parameter. Defaults to `menu`.
+
+ :param force: Optional, forces the closing of the window by using the Gecko API.
+ Defaults to `False`.
+ """
+ tab = tab or self.selected_tab
+ tab.close(trigger, force)
+
+ def open_tab(self, trigger='menu'):
+ """Opens a new tab in the current browser window.
+
+ If the tab opens in the foreground, a call to :func:`switch_to` will
+ automatically be performed. But if it opens in the background, the current
+ tab will keep its focus.
+
+ :param trigger: Optional, method to open the new tab. This can
+ be a string with one of `menu`, `button` or `shortcut`, or a callback
+ which gets triggered with the current :class:`Tab` as parameter.
+ Defaults to `menu`.
+
+ :returns: :class:`Tab` instance for the opened tab.
+ """
+ start_handles = self.marionette.window_handles
+
+ # Prepare action which triggers the opening of the browser window
+ if callable(trigger):
+ trigger(self.selected_tab)
+ elif trigger == 'button':
+ self.window.tabbar.newtab_button.click()
+ elif trigger == 'menu':
+ self.window.menubar.select_by_id('file-menu',
+ 'menu_newNavigatorTab')
+ elif trigger == 'shortcut':
+ self.window.send_shortcut(self.window.localize_entity('tabCmd.commandkey'),
+ accel=True)
+ # elif - need to add other cases
+ else:
+ raise ValueError('Unknown opening method: "%s"' % trigger)
+
+ # TODO: Needs to be replaced with event handling code (bug 1121705)
+ Wait(self.marionette).until(
+ lambda mn: len(mn.window_handles) == len(start_handles) + 1,
+ message='No new tab has been opened.')
+
+ handles = self.marionette.window_handles
+ [new_handle] = list(set(handles) - set(start_handles))
+ [new_tab] = [tab for tab in self.tabs if tab.handle == new_handle]
+
+ # if the new tab is the currently selected tab, switch to it
+ if new_tab == self.selected_tab:
+ new_tab.switch_to()
+
+ return new_tab
+
+ def switch_to(self, target):
+ """Switches the context to the specified tab.
+
+ :param target: The tab to switch to. `target` can be an index, a :class:`Tab`
+ instance, or a callback that returns True in the context of the desired tab.
+
+ :returns: Instance of the selected :class:`Tab`.
+ """
+ start_handle = self.marionette.current_window_handle
+
+ if isinstance(target, int):
+ return self.tabs[target].switch_to()
+ elif isinstance(target, Tab):
+ return target.switch_to()
+ if callable(target):
+ for tab in self.tabs:
+ tab.switch_to()
+ if target(tab):
+ return tab
+
+ self.marionette.switch_to_window(start_handle)
+ raise errors.UnknownTabError("No tab found for '{}'".format(target))
+
+ raise ValueError("The 'target' parameter must either be an index or a callable")
+
+ @staticmethod
+ def get_handle_for_tab(marionette, tab_element):
+ """Retrieves the marionette handle for the given :class:`Tab` instance.
+
+ :param marionette: An instance of the Marionette client.
+
+ :param tab_element: The DOM element corresponding to a tab inside the tabs toolbar.
+
+ :returns: `handle` of the tab.
+ """
+ # TODO: This introduces coupling with marionette's window handles
+ # implementation. To avoid this, the capacity to get the XUL
+ # element corresponding to the active window according to
+ # marionette or a similar ability should be added to marionette.
+ handle = marionette.execute_script("""
+ let win = arguments[0].linkedBrowser;
+ if (!win) {
+ return null;
+ }
+ return win.outerWindowID.toString();
+ """, script_args=[tab_element])
+
+ return handle
+
+
+class Tab(UIBaseLib):
+ """Wraps a tab DOM element."""
+
+ def __init__(self, marionette, window, element):
+ super(Tab, self).__init__(marionette, window, element)
+
+ self._security = Security(self.marionette)
+ self._handle = None
+
+ # Properties for visual elements of tabs #
+
+ @property
+ def close_button(self):
+ """The DOM element which represents the tab close button.
+
+ :returns: Reference to the tab close button.
+ """
+ return self.tab_element.find_element(By.ANON_ATTRIBUTE, {'anonid': 'close-button'})
+
+ @property
+ def tab_element(self):
+ """The inner tab DOM element.
+
+ :returns: Tab DOM element.
+ """
+ return self.element
+
+ # Properties for backend values
+
+ @property
+ def location(self):
+ """Returns the current URL
+
+ :returns: Current URL
+ """
+ self.switch_to()
+
+ return self.marionette.execute_script("""
+ return arguments[0].linkedBrowser.currentURI.spec;
+ """, script_args=[self.tab_element])
+
+ @property
+ def certificate(self):
+ """The SSL certificate assiciated with the loaded web page.
+
+ :returns: Certificate details as JSON blob.
+ """
+ self.switch_to()
+
+ return self._security.get_certificate_for_page(self.tab_element)
+
+ # Properties for helpers when working with tabs #
+
+ @property
+ def handle(self):
+ """The `handle` of the content window.
+
+ :returns: content window `handle`.
+ """
+ # If no handle has been set yet, wait until it is available
+ if not self._handle:
+ self._handle = Wait(self.marionette).until(
+ lambda mn: TabBar.get_handle_for_tab(mn, self.element),
+ message='Tab handle could not be found.')
+
+ return self._handle
+
+ @property
+ def selected(self):
+ """Checks if the tab is selected.
+
+ :return: `True` if the tab is selected.
+ """
+ return self.marionette.execute_script("""
+ return arguments[0].hasAttribute('selected');
+ """, script_args=[self.tab_element])
+
+ # Methods for helpers when working with tabs #
+
+ def __eq__(self, other):
+ return self.handle == other.handle
+
+ def close(self, trigger='menu', force=False):
+ """Closes the tab by using the specified trigger.
+
+ When the tab is closed a :func:`switch_to` call is automatically performed, so that
+ the new selected tab becomes active.
+
+ :param trigger: Optional, method in how to close the tab. This can
+ be a string with one of `button`, `menu` or `shortcut`, or a callback which
+ gets triggered with the current :class:`Tab` as parameter. Defaults to `menu`.
+
+ :param force: Optional, forces the closing of the window by using the Gecko API.
+ Defaults to `False`.
+ """
+ handle = self.handle
+ start_handles = self.marionette.window_handles
+
+ self.switch_to()
+
+ if force:
+ self.marionette.close()
+ elif callable(trigger):
+ trigger(self)
+ elif trigger == 'button':
+ self.close_button.click()
+ elif trigger == 'menu':
+ self.window.menubar.select_by_id('file-menu', 'menu_close')
+ elif trigger == 'shortcut':
+ self.window.send_shortcut(self.window.localize_entity('closeCmd.key'),
+ accel=True)
+ else:
+ raise ValueError('Unknown closing method: "%s"' % trigger)
+
+ Wait(self.marionette).until(
+ lambda _: len(self.window.tabbar.tabs) == len(start_handles) - 1,
+ message='Tab with handle "%s" has not been closed.' % handle)
+
+ # Ensure to switch to the window handle which represents the new selected tab
+ self.window.tabbar.selected_tab.switch_to()
+
+ def select(self):
+ """Selects the tab and sets the focus to it."""
+ self.tab_element.click()
+ self.switch_to()
+
+ # Bug 1121705: Maybe we have to wait for TabSelect event
+ Wait(self.marionette).until(
+ lambda _: self.selected,
+ message='Tab with handle "%s" could not be selected.' % self.handle)
+
+ def switch_to(self):
+ """Switches the context of Marionette to this tab.
+
+ Please keep in mind that calling this method will not select the tab.
+ Use the :func:`~Tab.select` method instead.
+ """
+ self.marionette.switch_to_window(self.handle)
+
+
+class MenuPanel(UIBaseLib):
+
+ @property
+ def popup(self):
+ """
+ :returns: The :class:`MenuPanelElement`.
+ """
+ popup = self.marionette.find_element(By.ID, 'PanelUI-popup')
+ return self.MenuPanelElement(popup)
+
+ class MenuPanelElement(DOMElement):
+ """Wraps the menu panel."""
+ _buttons = None
+
+ @property
+ def buttons(self):
+ """
+ :returns: A list of all the clickable buttons in the menu panel.
+ """
+ if not self._buttons:
+ self._buttons = (self.find_element(By.ID, 'PanelUI-multiView')
+ .find_element(By.ANON_ATTRIBUTE,
+ {'anonid': 'viewContainer'})
+ .find_elements(By.TAG_NAME,
+ 'toolbarbutton'))
+ return self._buttons
+
+ def click(self, target=None):
+ """
+ Overrides HTMLElement.click to provide a target to click.
+
+ :param target: The label associated with the button to click on,
+ e.g., `New Private Window`.
+ """
+ if not target:
+ return DOMElement.click(self)
+
+ for button in self.buttons:
+ if button.get_attribute('label') == target:
+ return button.click()
+ raise NoSuchElementException('Could not find "{}"" in the '
+ 'menu panel UI'.format(target))
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py
new file mode 100644
index 000000000..d490e488f
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py
@@ -0,0 +1,641 @@
+# 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 import By, keys, Wait
+
+from firefox_puppeteer.ui.base import UIBaseLib
+
+
+class NavBar(UIBaseLib):
+ """Provides access to the DOM elements contained in the
+ navigation bar as well as the location bar."""
+
+ def __init__(self, *args, **kwargs):
+ super(NavBar, self).__init__(*args, **kwargs)
+
+ self._locationbar = None
+
+ @property
+ def back_button(self):
+ """Provides access to the DOM element back button in the navbar.
+
+ :returns: Reference to the back button.
+ """
+ return self.marionette.find_element(By.ID, 'back-button')
+
+ @property
+ def forward_button(self):
+ """Provides access to the DOM element forward button in the navbar.
+
+ :returns: Reference to the forward button.
+ """
+ return self.marionette.find_element(By.ID, 'forward-button')
+
+ @property
+ def home_button(self):
+ """Provides access to the DOM element home button in the navbar.
+
+ :returns: Reference to the home button element
+ """
+ return self.marionette.find_element(By.ID, 'home-button')
+
+ @property
+ def locationbar(self):
+ """Provides access to the DOM elements contained in the
+ locationbar.
+
+ See the :class:`LocationBar` reference.
+ """
+ if not self._locationbar:
+ urlbar = self.marionette.find_element(By.ID, 'urlbar')
+ self._locationbar = LocationBar(self.marionette, self.window, urlbar)
+
+ return self._locationbar
+
+ @property
+ def menu_button(self):
+ """Provides access to the DOM element menu button in the navbar.
+
+ :returns: Reference to the menu button element.
+ """
+ return self.marionette.find_element(By.ID, 'PanelUI-menu-button')
+
+ @property
+ def toolbar(self):
+ """The DOM element which represents the navigation toolbar.
+
+ :returns: Reference to the navigation toolbar.
+ """
+ return self.element
+
+
+class LocationBar(UIBaseLib):
+ """Provides access to and methods for the DOM elements contained in the
+ locationbar (the text area of the ui that typically displays the current url)."""
+
+ def __init__(self, *args, **kwargs):
+ super(LocationBar, self).__init__(*args, **kwargs)
+
+ self._autocomplete_results = None
+ self._identity_popup = None
+
+ @property
+ def autocomplete_results(self):
+ """Provides access to and methods for the location bar
+ autocomplete results.
+
+ See the :class:`AutocompleteResults` reference."""
+ if not self._autocomplete_results:
+ popup = self.marionette.find_element(By.ID, 'PopupAutoCompleteRichResult')
+ self._autocomplete_results = AutocompleteResults(self.marionette,
+ self.window, popup)
+
+ return self._autocomplete_results
+
+ def clear(self):
+ """Clears the contents of the url bar (via the DELETE shortcut)."""
+ self.focus('shortcut')
+ self.urlbar.send_keys(keys.Keys.DELETE)
+ Wait(self.marionette).until(
+ lambda _: self.value == '',
+ message='Contents of location bar could not be cleared.')
+
+ def close_context_menu(self):
+ """Closes the Location Bar context menu by a key event."""
+ # TODO: This method should be implemented via the menu API.
+ self.contextmenu.send_keys(keys.Keys.ESCAPE)
+
+ @property
+ def connection_icon(self):
+ """ Provides access to the urlbar connection icon.
+
+ :returns: Reference to the connection icon element.
+ """
+ return self.marionette.find_element(By.ID, 'connection-icon')
+
+ @property
+ def contextmenu(self):
+ """Provides access to the urlbar context menu.
+
+ :returns: Reference to the urlbar context menu.
+ """
+ # TODO: This method should be implemented via the menu API.
+ parent = self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'textbox-input-box'})
+ return parent.find_element(By.ANON_ATTRIBUTE, {'anonid': 'input-box-contextmenu'})
+
+ @property
+ def focused(self):
+ """Checks the focus state of the location bar.
+
+ :returns: `True` if focused, otherwise `False`
+ """
+ return self.urlbar.get_attribute('focused') == 'true'
+
+ @property
+ def identity_icon(self):
+ """ Provides access to the urlbar identity icon.
+
+ :returns: Reference to the identity icon element.
+ """
+ return self.marionette.find_element(By.ID, 'identity-icon')
+
+ def focus(self, event='click'):
+ """Focus the location bar according to the provided event.
+
+ :param eventt: The event to synthesize in order to focus the urlbar
+ (one of `click` or `shortcut`).
+ """
+ if event == 'click':
+ self.urlbar.click()
+ elif event == 'shortcut':
+ cmd_key = self.window.localize_entity('openCmd.commandkey')
+ self.window.send_shortcut(cmd_key, accel=True)
+ else:
+ raise ValueError("An unknown event type was passed: %s" % event)
+
+ Wait(self.marionette).until(
+ lambda _: self.focused,
+ message='Location bar has not be focused.')
+
+ def get_contextmenu_entry(self, action):
+ """Retrieves the urlbar context menu entry corresponding
+ to the given action.
+
+ :param action: The action corresponding to the retrieved value.
+ :returns: Reference to the urlbar contextmenu entry.
+ """
+ # TODO: This method should be implemented via the menu API.
+ entries = self.contextmenu.find_elements(By.CSS_SELECTOR, 'menuitem')
+ filter_on = 'cmd_%s' % action
+ found = [e for e in entries if e.get_attribute('cmd') == filter_on]
+ return found[0] if len(found) else None
+
+ @property
+ def history_drop_marker(self):
+ """Provides access to the history drop marker.
+
+ :returns: Reference to the history drop marker.
+ """
+ return self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'historydropmarker'})
+
+ @property
+ def identity_box(self):
+ """The DOM element which represents the identity box.
+
+ :returns: Reference to the identity box.
+ """
+ return self.marionette.find_element(By.ID, 'identity-box')
+
+ @property
+ def identity_country_label(self):
+ """The DOM element which represents the identity icon country label.
+
+ :returns: Reference to the identity icon country label.
+ """
+ return self.marionette.find_element(By.ID, 'identity-icon-country-label')
+
+ @property
+ def identity_organization_label(self):
+ """The DOM element which represents the identity icon label.
+
+ :returns: Reference to the identity icon label.
+ """
+ return self.marionette.find_element(By.ID, 'identity-icon-label')
+
+ @property
+ def identity_popup(self):
+ """Provides utility members for accessing and manipulating the
+ identity popup.
+
+ See the :class:`IdentityPopup` reference.
+ """
+ if not self._identity_popup:
+ popup = self.marionette.find_element(By.ID, 'identity-popup')
+ self._identity_popup = IdentityPopup(self.marionette,
+ self.window, popup)
+
+ return self._identity_popup
+
+ def load_url(self, url):
+ """Load the specified url in the location bar by synthesized
+ keystrokes.
+
+ :param url: The url to load.
+ """
+ self.clear()
+ self.focus('shortcut')
+ self.urlbar.send_keys(url + keys.Keys.ENTER)
+
+ @property
+ def notification_popup(self):
+ """Provides access to the DOM element notification popup.
+
+ :returns: Reference to the notification popup.
+ """
+ return self.marionette.find_element(By.ID, "notification-popup")
+
+ def open_identity_popup(self):
+ """Open the identity popup."""
+ self.identity_box.click()
+ Wait(self.marionette).until(
+ lambda _: self.identity_popup.is_open,
+ message='Identity popup has not been opened.')
+
+ @property
+ def reload_button(self):
+ """Provides access to the DOM element reload button.
+
+ :returns: Reference to the reload button.
+ """
+ return self.marionette.find_element(By.ID, 'urlbar-reload-button')
+
+ def reload_url(self, trigger='button', force=False):
+ """Reload the currently open page.
+
+ :param trigger: The event type to use to cause the reload (one of
+ `shortcut`, `shortcut2`, or `button`).
+ :param force: Whether to cause a forced reload.
+ """
+ # TODO: The force parameter is ignored for the moment. Use
+ # mouse event modifiers or actions when they're ready.
+ # Bug 1097705 tracks this feature in marionette.
+ if trigger == 'button':
+ self.reload_button.click()
+ elif trigger == 'shortcut':
+ cmd_key = self.window.localize_entity('reloadCmd.commandkey')
+ self.window.send_shortcut(cmd_key)
+ elif trigger == 'shortcut2':
+ self.window.send_shortcut(keys.Keys.F5)
+
+ @property
+ def stop_button(self):
+ """Provides access to the DOM element stop button.
+
+ :returns: Reference to the stop button.
+ """
+ return self.marionette.find_element(By.ID, 'urlbar-stop-button')
+
+ @property
+ def urlbar(self):
+ """Provides access to the DOM element urlbar.
+
+ :returns: Reference to the url bar.
+ """
+ return self.marionette.find_element(By.ID, 'urlbar')
+
+ @property
+ def urlbar_input(self):
+ """Provides access to the urlbar input element.
+
+ :returns: Reference to the urlbar input.
+ """
+ return self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'input'})
+
+ @property
+ def value(self):
+ """Provides access to the currently displayed value of the urlbar.
+
+ :returns: The urlbar value.
+ """
+ return self.urlbar.get_property('value')
+
+
+class AutocompleteResults(UIBaseLib):
+ """Wraps DOM elements and methods for interacting with autocomplete results."""
+
+ def close(self, force=False):
+ """Closes the urlbar autocomplete popup.
+
+ :param force: If true, the popup is closed by its own hide function,
+ otherwise a key event is sent to close the popup.
+ """
+ if not self.is_open:
+ return
+
+ if force:
+ self.marionette.execute_script("""
+ arguments[0].hidePopup();
+ """, script_args=[self.element])
+ else:
+ self.element.send_keys(keys.Keys.ESCAPE)
+
+ Wait(self.marionette).until(
+ lambda _: not self.is_open,
+ message='Autocomplete popup has not been closed.')
+
+ def get_matching_text(self, result, match_type):
+ """Returns an array of strings of the matching text within an autocomplete
+ result in the urlbar.
+
+ :param result: The result to inspect for matches.
+ :param match_type: The type of match to search for (one of `title` or `url`).
+ """
+
+ if match_type not in ('title', 'url'):
+ raise ValueError('match_type provided must be one of'
+ '"title" or "url", not %s' % match_type)
+
+ # Search for nodes of the given type with emphasized text
+ emphasized_nodes = result.find_elements(
+ By.ANON_ATTRIBUTE,
+ {'class': 'ac-emphasize-text ac-emphasize-text-%s' % match_type}
+ )
+
+ return [node.get_property('textContent') for node in emphasized_nodes]
+
+ @property
+ def visible_results(self):
+ """Supplies the list of visible autocomplete result nodes.
+
+ :returns: The list of visible results.
+ """
+ match_count = self.element.get_property('_matchCount')
+
+ return self.marionette.execute_script("""
+ let rv = [];
+ let node = arguments[0];
+ let count = arguments[1];
+
+ for (let i = 0; i < count; ++i) {
+ rv.push(node.getItemAtIndex(i));
+ }
+
+ return rv;
+ """, script_args=[self.results, match_count])
+
+ @property
+ def is_open(self):
+ """Returns whether this popup is currently open.
+
+ :returns: True when the popup is open, otherwise false.
+ """
+ return self.element.get_property('state') == 'open'
+
+ @property
+ def is_complete(self):
+ """Returns when this popup is open and autocomplete results are complete.
+
+ :returns: True, when autocomplete results have been populated.
+ """
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ let win = Services.focus.activeWindow;
+ if (win) {
+ return win.gURLBar.controller.searchStatus >=
+ Components.interfaces.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+ }
+
+ return null;
+ """)
+
+ @property
+ def results(self):
+ """
+ :returns: The autocomplete result container node.
+ """
+ return self.element.find_element(By.ANON_ATTRIBUTE,
+ {'anonid': 'richlistbox'})
+
+ @property
+ def selected_index(self):
+ """Provides the index of the selected item in the autocomplete list.
+
+ :returns: The index.
+ """
+ return self.results.get_property('selectedIndex')
+
+
+class IdentityPopup(UIBaseLib):
+ """Wraps DOM elements and methods for interacting with the identity popup."""
+
+ def __init__(self, *args, **kwargs):
+ super(IdentityPopup, self).__init__(*args, **kwargs)
+
+ self._view = None
+
+ @property
+ def is_open(self):
+ """Returns whether this popup is currently open.
+
+ :returns: True when the popup is open, otherwise false.
+ """
+ return self.element.get_property('state') == 'open'
+
+ def close(self, force=False):
+ """Closes the identity popup by hitting the escape key.
+
+ :param force: Optional, If `True` force close the popup.
+ Defaults to `False`
+ """
+ if not self.is_open:
+ return
+
+ if force:
+ self.marionette.execute_script("""
+ arguments[0].hidePopup();
+ """, script_args=[self.element])
+ else:
+ self.element.send_keys(keys.Keys.ESCAPE)
+
+ Wait(self.marionette).until(
+ lambda _: not self.is_open,
+ message='Identity popup has not been closed.')
+
+ @property
+ def view(self):
+ """Provides utility members for accessing and manipulating the
+ identity popup's multi view.
+
+ See the :class:`IdentityPopupMultiView` reference.
+ """
+ if not self._view:
+ view = self.marionette.find_element(By.ID, 'identity-popup-multiView')
+ self._view = IdentityPopupMultiView(self.marionette, self.window, view)
+
+ return self._view
+
+
+class IdentityPopupMultiView(UIBaseLib):
+
+ def _create_view_for_id(self, view_id):
+ """Creates an instance of :class:`IdentityPopupView` for the specified view id.
+
+ :param view_id: The ID of the view to create an instance of.
+
+ :returns: :class:`IdentityPopupView` instance
+ """
+ mapping = {'identity-popup-mainView': IdentityPopupMainView,
+ 'identity-popup-securityView': IdentityPopupSecurityView,
+ }
+
+ view = self.marionette.find_element(By.ID, view_id)
+ return mapping.get(view_id, IdentityPopupView)(self.marionette, self.window, view)
+
+ @property
+ def main(self):
+ """The DOM element which represents the main view.
+
+ :returns: Reference to the main view.
+ """
+ return self._create_view_for_id('identity-popup-mainView')
+
+ @property
+ def security(self):
+ """The DOM element which represents the security view.
+
+ :returns: Reference to the security view.
+ """
+ return self._create_view_for_id('identity-popup-securityView')
+
+
+class IdentityPopupView(UIBaseLib):
+
+ @property
+ def selected(self):
+ """Checks if the view is selected.
+
+ :return: `True` if the view is selected.
+ """
+ return self.element.get_attribute('current') == 'true'
+
+
+class IdentityPopupMainView(IdentityPopupView):
+
+ @property
+ def selected(self):
+ """Checks if the view is selected.
+
+ :return: `True` if the view is selected.
+ """
+ return self.marionette.execute_script("""
+ return arguments[0].panelMultiView.getAttribute('viewtype') == 'main';
+ """, script_args=[self.element])
+
+ @property
+ def expander(self):
+ """The DOM element which represents the expander button for the security content.
+
+ :returns: Reference to the identity popup expander button.
+ """
+ return self.element.find_element(By.CLASS_NAME, 'identity-popup-expander')
+
+ @property
+ def host(self):
+ """The DOM element which represents the identity-popup content host.
+
+ :returns: Reference to the identity-popup content host.
+ """
+ return self.element.find_element(By.CLASS_NAME, 'identity-popup-headline host')
+
+ @property
+ def insecure_connection_label(self):
+ """The DOM element which represents the identity popup insecure connection label.
+
+ :returns: Reference to the identity-popup insecure connection label.
+ """
+ return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-not-secure')
+
+ @property
+ def internal_connection_label(self):
+ """The DOM element which represents the identity popup internal connection label.
+
+ :returns: Reference to the identity-popup internal connection label.
+ """
+ return self.element.find_element(By.CSS_SELECTOR, 'description[when-connection=chrome]')
+
+ @property
+ def permissions(self):
+ """The DOM element which represents the identity-popup permissions content.
+
+ :returns: Reference to the identity-popup permissions.
+ """
+ return self.element.find_element(By.ID, 'identity-popup-permissions-content')
+
+ @property
+ def secure_connection_label(self):
+ """The DOM element which represents the identity popup secure connection label.
+
+ :returns: Reference to the identity-popup secure connection label.
+ """
+ return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-secure')
+
+
+class IdentityPopupSecurityView(IdentityPopupView):
+
+ @property
+ def disable_mixed_content_blocking_button(self):
+ """The DOM element which represents the disable mixed content blocking button.
+
+ :returns: Reference to the disable mixed content blocking button.
+ """
+ return self.element.find_element(By.CSS_SELECTOR,
+ 'button[when-mixedcontent=active-blocked]')
+
+ @property
+ def enable_mixed_content_blocking_button(self):
+ """The DOM element which represents the enable mixed content blocking button.
+
+ :returns: Reference to the enable mixed content blocking button.
+ """
+ return self.element.find_element(By.CSS_SELECTOR,
+ 'button[when-mixedcontent=active-loaded]')
+
+ @property
+ def host(self):
+ """The DOM element which represents the identity-popup content host.
+
+ :returns: Reference to the identity-popup content host.
+ """
+ return self.element.find_element(By.CLASS_NAME, 'identity-popup-headline host')
+
+ @property
+ def insecure_connection_label(self):
+ """The DOM element which represents the identity popup insecure connection label.
+
+ :returns: Reference to the identity-popup insecure connection label.
+ """
+ return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-not-secure')
+
+ @property
+ def more_info_button(self):
+ """The DOM element which represents the identity-popup more info button.
+
+ :returns: Reference to the identity-popup more info button.
+ """
+ label = self.window.localize_entity('identity.moreInfoLinkText2')
+
+ return self.element.find_element(By.CSS_SELECTOR, u'button[label="{}"]'.format(label))
+
+ @property
+ def owner(self):
+ """The DOM element which represents the identity-popup content owner.
+
+ :returns: Reference to the identity-popup content owner.
+ """
+ return self.element.find_element(By.ID, 'identity-popup-content-owner')
+
+ @property
+ def owner_location(self):
+ """The DOM element which represents the identity-popup content supplemental.
+
+ :returns: Reference to the identity-popup content supplemental.
+ """
+ return self.element.find_element(By.ID, 'identity-popup-content-supplemental')
+
+ @property
+ def secure_connection_label(self):
+ """The DOM element which represents the identity popup secure connection label.
+
+ :returns: Reference to the identity-popup secure connection label.
+ """
+ return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-secure')
+
+ @property
+ def verifier(self):
+ """The DOM element which represents the identity-popup content verifier.
+
+ :returns: Reference to the identity-popup content verifier.
+ """
+ return self.element.find_element(By.ID, 'identity-popup-content-verifier')
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py
new file mode 100644
index 000000000..728a2fd20
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py
@@ -0,0 +1,260 @@
+# 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 import By, Wait
+from marionette_driver.errors import NoSuchElementException
+
+from firefox_puppeteer.ui.about_window.window import AboutWindow
+from firefox_puppeteer.ui.browser.notifications import (
+ AddOnInstallBlockedNotification,
+ AddOnInstallConfirmationNotification,
+ AddOnInstallCompleteNotification,
+ AddOnInstallFailedNotification,
+ AddOnProgressNotification,
+ BaseNotification)
+from firefox_puppeteer.ui.browser.tabbar import TabBar
+from firefox_puppeteer.ui.browser.toolbars import NavBar
+from firefox_puppeteer.ui.pageinfo.window import PageInfoWindow
+from firefox_puppeteer.ui.windows import BaseWindow, Windows
+
+
+class BrowserWindow(BaseWindow):
+ """Representation of a browser window."""
+
+ window_type = 'navigator:browser'
+
+ dtds = [
+ 'chrome://branding/locale/brand.dtd',
+ 'chrome://browser/locale/aboutPrivateBrowsing.dtd',
+ 'chrome://browser/locale/browser.dtd',
+ 'chrome://browser/locale/netError.dtd',
+ ]
+
+ properties = [
+ 'chrome://branding/locale/brand.properties',
+ 'chrome://branding/locale/browserconfig.properties',
+ 'chrome://browser/locale/browser.properties',
+ 'chrome://browser/locale/preferences/preferences.properties',
+ 'chrome://global/locale/browser.properties',
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super(BrowserWindow, self).__init__(*args, **kwargs)
+
+ self._navbar = None
+ self._tabbar = None
+
+ @property
+ def default_homepage(self):
+ """The default homepage as used by the current locale.
+
+ :returns: The default homepage for the current locale.
+ """
+ return self.marionette.get_pref('browser.startup.homepage',
+ value_type='nsIPrefLocalizedString')
+
+ @property
+ def is_private(self):
+ """Returns True if this is a Private Browsing window."""
+ self.switch_to()
+
+ with self.marionette.using_context('chrome'):
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+ let chromeWindow = arguments[0].ownerDocument.defaultView;
+ return PrivateBrowsingUtils.isWindowPrivate(chromeWindow);
+ """, script_args=[self.window_element])
+
+ @property
+ def navbar(self):
+ """Provides access to the navigation bar. This is the toolbar containing
+ the back, forward and home buttons. It also contains the location bar.
+
+ See the :class:`~ui.browser.toolbars.NavBar` reference.
+ """
+ self.switch_to()
+
+ if not self._navbar:
+ navbar = self.window_element.find_element(By.ID, 'nav-bar')
+ self._navbar = NavBar(self.marionette, self, navbar)
+
+ return self._navbar
+
+ @property
+ def notification(self):
+ """Provides access to the currently displayed notification."""
+
+ notifications_map = {
+ 'addon-install-blocked-notification': AddOnInstallBlockedNotification,
+ 'addon-install-confirmation-notification': AddOnInstallConfirmationNotification,
+ 'addon-install-complete-notification': AddOnInstallCompleteNotification,
+ 'addon-install-failed-notification': AddOnInstallFailedNotification,
+ 'addon-progress-notification': AddOnProgressNotification,
+ }
+
+ try:
+ notification = self.window_element.find_element(
+ By.CSS_SELECTOR, '#notification-popup popupnotification')
+
+ notification_id = notification.get_attribute('id')
+ return notifications_map.get(notification_id, BaseNotification)(
+ self.marionette, self, notification)
+
+ except NoSuchElementException:
+ return None # no notification is displayed
+
+ def wait_for_notification(self, notification_class=BaseNotification,
+ timeout=5):
+ """Waits for the specified notification to be displayed.
+
+ :param notification_class: Optional, the notification class to wait for.
+ If `None` is specified it will wait for any notification to be closed.
+ Defaults to `BaseNotification`.
+ :param timeout: Optional, how long to wait for the expected notification.
+ Defaults to 5 seconds.
+ """
+ wait = Wait(self.marionette, timeout=timeout)
+
+ if notification_class:
+ if notification_class is BaseNotification:
+ message = 'No notification was shown.'
+ else:
+ message = '{0} was not shown.'.format(notification_class.__name__)
+ wait.until(
+ lambda _: isinstance(self.notification, notification_class),
+ message=message)
+ else:
+ message = 'Unexpected notification shown.'
+ wait.until(
+ lambda _: self.notification is None,
+ message='Unexpected notification shown.')
+
+ @property
+ def tabbar(self):
+ """Provides access to the tab bar.
+
+ See the :class:`~ui.browser.tabbar.TabBar` reference.
+ """
+ self.switch_to()
+
+ if not self._tabbar:
+ tabbrowser = self.window_element.find_element(By.ID, 'tabbrowser-tabs')
+ self._tabbar = TabBar(self.marionette, self, tabbrowser)
+
+ return self._tabbar
+
+ def close(self, trigger='menu', force=False):
+ """Closes the current browser window by using the specified trigger.
+
+ :param trigger: Optional, method to close the current browser window. This can
+ be a string with one of `menu` or `shortcut`, or a callback which gets triggered
+ with the current :class:`BrowserWindow` as parameter. Defaults to `menu`.
+
+ :param force: Optional, forces the closing of the window by using the Gecko API.
+ Defaults to `False`.
+ """
+ def callback(win):
+ # Prepare action which triggers the opening of the browser window
+ if callable(trigger):
+ trigger(win)
+ elif trigger == 'menu':
+ self.menubar.select_by_id('file-menu', 'menu_closeWindow')
+ elif trigger == 'shortcut':
+ win.send_shortcut(win.localize_entity('closeCmd.key'),
+ accel=True, shift=True)
+ else:
+ raise ValueError('Unknown closing method: "%s"' % trigger)
+
+ BaseWindow.close(self, callback, force)
+
+ def get_final_url(self, url):
+ """Loads the page at `url` and returns the resulting url.
+
+ This function enables testing redirects.
+
+ :param url: The url to test.
+ :returns: The resulting loaded url.
+ """
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(url)
+ return self.marionette.get_url()
+
+ def open_browser(self, trigger='menu', is_private=False):
+ """Opens a new browser window by using the specified trigger.
+
+ :param trigger: Optional, method in how to open the new browser window. This can
+ be a string with one of `menu` or `shortcut`, or a callback which gets triggered
+ with the current :class:`BrowserWindow` as parameter. Defaults to `menu`.
+
+ :param is_private: Optional, if True the new window will be a private browsing one.
+
+ :returns: :class:`BrowserWindow` instance for the new browser window.
+ """
+ def callback(win):
+ # Prepare action which triggers the opening of the browser window
+ if callable(trigger):
+ trigger(win)
+ elif trigger == 'menu':
+ menu_id = 'menu_newPrivateWindow' if is_private else 'menu_newNavigator'
+ self.menubar.select_by_id('file-menu', menu_id)
+ elif trigger == 'shortcut':
+ cmd_key = 'privateBrowsingCmd.commandkey' if is_private else 'newNavigatorCmd.key'
+ win.send_shortcut(win.localize_entity(cmd_key),
+ accel=True, shift=is_private)
+ else:
+ raise ValueError('Unknown opening method: "%s"' % trigger)
+
+ return BaseWindow.open_window(self, callback, BrowserWindow)
+
+ def open_about_window(self, trigger='menu'):
+ """Opens the about window by using the specified trigger.
+
+ :param trigger: Optional, method in how to open the new browser window. This can
+ either the string `menu` or a callback which gets triggered
+ with the current :class:`BrowserWindow` as parameter. Defaults to `menu`.
+
+ :returns: :class:`AboutWindow` instance of the opened window.
+ """
+ def callback(win):
+ # Prepare action which triggers the opening of the browser window
+ if callable(trigger):
+ trigger(win)
+ elif trigger == 'menu':
+ self.menubar.select_by_id('helpMenu', 'aboutName')
+ else:
+ raise ValueError('Unknown opening method: "%s"' % trigger)
+
+ return BaseWindow.open_window(self, callback, AboutWindow)
+
+ def open_page_info_window(self, trigger='menu'):
+ """Opens the page info window by using the specified trigger.
+
+ :param trigger: Optional, method in how to open the new browser window. This can
+ be a string with one of `menu` or `shortcut`, or a callback which gets triggered
+ with the current :class:`BrowserWindow` as parameter. Defaults to `menu`.
+
+ :returns: :class:`PageInfoWindow` instance of the opened window.
+ """
+ def callback(win):
+ # Prepare action which triggers the opening of the browser window
+ if callable(trigger):
+ trigger(win)
+ elif trigger == 'menu':
+ self.menubar.select_by_id('tools-menu', 'menu_pageInfo')
+ elif trigger == 'shortcut':
+ if win.marionette.session_capabilities['platformName'] == 'windows_nt':
+ raise ValueError('Page info shortcut not available on Windows.')
+ win.send_shortcut(win.localize_entity('pageInfoCmd.commandkey'),
+ accel=True)
+ elif trigger == 'context_menu':
+ # TODO: Add once we can do right clicks
+ pass
+ else:
+ raise ValueError('Unknown opening method: "%s"' % trigger)
+
+ return BaseWindow.open_window(self, callback, PageInfoWindow)
+
+
+Windows.register_window(BrowserWindow.window_type, BrowserWindow)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/deck.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/deck.py
new file mode 100644
index 000000000..acc6d2458
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/deck.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/.
+
+from firefox_puppeteer.ui.base import UIBaseLib
+
+
+class Panel(UIBaseLib):
+
+ def __eq__(self, other):
+ return self.element.get_attribute('id') == other.element.get_attribute('id')
+
+ def __ne__(self, other):
+ return self.element.get_attribute('id') != other.element.get_attribute('id')
+
+ def __str__(self):
+ return self.element.get_attribute('id')
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/menu.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/menu.py
new file mode 100644
index 000000000..8251daa01
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/menu.py
@@ -0,0 +1,110 @@
+# 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 import By
+from marionette_driver.errors import NoSuchElementException
+
+from firefox_puppeteer.base import BaseLib
+from firefox_puppeteer.ui.base import DOMElement
+
+
+class MenuBar(BaseLib):
+ """Wraps the menubar DOM element inside a browser window."""
+
+ @property
+ def menus(self):
+ """A list of :class:`MenuElement` instances corresponding to
+ the top level menus in the menubar.
+
+ :returns: A list of :class:`MenuElement` instances.
+ """
+ menus = (self.marionette.find_element(By.ID, 'main-menubar')
+ .find_elements(By.TAG_NAME, 'menu'))
+ return [self.MenuElement(menu) for menu in menus]
+
+ def get_menu(self, label):
+ """Get a :class:`MenuElement` instance corresponding to the specified label.
+
+ :param label: The label of the menu, e.g., **File** or **View**.
+ :returns: A :class:`MenuElement` instance.
+ """
+ menu = [m for m in self.menus if m.get_attribute('label') == label]
+
+ if not menu:
+ raise NoSuchElementException('Could not find a menu with '
+ 'label "{}"'.format(label))
+
+ return menu[0]
+
+ def get_menu_by_id(self, menu_id):
+ """Get a :class:`MenuElement` instance corresponding to the specified
+ ID.
+
+ :param menu_id: The ID of the menu, e.g., **file-menu** or **view-menu**.
+ :returns: A :class:`MenuElement` instance.
+ """
+ menu = [m for m in self.menus if m.get_attribute('id') == menu_id]
+
+ if not menu:
+ raise NoSuchElementException('Could not find a menu with '
+ 'id "{}"'.format(menu_id))
+
+ return menu[0]
+
+ def select(self, label, item):
+ """Select an item in a menu.
+
+ :param label: The label of the menu, e.g., **File** or **View**.
+ :param item: The label of the item in the menu, e.g., **New Tab**.
+ """
+ return self.get_menu(label).select(item)
+
+ def select_by_id(self, menu_id, item_id):
+ """Select an item in a menu.
+
+ :param menu_id: The ID of the menu, e.g. **file-menu** or **view-menu**.
+ :param item_id: The ID of the item in the menu, e.g. **menu_newNavigatorTab**.
+ """
+ return self.get_menu_by_id(menu_id).select_by_id(item_id)
+
+ class MenuElement(DOMElement):
+ """Wraps a menu element DOM element."""
+
+ @property
+ def items(self):
+ """A list of menuitem DOM elements within this :class:`MenuElement` instance.
+
+ :returns: A list of items in the menu.
+ """
+ return (self.find_element(By.TAG_NAME, 'menupopup')
+ .find_elements(By.TAG_NAME, 'menuitem'))
+
+ def select(self, label):
+ """Click on a menu item within this menu.
+
+ :param label: The label of the menu item, e.g., **New Tab**.
+ """
+ item = [l for l in self.items if l.get_attribute('label') == label]
+
+ if not item:
+ message = ("Item labeled '{}' not found in the '{}' menu"
+ .format(label, self.get_attribute('label')))
+ raise NoSuchElementException(message)
+
+ return item[0].click()
+
+ def select_by_id(self, menu_item_id):
+ """Click on a menu item within this menu.
+
+ :param menu_item_id: The ID of the menu item, e.g. **menu_newNavigatorTab**.
+ """
+ item = [l for l in self.items if l.get_attribute('id') ==
+ menu_item_id]
+
+ if not item:
+ message = ("Item with ID '{}' not found in the '{}' menu"
+ .format(menu_item_id, self.get_attribute('id')))
+ raise NoSuchElementException(message)
+
+ return item[0].click()
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py
new file mode 100644
index 000000000..0f2a2167a
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py
@@ -0,0 +1,204 @@
+# 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 import By, Wait
+
+from firefox_puppeteer.ui.base import UIBaseLib
+from firefox_puppeteer.ui.deck import Panel
+
+
+class Deck(UIBaseLib):
+
+ def _create_panel_for_id(self, panel_id):
+ """Creates an instance of :class:`Panel` for the specified panel id.
+
+ :param panel_id: The ID of the panel to create an instance of.
+
+ :returns: :class:`Panel` instance
+ """
+ mapping = {'feedPanel': FeedPanel,
+ 'generalPanel': GeneralPanel,
+ 'mediaPanel': MediaPanel,
+ 'permPanel': PermissionsPanel,
+ 'securityPanel': SecurityPanel
+ }
+
+ panel = self.element.find_element(By.ID, panel_id)
+ return mapping.get(panel_id, Panel)(self.marionette, self.window, panel)
+
+ # Properties for visual elements of the deck #
+
+ @property
+ def feed(self):
+ """The :class:`FeedPanel` instance for the feed panel.
+
+ :returns: :class:`FeedPanel` instance.
+ """
+ return self._create_panel_for_id('feedPanel')
+
+ @property
+ def general(self):
+ """The :class:`GeneralPanel` instance for the general panel.
+
+ :returns: :class:`GeneralPanel` instance.
+ """
+ return self._create_panel_for_id('generalPanel')
+
+ @property
+ def media(self):
+ """The :class:`MediaPanel` instance for the media panel.
+
+ :returns: :class:`MediaPanel` instance.
+ """
+ return self._create_panel_for_id('mediaPanel')
+
+ @property
+ def panels(self):
+ """List of all the :class:`Panel` instances of the current deck.
+
+ :returns: List of :class:`Panel` instances.
+ """
+ panels = self.marionette.execute_script("""
+ let deck = arguments[0];
+ let panels = [];
+
+ for (let index = 0; index < deck.children.length; index++) {
+ if (deck.children[index].id) {
+ panels.push(deck.children[index].id);
+ }
+ }
+
+ return panels;
+ """, script_args=[self.element])
+
+ return [self._create_panel_for_id(panel) for panel in panels]
+
+ @property
+ def permissions(self):
+ """The :class:`PermissionsPanel` instance for the permissions panel.
+
+ :returns: :class:`PermissionsPanel` instance.
+ """
+ return self._create_panel_for_id('permPanel')
+
+ @property
+ def security(self):
+ """The :class:`SecurityPanel` instance for the security panel.
+
+ :returns: :class:`SecurityPanel` instance.
+ """
+ return self._create_panel_for_id('securityPanel')
+
+ # Properties for helpers when working with the deck #
+
+ @property
+ def selected_index(self):
+ """The index of the currently selected panel.
+
+ :return: Index of the selected panel.
+ """
+ return int(self.element.get_property('selectedIndex'))
+
+ @property
+ def selected_panel(self):
+ """A :class:`Panel` instance of the currently selected panel.
+
+ :returns: :class:`Panel` instance.
+ """
+ return self.panels[self.selected_index]
+
+ # Methods for helpers when working with the deck #
+
+ def select(self, panel):
+ """Selects the specified panel via the tab element.
+
+ :param panel: The panel to select.
+
+ :returns: :class:`Panel` instance of the selected panel.
+ """
+ panel.tab.click()
+ Wait(self.marionette).until(
+ lambda _: self.selected_panel == panel,
+ message='Panel with ID "%s" could not be selected.' % panel)
+
+ return panel
+
+
+class PageInfoPanel(Panel):
+
+ @property
+ def tab(self):
+ """The DOM element which represents the corresponding tab element at the top.
+
+ :returns: Reference to the tab element.
+ """
+ name = self.element.get_property('id').split('Panel')[0]
+ return self.window.window_element.find_element(By.ID, name + 'Tab')
+
+
+class FeedPanel(PageInfoPanel):
+ pass
+
+
+class GeneralPanel(PageInfoPanel):
+ pass
+
+
+class MediaPanel(PageInfoPanel):
+ pass
+
+
+class PermissionsPanel(PageInfoPanel):
+ pass
+
+
+class SecurityPanel(PageInfoPanel):
+
+ @property
+ def domain(self):
+ """The DOM element which represents the domain textbox.
+
+ :returns: Reference to the textbox element.
+ """
+ return self.element.find_element(By.ID, 'security-identity-domain-value')
+
+ @property
+ def owner(self):
+ """The DOM element which represents the owner textbox.
+
+ :returns: Reference to the textbox element.
+ """
+ return self.element.find_element(By.ID, 'security-identity-owner-value')
+
+ @property
+ def verifier(self):
+ """The DOM element which represents the verifier textbox.
+
+ :returns: Reference to the textbox element.
+ """
+ return self.element.find_element(By.ID, 'security-identity-verifier-value')
+
+ @property
+ def view_certificate(self):
+ """The DOM element which represents the view certificate button.
+
+ :returns: Reference to the button element.
+ """
+ return self.element.find_element(By.ID, 'security-view-cert')
+
+ @property
+ def view_cookies(self):
+ """The DOM element which represents the view cookies button.
+
+ :returns: Reference to the button element.
+ """
+ return self.element.find_element(By.ID, 'security-view-cookies')
+
+ @property
+ def view_passwords(self):
+ """The DOM element which represents the view passwords button.
+
+ :returns: Reference to the button element.
+ """
+ return self.element.find_element(By.ID, 'security-view-password')
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py
new file mode 100644
index 000000000..070f39f79
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py
@@ -0,0 +1,61 @@
+# 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 import By
+
+from firefox_puppeteer.ui.pageinfo.deck import Deck
+from firefox_puppeteer.ui.windows import BaseWindow, Windows
+
+
+class PageInfoWindow(BaseWindow):
+ """Representation of a page info window."""
+
+ window_type = 'Browser:page-info'
+
+ dtds = [
+ 'chrome://browser/locale/pageInfo.dtd',
+ ]
+
+ properties = [
+ 'chrome://browser/locale/browser.properties',
+ 'chrome://browser/locale/pageInfo.properties',
+ 'chrome://pippki/locale/pippki.properties',
+ ]
+
+ @property
+ def deck(self):
+ """The :class:`Deck` instance which represents the deck.
+
+ :returns: Reference to the deck.
+ """
+ deck = self.window_element.find_element(By.ID, 'mainDeck')
+ return Deck(self.marionette, self, deck)
+
+ def close(self, trigger='shortcut', force=False):
+ """Closes the current page info window by using the specified trigger.
+
+ :param trigger: Optional, method to close the current window. This can
+ be a string with one of `menu` (OS X only) or `shortcut`, or a callback
+ which gets triggered with the current :class:`PageInfoWindow` as parameter.
+ Defaults to `shortcut`.
+
+ :param force: Optional, forces the closing of the window by using the Gecko API.
+ Defaults to `False`.
+ """
+ def callback(win):
+ # Prepare action which triggers the opening of the browser window
+ if callable(trigger):
+ trigger(win)
+ elif trigger == 'menu':
+ self.menubar.select_by_id('file-menu', 'menu_close')
+ elif trigger == 'shortcut':
+ win.send_shortcut(win.localize_entity('closeWindow.key'),
+ accel=True)
+ else:
+ raise ValueError('Unknown closing method: "%s"' % trigger)
+
+ super(PageInfoWindow, self).close(callback, force)
+
+
+Windows.register_window(PageInfoWindow.window_type, PageInfoWindow)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py
new file mode 100644
index 000000000..020996694
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py
@@ -0,0 +1,5 @@
+# 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 dialog import UpdateWizardDialog
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py
new file mode 100644
index 000000000..19435b211
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py
@@ -0,0 +1,46 @@
+# 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 import By, Wait
+
+from firefox_puppeteer.ui.update_wizard.wizard import Wizard
+from firefox_puppeteer.ui.windows import BaseWindow, Windows
+
+
+# Bug 1143020 - Subclass from BaseDialog ui class with possible wizard mixin
+class UpdateWizardDialog(BaseWindow):
+ """Representation of the old Software Update Wizard Dialog."""
+ window_type = 'Update:Wizard'
+
+ dtds = [
+ 'chrome://branding/locale/brand.dtd',
+ 'chrome://mozapps/locale/update/updates.dtd',
+ ]
+
+ properties = [
+ 'chrome://branding/locale/brand.properties',
+ 'chrome://mozapps/locale/update/updates.properties',
+ ]
+
+ @property
+ def wizard(self):
+ """The :class:`Wizard` instance which represents the wizard.
+
+ :returns: Reference to the wizard.
+ """
+ # The deck is also the root element
+ wizard = self.marionette.find_element(By.ID, 'updates')
+ return Wizard(self.marionette, self, wizard)
+
+ def select_next_page(self):
+ """Clicks on "Next" button, and waits for the next page to show up."""
+ current_panel = self.wizard.selected_panel
+
+ self.wizard.next_button.click()
+ Wait(self.marionette).until(
+ lambda _: self.wizard.selected_panel != current_panel,
+ message='Next panel has not been selected.')
+
+
+Windows.register_window(UpdateWizardDialog.window_type, UpdateWizardDialog)
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py
new file mode 100644
index 000000000..9687ac917
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py
@@ -0,0 +1,291 @@
+# 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 import By, Wait
+
+from firefox_puppeteer.ui.base import UIBaseLib
+from firefox_puppeteer.ui.deck import Panel
+
+
+class Wizard(UIBaseLib):
+
+ def __init__(self, *args, **kwargs):
+ super(Wizard, self).__init__(*args, **kwargs)
+
+ Wait(self.marionette).until(
+ lambda _: self.selected_panel,
+ message='No panel has been selected by default.')
+
+ def _create_panel_for_id(self, panel_id):
+ """Creates an instance of :class:`Panel` for the specified panel id.
+
+ :param panel_id: The ID of the panel to create an instance of.
+
+ :returns: :class:`Panel` instance
+ """
+ mapping = {'checking': CheckingPanel,
+ 'downloading': DownloadingPanel,
+ 'dummy': DummyPanel,
+ 'errorpatching': ErrorPatchingPanel,
+ 'errors': ErrorPanel,
+ 'errorextra': ErrorExtraPanel,
+ 'finished': FinishedPanel,
+ 'finishedBackground': FinishedBackgroundPanel,
+ 'manualUpdate': ManualUpdatePanel,
+ 'noupdatesfound': NoUpdatesFoundPanel,
+ 'updatesfoundbasic': UpdatesFoundBasicPanel,
+
+ # TODO: Remove once we no longer support version Firefox 45.0ESR
+ 'incompatibleCheck': IncompatibleCheckPanel,
+ 'incompatibleList': IncompatibleListPanel,
+ }
+
+ panel = self.element.find_element(By.ID, panel_id)
+ return mapping.get(panel_id, Panel)(self.marionette, self.window, panel)
+
+ # Properties for visual buttons of the wizard #
+
+ @property
+ def _buttons(self):
+ return self.element.find_element(By.ANON_ATTRIBUTE, {'anonid': 'Buttons'})
+
+ @property
+ def cancel_button(self):
+ return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'cancel'})
+
+ @property
+ def extra1_button(self):
+ return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'extra1'})
+
+ @property
+ def extra2_button(self):
+ return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'extra2'})
+
+ @property
+ def previous_button(self):
+ return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'back'})
+
+ @property
+ def finish_button(self):
+ return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'finish'})
+
+ @property
+ def next_button(self):
+ return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'next'})
+
+ # Properties for visual panels of the wizard #
+
+ @property
+ def checking(self):
+ """The checking for updates panel.
+
+ :returns: :class:`CheckingPanel` instance.
+ """
+ return self._create_panel_for_id('checking')
+
+ @property
+ def downloading(self):
+ """The downloading panel.
+
+ :returns: :class:`DownloadingPanel` instance.
+ """
+ return self._create_panel_for_id('downloading')
+
+ @property
+ def dummy(self):
+ """The dummy panel.
+
+ :returns: :class:`DummyPanel` instance.
+ """
+ return self._create_panel_for_id('dummy')
+
+ @property
+ def error_patching(self):
+ """The error patching panel.
+
+ :returns: :class:`ErrorPatchingPanel` instance.
+ """
+ return self._create_panel_for_id('errorpatching')
+
+ @property
+ def error(self):
+ """The errors panel.
+
+ :returns: :class:`ErrorPanel` instance.
+ """
+ return self._create_panel_for_id('errors')
+
+ @property
+ def error_extra(self):
+ """The error extra panel.
+
+ :returns: :class:`ErrorExtraPanel` instance.
+ """
+ return self._create_panel_for_id('errorextra')
+
+ @property
+ def finished(self):
+ """The finished panel.
+
+ :returns: :class:`FinishedPanel` instance.
+ """
+ return self._create_panel_for_id('finished')
+
+ @property
+ def finished_background(self):
+ """The finished background panel.
+
+ :returns: :class:`FinishedBackgroundPanel` instance.
+ """
+ return self._create_panel_for_id('finishedBackground')
+
+ @property
+ def incompatible_check(self):
+ """The incompatible check panel.
+
+ :returns: :class:`IncompatibleCheckPanel` instance.
+ """
+ return self._create_panel_for_id('incompatibleCheck')
+
+ @property
+ def incompatible_list(self):
+ """The incompatible list panel.
+
+ :returns: :class:`IncompatibleListPanel` instance.
+ """
+ return self._create_panel_for_id('incompatibleList')
+
+ @property
+ def manual_update(self):
+ """The manual update panel.
+
+ :returns: :class:`ManualUpdatePanel` instance.
+ """
+ return self._create_panel_for_id('manualUpdate')
+
+ @property
+ def no_updates_found(self):
+ """The no updates found panel.
+
+ :returns: :class:`NoUpdatesFoundPanel` instance.
+ """
+ return self._create_panel_for_id('noupdatesfound')
+
+ @property
+ def updates_found_basic(self):
+ """The updates found panel.
+
+ :returns: :class:`UpdatesFoundPanel` instance.
+ """
+ return self._create_panel_for_id('updatesfoundbasic')
+
+ @property
+ def panels(self):
+ """List of all the available :class:`Panel` instances.
+
+ :returns: List of :class:`Panel` instances.
+ """
+ panels = self.marionette.execute_script("""
+ let wizard = arguments[0];
+ let panels = [];
+
+ for (let index = 0; index < wizard.children.length; index++) {
+ if (wizard.children[index].id) {
+ panels.push(wizard.children[index].id);
+ }
+ }
+
+ return panels;
+ """, script_args=[self.element])
+
+ return [self._create_panel_for_id(panel) for panel in panels]
+
+ @property
+ def selected_index(self):
+ """The index of the currently selected panel.
+
+ :return: Index of the selected panel.
+ """
+ return int(self.element.get_property('pageIndex'))
+
+ @property
+ def selected_panel(self):
+ """A :class:`Panel` instance of the currently selected panel.
+
+ :returns: :class:`Panel` instance.
+ """
+ return self._create_panel_for_id(self.element.get_attribute('currentpageid'))
+
+
+class CheckingPanel(Panel):
+
+ @property
+ def progress(self):
+ """The DOM element which represents the progress meter.
+
+ :returns: Reference to the progress element.
+ """
+ return self.element.find_element(By.ID, 'checkingProgress')
+
+
+class DownloadingPanel(Panel):
+
+ @property
+ def progress(self):
+ """The DOM element which represents the progress meter.
+
+ :returns: Reference to the progress element.
+ """
+ return self.element.find_element(By.ID, 'downloadProgress')
+
+
+class DummyPanel(Panel):
+ pass
+
+
+class ErrorPatchingPanel(Panel):
+ pass
+
+
+class ErrorPanel(Panel):
+ pass
+
+
+class ErrorExtraPanel(Panel):
+ pass
+
+
+class FinishedPanel(Panel):
+ pass
+
+
+class FinishedBackgroundPanel(Panel):
+ pass
+
+
+class IncompatibleCheckPanel(Panel):
+
+ @property
+ def progress(self):
+ """The DOM element which represents the progress meter.
+
+ :returns: Reference to the progress element.
+ """
+ return self.element.find_element(By.ID, 'incompatibleCheckProgress')
+
+
+class IncompatibleListPanel(Panel):
+ pass
+
+
+class ManualUpdatePanel(Panel):
+ pass
+
+
+class NoUpdatesFoundPanel(Panel):
+ pass
+
+
+class UpdatesFoundBasicPanel(Panel):
+ pass
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/windows.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/windows.py
new file mode 100644
index 000000000..9248a3935
--- /dev/null
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/windows.py
@@ -0,0 +1,435 @@
+# 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 import By, Wait
+from marionette_driver.errors import NoSuchWindowException
+from marionette_driver.keys import Keys
+
+import firefox_puppeteer.errors as errors
+
+from firefox_puppeteer.api.l10n import L10n
+from firefox_puppeteer.base import BaseLib
+from firefox_puppeteer.decorators import use_class_as_property
+
+
+class Windows(BaseLib):
+
+ # Used for registering the different windows with this class to avoid
+ # circular dependencies with BaseWindow
+ windows_map = {}
+
+ @property
+ def all(self):
+ """Retrieves a list of all open chrome windows.
+
+ :returns: List of :class:`BaseWindow` instances corresponding to the
+ windows in `marionette.chrome_window_handles`.
+ """
+ return [self.create_window_instance(handle) for handle in
+ self.marionette.chrome_window_handles]
+
+ @property
+ def current(self):
+ """Retrieves the currently selected chrome window.
+
+ :returns: The :class:`BaseWindow` for the currently active window.
+ """
+ return self.create_window_instance(self.marionette.current_chrome_window_handle)
+
+ @property
+ def focused_chrome_window_handle(self):
+ """Returns the currently focused chrome window handle.
+
+ :returns: The `window handle` of the focused chrome window.
+ """
+ def get_active_handle(mn):
+ with self.marionette.using_context('chrome'):
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ let win = Services.focus.activeWindow;
+ if (win) {
+ return win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .outerWindowID.toString();
+ }
+
+ return null;
+ """)
+
+ # In case of `None` being returned no window is currently active. This can happen
+ # when a focus change action is currently happening. So lets wait until it is done.
+ return Wait(self.marionette).until(get_active_handle,
+ message='No focused window has been found.')
+
+ def close(self, handle):
+ """Closes the chrome window with the given handle.
+
+ :param handle: The handle of the chrome window.
+ """
+ self.switch_to(handle)
+
+ # TODO: Maybe needs to wait as handled via an observer
+ return self.marionette.close_chrome_window()
+
+ def close_all(self, exceptions=None):
+ """Closes all open chrome windows.
+
+ There is an optional `exceptions` list, which can be used to exclude
+ specific chrome windows from being closed.
+
+ :param exceptions: Optional, list of :class:`BaseWindow` instances not to close.
+ """
+ windows_to_keep = exceptions or []
+
+ # Get handles of windows_to_keep
+ handles_to_keep = [entry.handle for entry in windows_to_keep]
+
+ # Find handles to close and close them all
+ handles_to_close = set(self.marionette.chrome_window_handles) - set(handles_to_keep)
+ for handle in handles_to_close:
+ self.close(handle)
+
+ def create_window_instance(self, handle, expected_class=None):
+ """Creates a :class:`BaseWindow` instance for the given chrome window.
+
+ :param handle: The handle of the chrome window.
+ :param expected_class: Optional, check for the correct window class.
+ """
+ current_handle = self.marionette.current_chrome_window_handle
+ window = None
+
+ with self.marionette.using_context('chrome'):
+ try:
+ # Retrieve window type to determine the type of chrome window
+ if handle != self.marionette.current_chrome_window_handle:
+ self.switch_to(handle)
+ window_type = self.marionette.get_window_type()
+ finally:
+ # Ensure to switch back to the original window
+ if handle != current_handle:
+ self.switch_to(current_handle)
+
+ if window_type in self.windows_map:
+ window = self.windows_map[window_type](self.marionette, handle)
+ else:
+ window = BaseWindow(self.marionette, handle)
+
+ if expected_class is not None and type(window) is not expected_class:
+ raise errors.UnexpectedWindowTypeError('Expected window "%s" but got "%s"' %
+ (expected_class, type(window)))
+
+ # Before continuing ensure the chrome window has been completed loading
+ Wait(self.marionette).until(
+ lambda _: self.loaded(handle),
+ message='Chrome window with handle "%s" did not finish loading.' % handle)
+
+ return window
+
+ def focus(self, handle):
+ """Focuses the chrome window with the given handle.
+
+ :param handle: The handle of the chrome window.
+ """
+ self.switch_to(handle)
+
+ with self.marionette.using_context('chrome'):
+ self.marionette.execute_script(""" window.focus(); """)
+
+ Wait(self.marionette).until(
+ lambda _: handle == self.focused_chrome_window_handle,
+ message='Focus has not been set to chrome window handle "%s".' % handle)
+
+ def loaded(self, handle):
+ """Check if the chrome window with the given handle has been completed loading.
+
+ :param handle: The handle of the chrome window.
+
+ :returns: True, if the chrome window has been loaded.
+ """
+ with self.marionette.using_context('chrome'):
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ let win = Services.wm.getOuterWindowWithId(Number(arguments[0]));
+ return win.document.readyState == 'complete';
+ """, script_args=[handle])
+
+ def switch_to(self, target):
+ """Switches context to the specified chrome window.
+
+ :param target: The window to switch to. `target` can be a `handle` or a
+ callback that returns True in the context of the desired
+ window.
+
+ :returns: Instance of the selected :class:`BaseWindow`.
+ """
+ target_handle = None
+
+ if target in self.marionette.chrome_window_handles:
+ target_handle = target
+ elif callable(target):
+ current_handle = self.marionette.current_chrome_window_handle
+
+ # switches context if callback for a chrome window returns `True`.
+ for handle in self.marionette.chrome_window_handles:
+ self.marionette.switch_to_window(handle)
+ window = self.create_window_instance(handle)
+ if target(window):
+ target_handle = handle
+ break
+
+ # if no handle has been found switch back to original window
+ if not target_handle:
+ self.marionette.switch_to_window(current_handle)
+
+ if target_handle is None:
+ raise NoSuchWindowException("No window found for '{}'"
+ .format(target))
+
+ # only switch if necessary
+ if target_handle != self.marionette.current_chrome_window_handle:
+ self.marionette.switch_to_window(target_handle)
+
+ return self.create_window_instance(target_handle)
+
+ @classmethod
+ def register_window(cls, window_type, window_class):
+ """Registers a chrome window with this class so that this class may in
+ turn create the appropriate window instance later on.
+
+ :param window_type: The type of window.
+ :param window_class: The constructor of the window
+ """
+ cls.windows_map[window_type] = window_class
+
+
+class BaseWindow(BaseLib):
+ """Base class for any kind of chrome window."""
+
+ # l10n class attributes will be set by each window class individually
+ dtds = []
+ properties = []
+
+ def __init__(self, marionette, window_handle):
+ super(BaseWindow, self).__init__(marionette)
+
+ self._l10n = L10n(self.marionette)
+ self._windows = Windows(self.marionette)
+
+ if window_handle not in self.marionette.chrome_window_handles:
+ raise errors.UnknownWindowError('Window with handle "%s" does not exist' %
+ window_handle)
+ self._handle = window_handle
+
+ def __eq__(self, other):
+ return self.handle == other.handle
+
+ @property
+ def closed(self):
+ """Returns closed state of the chrome window.
+
+ :returns: True if the window has been closed.
+ """
+ return self.handle not in self.marionette.chrome_window_handles
+
+ @property
+ def focused(self):
+ """Returns `True` if the chrome window is focused.
+
+ :returns: True if the window is focused.
+ """
+ self.switch_to()
+
+ return self.handle == self._windows.focused_chrome_window_handle
+
+ @property
+ def handle(self):
+ """Returns the `window handle` of the chrome window.
+
+ :returns: `window handle`.
+ """
+ return self._handle
+
+ @property
+ def loaded(self):
+ """Checks if the window has been fully loaded.
+
+ :returns: True, if the window is loaded.
+ """
+ self._windows.loaded(self.handle)
+
+ @use_class_as_property('ui.menu.MenuBar')
+ def menubar(self):
+ """Provides access to the menu bar, for example, the **File** menu.
+
+ See the :class:`~ui.menu.MenuBar` reference.
+ """
+
+ @property
+ def window_element(self):
+ """Returns the inner DOM window element.
+
+ :returns: DOM window element.
+ """
+ self.switch_to()
+
+ return self.marionette.find_element(By.CSS_SELECTOR, ':root')
+
+ def close(self, callback=None, force=False):
+ """Closes the current chrome window.
+
+ If this is the last remaining window, the Marionette session is ended.
+
+ :param callback: Optional, function to trigger the window to open. It is
+ triggered with the current :class:`BaseWindow` as parameter.
+ Defaults to `window.open()`.
+
+ :param force: Optional, forces the closing of the window by using the Gecko API.
+ Defaults to `False`.
+ """
+ self.switch_to()
+
+ # Bug 1121698
+ # For more stable tests register an observer topic first
+ prev_win_count = len(self.marionette.chrome_window_handles)
+
+ handle = self.handle
+ if force or callback is None:
+ self._windows.close(handle)
+ else:
+ callback(self)
+
+ # Bug 1121698
+ # Observer code should let us ditch this wait code
+ Wait(self.marionette).until(
+ lambda m: len(m.chrome_window_handles) == prev_win_count - 1,
+ message='Chrome window with handle "%s" has not been closed.' % handle)
+
+ def focus(self):
+ """Sets the focus to the current chrome window."""
+ return self._windows.focus(self.handle)
+
+ def localize_entity(self, entity_id):
+ """Returns the localized string for the specified DTD entity id.
+
+ :param entity_id: The id to retrieve the value from.
+
+ :returns: The localized string for the requested entity.
+
+ :raises MarionetteException: When entity id is not found.
+ """
+ return self._l10n.localize_entity(self.dtds, entity_id)
+
+ def localize_property(self, property_id):
+ """Returns the localized string for the specified property id.
+
+ :param property_id: The id to retrieve the value from.
+
+ :returns: The localized string for the requested property.
+
+ :raises MarionetteException: When property id is not found.
+ """
+ return self._l10n.localize_property(self.properties, property_id)
+
+ def open_window(self, callback=None, expected_window_class=None, focus=True):
+ """Opens a new top-level chrome window.
+
+ :param callback: Optional, function to trigger the window to open. It is
+ triggered with the current :class:`BaseWindow` as parameter.
+ Defaults to `window.open()`.
+ :param expected_class: Optional, check for the correct window class.
+ :param focus: Optional, if true, focus the new window.
+ Defaults to `True`.
+ """
+ # Bug 1121698
+ # For more stable tests register an observer topic first
+ start_handles = self.marionette.chrome_window_handles
+
+ self.switch_to()
+ with self.marionette.using_context('chrome'):
+ if callback is not None:
+ callback(self)
+ else:
+ self.marionette.execute_script(""" window.open(); """)
+
+ # TODO: Needs to be replaced with observer handling code (bug 1121698)
+ def window_opened(mn):
+ return len(mn.chrome_window_handles) == len(start_handles) + 1
+ Wait(self.marionette).until(
+ window_opened,
+ message='No new chrome window has been opened.')
+
+ handles = self.marionette.chrome_window_handles
+ [new_handle] = list(set(handles) - set(start_handles))
+
+ assert new_handle is not None
+
+ window = self._windows.create_window_instance(new_handle, expected_window_class)
+
+ if focus:
+ window.focus()
+
+ return window
+
+ def send_shortcut(self, command_key, **kwargs):
+ """Sends a keyboard shortcut to the window.
+
+ :param command_key: The key (usually a letter) to be pressed.
+
+ :param accel: Optional, If `True`, the `Accel` modifier key is pressed.
+ This key differs between OS X (`Meta`) and Linux/Windows (`Ctrl`). Defaults to `False`.
+
+ :param alt: Optional, If `True`, the `Alt` modifier key is pressed. Defaults to `False`.
+
+ :param ctrl: Optional, If `True`, the `Ctrl` modifier key is pressed. Defaults to `False`.
+
+ :param meta: Optional, If `True`, the `Meta` modifier key is pressed. Defaults to `False`.
+
+ :param shift: Optional, If `True`, the `Shift` modifier key is pressed.
+ Defaults to `False`.
+ """
+
+ platform = self.marionette.session_capabilities['platformName']
+
+ keymap = {
+ 'accel': Keys.META if platform == 'darwin' else Keys.CONTROL,
+ 'alt': Keys.ALT,
+ 'cmd': Keys.COMMAND,
+ 'ctrl': Keys.CONTROL,
+ 'meta': Keys.META,
+ 'shift': Keys.SHIFT,
+ }
+
+ # Append all to press modifier keys
+ keys = []
+ for modifier in kwargs:
+ if modifier not in keymap:
+ raise KeyError('"%s" is not a known modifier' % modifier)
+
+ if kwargs[modifier] is True:
+ keys.append(keymap[modifier])
+
+ # Bug 1125209 - Only lower-case command keys should be sent
+ keys.append(command_key.lower())
+
+ self.switch_to()
+ self.window_element.send_keys(*keys)
+
+ def switch_to(self, focus=False):
+ """Switches the context to this chrome window.
+
+ By default it will not focus the window. If that behavior is wanted, the
+ `focus` parameter can be used.
+
+ :param focus: If `True`, the chrome window will be focused.
+
+ :returns: Current window as :class:`BaseWindow` instance.
+ """
+ if focus:
+ self._windows.focus(self.handle)
+ else:
+ self._windows.switch_to(self.handle)
+
+ return self