summaryrefslogtreecommitdiffstats
path: root/testing/firefox-ui/harness
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/firefox-ui/harness
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/firefox-ui/harness')
-rw-r--r--testing/firefox-ui/harness/MANIFEST.in2
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/__init__.py8
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/arguments/__init__.py6
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/arguments/base.py18
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/arguments/update.py65
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/cli_functional.py21
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/cli_update.py21
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/runners/__init__.py6
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/runners/base.py45
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/runners/update.py101
-rw-r--r--testing/firefox-ui/harness/firefox_ui_harness/testcases.py420
-rw-r--r--testing/firefox-ui/harness/requirements.txt5
-rw-r--r--testing/firefox-ui/harness/setup.py44
13 files changed, 762 insertions, 0 deletions
diff --git a/testing/firefox-ui/harness/MANIFEST.in b/testing/firefox-ui/harness/MANIFEST.in
new file mode 100644
index 000000000..cf628b039
--- /dev/null
+++ b/testing/firefox-ui/harness/MANIFEST.in
@@ -0,0 +1,2 @@
+exclude MANIFEST.in
+include requirements.txt
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/__init__.py b/testing/firefox-ui/harness/firefox_ui_harness/__init__.py
new file mode 100644
index 000000000..02ae10cdf
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/__init__.py
@@ -0,0 +1,8 @@
+# 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/.
+
+__version__ = '1.4.0'
+
+import cli_functional
+import cli_update
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/arguments/__init__.py b/testing/firefox-ui/harness/firefox_ui_harness/arguments/__init__.py
new file mode 100644
index 000000000..57dc77f60
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/arguments/__init__.py
@@ -0,0 +1,6 @@
+# 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_ui_harness.arguments.base import FirefoxUIArguments
+from firefox_ui_harness.arguments.update import UpdateArguments
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/arguments/base.py b/testing/firefox-ui/harness/firefox_ui_harness/arguments/base.py
new file mode 100644
index 000000000..e6427e764
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/arguments/base.py
@@ -0,0 +1,18 @@
+# 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_harness import BaseMarionetteArguments
+
+
+class FirefoxUIBaseArguments(object):
+ name = 'Firefox UI Tests'
+ args = []
+
+
+class FirefoxUIArguments(BaseMarionetteArguments):
+
+ def __init__(self, **kwargs):
+ super(FirefoxUIArguments, self).__init__(**kwargs)
+
+ self.register_argument_container(FirefoxUIBaseArguments())
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/arguments/update.py b/testing/firefox-ui/harness/firefox_ui_harness/arguments/update.py
new file mode 100644
index 000000000..9b6d3e5e0
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/arguments/update.py
@@ -0,0 +1,65 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from base import FirefoxUIArguments
+
+
+class UpdateBaseArguments(object):
+ name = 'Firefox UI Update Tests'
+ args = [
+ [['--update-allow-mar-channel'], {
+ 'dest': 'update_mar_channels',
+ 'default': [],
+ 'action': 'append',
+ 'metavar': 'MAR_CHANNEL',
+ 'help': 'Additional MAR channel to be allowed for updates, '
+ 'e.g. "firefox-mozilla-beta" for updating a release '
+ 'build to the latest beta build.'
+ }],
+ [['--update-channel'], {
+ 'dest': 'update_channel',
+ 'metavar': 'CHANNEL',
+ 'help': 'Channel to use for the update check.'
+ }],
+ [['--update-direct-only'], {
+ 'dest': 'update_direct_only',
+ 'default': False,
+ 'action': 'store_true',
+ 'help': 'Only perform a direct update'
+ }],
+ [['--update-fallback-only'], {
+ 'dest': 'update_fallback_only',
+ 'default': False,
+ 'action': 'store_true',
+ 'help': 'Only perform a fallback update'
+ }],
+ [['--update-override-url'], {
+ 'dest': 'update_override_url',
+ 'metavar': 'URL',
+ 'help': 'Force specified URL to use for update checks.'
+ }],
+ [['--update-target-version'], {
+ 'dest': 'update_target_version',
+ 'metavar': 'VERSION',
+ 'help': 'Version of the updated build.'
+ }],
+ [['--update-target-buildid'], {
+ 'dest': 'update_target_buildid',
+ 'metavar': 'BUILD_ID',
+ 'help': 'Build ID of the updated build.'
+ }],
+ ]
+
+ def verify_usage_handler(self, args):
+ if args.update_direct_only and args.update_fallback_only:
+ raise ValueError('Arguments --update-direct-only and --update-fallback-only '
+ 'are mutually exclusive.')
+
+
+class UpdateArguments(FirefoxUIArguments):
+
+ def __init__(self, **kwargs):
+ super(UpdateArguments, self).__init__(**kwargs)
+
+ self.register_argument_container(UpdateBaseArguments())
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/cli_functional.py b/testing/firefox-ui/harness/firefox_ui_harness/cli_functional.py
new file mode 100644
index 000000000..e83d88b51
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/cli_functional.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# 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_harness.runtests import cli as mn_cli
+
+from firefox_ui_harness.arguments import FirefoxUIArguments
+from firefox_ui_harness.runners import FirefoxUITestRunner
+
+
+def cli(args=None):
+ mn_cli(runner_class=FirefoxUITestRunner,
+ parser_class=FirefoxUIArguments,
+ args=args,
+ )
+
+
+if __name__ == '__main__':
+ cli()
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/cli_update.py b/testing/firefox-ui/harness/firefox_ui_harness/cli_update.py
new file mode 100644
index 000000000..27446a099
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/cli_update.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# 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_harness.runtests import cli as mn_cli
+
+from firefox_ui_harness.arguments import UpdateArguments
+from firefox_ui_harness.runners import UpdateTestRunner
+
+
+def cli(args=None):
+ mn_cli(runner_class=UpdateTestRunner,
+ parser_class=UpdateArguments,
+ args=args,
+ )
+
+
+if __name__ == '__main__':
+ cli()
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/runners/__init__.py b/testing/firefox-ui/harness/firefox_ui_harness/runners/__init__.py
new file mode 100644
index 000000000..9022a45b8
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/runners/__init__.py
@@ -0,0 +1,6 @@
+# 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_ui_harness.runners.base import FirefoxUITestRunner
+from firefox_ui_harness.runners.update import UpdateTestRunner
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/runners/base.py b/testing/firefox-ui/harness/firefox_ui_harness/runners/base.py
new file mode 100644
index 000000000..66c2a5308
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/runners/base.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/.
+
+import os
+import shutil
+import tempfile
+
+import mozfile
+import mozinfo
+
+from marionette_harness import BaseMarionetteTestRunner, MarionetteTestCase
+
+
+class FirefoxUITestRunner(BaseMarionetteTestRunner):
+
+ def __init__(self, **kwargs):
+ super(FirefoxUITestRunner, self).__init__(**kwargs)
+
+ # select the appropriate GeckoInstance
+ self.app = 'fxdesktop'
+
+ self.test_handlers = [MarionetteTestCase]
+
+ def duplicate_application(self, application_folder):
+ """Creates a copy of the specified binary."""
+
+ if self.workspace:
+ target_folder = os.path.join(self.workspace_path, 'application.copy')
+ else:
+ target_folder = tempfile.mkdtemp('.application.copy')
+
+ self.logger.info('Creating a copy of the application at "%s".' % target_folder)
+ mozfile.remove(target_folder)
+ shutil.copytree(application_folder, target_folder)
+
+ return target_folder
+
+ def get_application_folder(self, binary):
+ """Returns the directory of the application."""
+ if mozinfo.isMac:
+ end_index = binary.find('.app') + 4
+ return binary[:end_index]
+ else:
+ return os.path.dirname(binary)
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/runners/update.py b/testing/firefox-ui/harness/firefox_ui_harness/runners/update.py
new file mode 100644
index 000000000..fe4936f68
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/runners/update.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/.
+
+import sys
+
+import mozfile
+import mozinstall
+
+from firefox_ui_harness.runners import FirefoxUITestRunner
+from firefox_ui_harness.testcases import UpdateTestCase
+
+
+DEFAULT_PREFS = {
+ # Bug 1355026: Re-enable when support for the new simplified UI update is available
+ 'app.update.doorhanger': False,
+ 'app.update.log': True,
+ 'startup.homepage_override_url': 'about:blank',
+}
+
+
+class UpdateTestRunner(FirefoxUITestRunner):
+
+ def __init__(self, **kwargs):
+ super(UpdateTestRunner, self).__init__(**kwargs)
+
+ self.original_bin = self.bin
+
+ self.prefs.update(DEFAULT_PREFS)
+
+ # In case of overriding the update URL, set the appropriate preference
+ override_url = kwargs.pop('update_override_url', None)
+ if override_url:
+ self.prefs.update({'app.update.url.override': override_url})
+
+ self.run_direct_update = not kwargs.pop('update_fallback_only', False)
+ self.run_fallback_update = not kwargs.pop('update_direct_only', False)
+
+ self.test_handlers = [UpdateTestCase]
+
+ def run_tests(self, tests):
+ # Used to store the last occurred exception because we execute
+ # run_tests() multiple times
+ self.exc_info = None
+
+ failed = 0
+ source_folder = self.get_application_folder(self.original_bin)
+
+ results = {}
+
+ def _run_tests(tags):
+ application_folder = None
+
+ try:
+ # Backup current tags
+ test_tags = self.test_tags
+
+ application_folder = self.duplicate_application(source_folder)
+ self.bin = mozinstall.get_binary(application_folder, 'Firefox')
+
+ self.test_tags = tags
+ super(UpdateTestRunner, self).run_tests(tests)
+
+ except Exception:
+ self.exc_info = sys.exc_info()
+ self.logger.error('Failure during execution of the update test.',
+ exc_info=self.exc_info)
+
+ finally:
+ self.test_tags = test_tags
+
+ self.logger.info('Removing copy of the application at "%s"' % application_folder)
+ try:
+ mozfile.remove(application_folder)
+ except IOError as e:
+ self.logger.error('Cannot remove copy of application: "%s"' % str(e))
+
+ # Run direct update tests if wanted
+ if self.run_direct_update:
+ _run_tests(tags=['direct'])
+ failed += self.failed
+ results['Direct'] = False if self.failed else True
+
+ # Run fallback update tests if wanted
+ if self.run_fallback_update:
+ _run_tests(tags=['fallback'])
+ failed += self.failed
+ results['Fallback'] = False if self.failed else True
+
+ self.logger.info("Summary of update tests:")
+ for test_type, result in results.iteritems():
+ self.logger.info("\t%s update test ran and %s" %
+ (test_type, 'PASSED' if result else 'FAILED'))
+
+ # Combine failed tests for all run_test() executions
+ self.failed = failed
+
+ # If exceptions happened, re-throw the last one
+ if self.exc_info:
+ ex_type, exception, tb = self.exc_info
+ raise ex_type, exception, tb
diff --git a/testing/firefox-ui/harness/firefox_ui_harness/testcases.py b/testing/firefox-ui/harness/firefox_ui_harness/testcases.py
new file mode 100644
index 000000000..abcbd6555
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/testcases.py
@@ -0,0 +1,420 @@
+# 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 os
+import pprint
+from datetime import datetime
+
+import mozfile
+
+from firefox_puppeteer import PuppeteerMixin
+from firefox_puppeteer.api.software_update import SoftwareUpdate
+from firefox_puppeteer.ui.update_wizard import UpdateWizardDialog
+from marionette_driver import Wait
+from marionette_driver.errors import NoSuchWindowException
+from marionette_harness import MarionetteTestCase
+
+
+class UpdateTestCase(PuppeteerMixin, MarionetteTestCase):
+
+ TIMEOUT_UPDATE_APPLY = 300
+ TIMEOUT_UPDATE_CHECK = 30
+ TIMEOUT_UPDATE_DOWNLOAD = 720
+
+ # For the old update wizard, the errors are displayed inside the dialog. For the
+ # handling of updates in the about window the errors are displayed in new dialogs.
+ # When the old wizard is open we have to set the preference, so the errors will be
+ # shown as expected, otherwise we would have unhandled modal dialogs when errors are
+ # raised. See:
+ # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4813
+ # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/update/nsUpdateService.js?rev=a9240b1eb2fb#4756
+ PREF_APP_UPDATE_ALTWINDOWTYPE = 'app.update.altwindowtype'
+
+ def __init__(self, *args, **kwargs):
+ super(UpdateTestCase, self).__init__(*args, **kwargs)
+
+ self.update_channel = kwargs.pop('update_channel')
+ self.update_mar_channels = set(kwargs.pop('update_mar_channels'))
+
+ self.target_buildid = kwargs.pop('update_target_buildid')
+ self.target_version = kwargs.pop('update_target_version')
+
+ def setUp(self, is_fallback=False):
+ super(UpdateTestCase, self).setUp()
+
+ self.software_update = SoftwareUpdate(self.marionette)
+ self.download_duration = None
+
+ # If a custom update channel has to be set, force a restart of
+ # Firefox to actually get it applied as a default pref. Use the clean
+ # option to force a non in_app restart, which would allow Firefox to
+ # dump the logs to the console.
+ if self.update_channel:
+ self.software_update.update_channel = self.update_channel
+ self.restart(clean=True)
+
+ self.assertEqual(self.software_update.update_channel, self.update_channel)
+
+ # If requested modify the list of allowed MAR channels
+ if self.update_mar_channels:
+ self.software_update.mar_channels.add_channels(self.update_mar_channels)
+
+ self.assertTrue(self.update_mar_channels.issubset(
+ self.software_update.mar_channels.channels),
+ 'Allowed MAR channels have been set: expected "{}" in "{}"'.format(
+ ', '.join(self.update_mar_channels),
+ ', '.join(self.software_update.mar_channels.channels)))
+
+ # Ensure that there exists no already partially downloaded update
+ self.remove_downloaded_update()
+
+ # Dictionary which holds the information for each update
+ self.update_status = {
+ 'build_pre': self.software_update.build_info,
+ 'build_post': None,
+ 'fallback': is_fallback,
+ 'patch': {},
+ 'success': False,
+ }
+
+ # Check if the user has permissions to run the update
+ self.assertTrue(self.software_update.allowed,
+ 'Current user has permissions to update the application.')
+
+ def tearDown(self):
+ try:
+ self.browser.tabbar.close_all_tabs([self.browser.tabbar.selected_tab])
+
+ # Add content of the update log file for detailed failures when applying an update
+ self.update_status['update_log'] = self.read_update_log()
+
+ # Print results for now until we have treeherder integration
+ output = pprint.pformat(self.update_status)
+ self.logger.info('Update test results: \n{}'.format(output))
+ finally:
+ super(UpdateTestCase, self).tearDown()
+
+ # Ensure that no trace of an partially downloaded update remain
+ self.remove_downloaded_update()
+
+ @property
+ def patch_info(self):
+ """ Returns information about the active update in the queue.
+
+ :returns: A dictionary with information about the active patch
+ """
+ patch = self.software_update.patch_info
+ patch['download_duration'] = self.download_duration
+
+ return patch
+
+ def check_for_updates(self, about_window, timeout=TIMEOUT_UPDATE_CHECK):
+ """Clicks on "Check for Updates" button, and waits for check to complete.
+
+ :param about_window: Instance of :class:`AboutWindow`.
+ :param timeout: How long to wait for the update check to finish. Optional,
+ defaults to 60s.
+
+ :returns: True, if an update is available.
+ """
+ self.assertEqual(about_window.deck.selected_panel,
+ about_window.deck.check_for_updates)
+
+ about_window.deck.check_for_updates.button.click()
+ Wait(self.marionette, timeout=self.TIMEOUT_UPDATE_CHECK).until(
+ lambda _: about_window.deck.selected_panel not in
+ (about_window.deck.check_for_updates, about_window.deck.checking_for_updates),
+ message='Check for updates has been finished.')
+
+ return about_window.deck.selected_panel != about_window.deck.no_updates_found
+
+ def check_update_applied(self):
+ """Check that the update has been applied correctly"""
+ self.update_status['build_post'] = self.software_update.build_info
+
+ # Ensure that the target version is the same or higher. No downgrade
+ # should have happened.
+ version_check = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ return Services.vc.compare(arguments[0], arguments[1]);
+ """, script_args=(self.update_status['build_post']['version'],
+ self.update_status['build_pre']['version']))
+
+ self.assertGreaterEqual(version_check, 0,
+ 'A downgrade from version {} to {} is not allowed'.format(
+ self.update_status['build_pre']['version'],
+ self.update_status['build_post']['version']))
+
+ self.assertNotEqual(self.update_status['build_post']['buildid'],
+ self.update_status['build_pre']['buildid'],
+ 'The staged update to buildid {} has not been applied'.format(
+ self.update_status['patch']['buildid']))
+
+ self.assertEqual(self.update_status['build_post']['buildid'],
+ self.update_status['patch']['buildid'],
+ 'Unexpected target buildid after applying the patch, {} != {}'.format(
+ self.update_status['build_post']['buildid'],
+ self.update_status['patch']['buildid']))
+
+ self.assertEqual(self.update_status['build_post']['locale'],
+ self.update_status['build_pre']['locale'],
+ 'Unexpected change of the locale from {} to {}'.format(
+ self.update_status['build_pre']['locale'],
+ self.update_status['build_post']['locale']))
+
+ self.assertEqual(self.update_status['build_post']['disabled_addons'],
+ self.update_status['build_pre']['disabled_addons'],
+ 'Application-wide addons have been unexpectedly disabled: {}'.format(
+ ', '.join(set(self.update_status['build_pre']['locale']) -
+ set(self.update_status['build_post']['locale']))
+ ))
+
+ if self.target_version:
+ self.assertEqual(self.update_status['build_post']['version'],
+ self.target_version,
+ 'Current target version {} does not match expected version {}'.format(
+ self.update_status['build_post']['version'], self.target_version))
+
+ if self.target_buildid:
+ self.assertEqual(self.update_status['build_post']['buildid'],
+ self.target_buildid,
+ 'Current target buildid {} does not match expected buildid {}'.format(
+ self.update_status['build_post']['buildid'], self.target_buildid))
+
+ self.update_status['success'] = True
+
+ def check_update_not_applied(self):
+ """Check that the update has not been applied due to a forced invalidation of the patch"""
+ build_info = self.software_update.build_info
+
+ # Ensure that the version has not been changed
+ version_check = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ return Services.vc.compare(arguments[0], arguments[1]);
+ """, script_args=(build_info['version'],
+ self.update_status['build_pre']['version']))
+
+ self.assertEqual(version_check, 0,
+ 'An update from version {} to {} has been unexpectedly applied'.format(
+ self.update_status['build_pre']['version'],
+ build_info['version']))
+
+ # Check that the build id of the source build and the current build are identical
+ self.assertEqual(build_info['buildid'],
+ self.update_status['build_pre']['buildid'],
+ 'The build id has been unexpectedly changed from {} to {}'.format(
+ self.update_status['build_pre']['buildid'], build_info['buildid']))
+
+ def download_update(self, window, wait_for_finish=True, timeout=TIMEOUT_UPDATE_DOWNLOAD):
+ """ Download the update patch.
+
+ :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`.
+ :param wait_for_finish: If True the function has to wait for the download to be finished.
+ Optional, default to `True`.
+ :param timeout: How long to wait for the download to finish. Optional, default to 360s.
+ """
+
+ def download_via_update_wizard(dialog):
+ """ Download the update via the old update wizard dialog.
+
+ :param dialog: Instance of :class:`UpdateWizardDialog`.
+ """
+ self.marionette.set_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE, dialog.window_type)
+
+ try:
+ # If updates have already been found, proceed to download
+ if dialog.wizard.selected_panel in [dialog.wizard.updates_found_basic,
+ dialog.wizard.error_patching,
+ ]:
+ dialog.select_next_page()
+
+ # If incompatible add-on are installed, skip over the wizard page
+ # TODO: Remove once we no longer support version Firefox 45.0ESR
+ if self.puppeteer.utils.compare_version(self.puppeteer.appinfo.version,
+ '49.0a1') == -1:
+ if dialog.wizard.selected_panel == dialog.wizard.incompatible_list:
+ dialog.select_next_page()
+
+ # Updates were stored in the cache, so no download is necessary
+ if dialog.wizard.selected_panel in [dialog.wizard.finished,
+ dialog.wizard.finished_background,
+ ]:
+ pass
+
+ # Download the update
+ elif dialog.wizard.selected_panel == dialog.wizard.downloading:
+ if wait_for_finish:
+ start_time = datetime.now()
+ self.wait_for_download_finished(dialog, timeout)
+ self.download_duration = (datetime.now() - start_time).total_seconds()
+
+ Wait(self.marionette).until(lambda _: (
+ dialog.wizard.selected_panel in [dialog.wizard.finished,
+ dialog.wizard.finished_background,
+ ]),
+ message='Final wizard page has been selected.')
+
+ else:
+ raise Exception('Invalid wizard page for downloading an update: {}'.format(
+ dialog.wizard.selected_panel))
+
+ finally:
+ self.marionette.clear_pref(self.PREF_APP_UPDATE_ALTWINDOWTYPE)
+
+ # The old update wizard dialog has to be handled differently. It's necessary
+ # for fallback updates and invalid add-on versions.
+ if isinstance(window, UpdateWizardDialog):
+ download_via_update_wizard(window)
+ return
+
+ if window.deck.selected_panel == window.deck.download_and_install:
+ window.deck.download_and_install.button.click()
+
+ # Wait for the download to start
+ Wait(self.marionette).until(lambda _: (
+ window.deck.selected_panel != window.deck.download_and_install),
+ message='Download of the update has been started.')
+
+ if wait_for_finish:
+ start_time = datetime.now()
+ self.wait_for_download_finished(window, timeout)
+ self.download_duration = (datetime.now() - start_time).total_seconds()
+
+ def download_and_apply_available_update(self, force_fallback=False):
+ """Checks, downloads, and applies an available update.
+
+ :param force_fallback: Optional, if `True` invalidate current update status.
+ Defaults to `False`.
+ """
+ # Open the about window and check for updates
+ about_window = self.browser.open_about_window()
+
+ try:
+ update_available = self.check_for_updates(about_window)
+ self.assertTrue(update_available,
+ "Available update has been found")
+
+ # Download update and wait until it has been applied
+ self.download_update(about_window)
+ self.wait_for_update_applied(about_window)
+
+ finally:
+ self.update_status['patch'] = self.patch_info
+
+ if force_fallback:
+ # Set the downloaded update into failed state
+ self.software_update.force_fallback()
+
+ # Restart Firefox to apply the downloaded update
+ self.restart()
+
+ def download_and_apply_forced_update(self):
+ self.check_update_not_applied()
+
+ # The update wizard dialog opens automatically after the restart but with a short delay
+ dialog = Wait(self.marionette, ignored_exceptions=[NoSuchWindowException]).until(
+ lambda _: self.puppeteer.windows.switch_to(lambda win: type(win) is UpdateWizardDialog)
+ )
+
+ # In case of a broken complete update the about window has to be used
+ if self.update_status['patch']['is_complete']:
+ about_window = None
+ try:
+ self.assertEqual(dialog.wizard.selected_panel,
+ dialog.wizard.error)
+ dialog.close()
+
+ # Open the about window and check for updates
+ about_window = self.browser.open_about_window()
+ update_available = self.check_for_updates(about_window)
+ self.assertTrue(update_available,
+ 'Available update has been found')
+
+ # Download update and wait until it has been applied
+ self.download_update(about_window)
+ self.wait_for_update_applied(about_window)
+
+ finally:
+ if about_window:
+ self.update_status['patch'] = self.patch_info
+
+ else:
+ try:
+ self.assertEqual(dialog.wizard.selected_panel,
+ dialog.wizard.error_patching)
+
+ # Start downloading the fallback update
+ self.download_update(dialog)
+
+ finally:
+ self.update_status['patch'] = self.patch_info
+
+ # Restart Firefox to apply the update
+ self.restart()
+
+ def read_update_log(self):
+ """Read the content of the update log file for the last update attempt."""
+ path = os.path.join(os.path.dirname(self.software_update.staging_directory),
+ 'last-update.log')
+ try:
+ with open(path, 'rb') as f:
+ return f.read().splitlines()
+ except IOError as exc:
+ self.logger.warning(str(exc))
+ return None
+
+ def remove_downloaded_update(self):
+ """Remove an already downloaded update from the update staging directory.
+
+ Hereby not only remove the update subdir but everything below 'updates'.
+ """
+ path = os.path.dirname(self.software_update.staging_directory)
+ self.logger.info('Clean-up update staging directory: {}'.format(path))
+ mozfile.remove(path)
+
+ def wait_for_download_finished(self, window, timeout=TIMEOUT_UPDATE_DOWNLOAD):
+ """ Waits until download is completed.
+
+ :param window: Instance of :class:`AboutWindow` or :class:`UpdateWizardDialog`.
+ :param timeout: How long to wait for the download to finish. Optional,
+ default to 360 seconds.
+ """
+ # The old update wizard dialog has to be handled differently. It's necessary
+ # for fallback updates and invalid add-on versions.
+ if isinstance(window, UpdateWizardDialog):
+ Wait(self.marionette, timeout=timeout).until(
+ lambda _: window.wizard.selected_panel != window.wizard.downloading,
+ message='Download has been completed.')
+
+ self.assertNotIn(window.wizard.selected_panel,
+ [window.wizard.error, window.wizard.error_extra])
+ return
+
+ Wait(self.marionette, timeout=timeout).until(
+ lambda _: window.deck.selected_panel not in
+ (window.deck.download_and_install, window.deck.downloading),
+ message='Download has been completed.')
+
+ self.assertNotEqual(window.deck.selected_panel,
+ window.deck.download_failed)
+
+ def wait_for_update_applied(self, about_window, timeout=TIMEOUT_UPDATE_APPLY):
+ """ Waits until the downloaded update has been applied.
+
+ :param about_window: Instance of :class:`AboutWindow`.
+ :param timeout: How long to wait for the update to apply. Optional,
+ default to 300 seconds
+ """
+ Wait(self.marionette, timeout=timeout).until(
+ lambda _: about_window.deck.selected_panel == about_window.deck.apply,
+ message='Final wizard page has been selected.')
+
+ # Wait for update to be staged because for update tests we modify the update
+ # status file to enforce the fallback update. If we modify the file before
+ # Firefox does, Firefox will override our change and we will have no fallback update.
+ Wait(self.marionette, timeout=timeout).until(
+ lambda _: 'applied' in self.software_update.active_update.state,
+ message='Update has been applied.')
diff --git a/testing/firefox-ui/harness/requirements.txt b/testing/firefox-ui/harness/requirements.txt
new file mode 100644
index 000000000..54114debb
--- /dev/null
+++ b/testing/firefox-ui/harness/requirements.txt
@@ -0,0 +1,5 @@
+firefox-puppeteer >= 52.1.0, <53.0.0
+marionette-harness >= 4.0.0
+mozfile >= 1.2
+mozinfo >= 0.8
+mozinstall >= 1.12
diff --git a/testing/firefox-ui/harness/setup.py b/testing/firefox-ui/harness/setup.py
new file mode 100644
index 000000000..1799d5057
--- /dev/null
+++ b/testing/firefox-ui/harness/setup.py
@@ -0,0 +1,44 @@
+# 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 os
+import re
+from setuptools import setup, find_packages
+
+THIS_DIR = os.path.dirname(os.path.realpath(__name__))
+
+
+def read(*parts):
+ with open(os.path.join(THIS_DIR, *parts)) as f:
+ return f.read()
+
+
+def get_version():
+ return re.findall("__version__ = '([\d\.]+)'",
+ read('firefox_ui_harness', '__init__.py'), re.M)[0]
+
+long_description = """Custom Marionette runner classes and entry scripts for Firefox Desktop
+specific Marionette tests.
+"""
+
+setup(name='firefox-ui-harness',
+ version=get_version(),
+ description="Firefox UI Harness",
+ long_description=long_description,
+ classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ keywords='mozilla',
+ author='Auto-tools',
+ author_email='tools-marionette@lists.mozilla.org',
+ url='https://wiki.mozilla.org/Auto-tools/Projects/Marionette/Harnesses/FirefoxUI',
+ license='MPL',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=read('requirements.txt').splitlines(),
+ entry_points="""
+ [console_scripts]
+ firefox-ui-functional = firefox_ui_harness.cli_functional:cli
+ firefox-ui-update = firefox_ui_harness.cli_update:cli
+ """,
+ )