new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/MANIFEST.in
@@ -0,0 +1,2 @@
+exclude MANIFEST.in
+include requirements.txt
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/__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_ui_harness.cli_functional import cli_functional
+from firefox_ui_harness.cli_update import cli_update
+
+
+__version__ = '1.0.0'
new file mode 100644
--- /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
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/arguments/base.py
@@ -0,0 +1,24 @@
+# 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 firefox_ui_tests
+
+from marionette import BaseMarionetteArguments
+
+
+class FirefoxUIBaseArguments(object):
+ name = 'Firefox UI Tests'
+ args = []
+
+ def parse_args_handler(self, args):
+ # If no tests are specified fall back to all firefox ui tests
+ args.tests = args.tests or [firefox_ui_tests.manifest_all]
+
+
+class FirefoxUIArguments(BaseMarionetteArguments):
+
+ def __init__(self, **kwargs):
+ BaseMarionetteArguments.__init__(self, **kwargs)
+
+ self.register_argument_container(FirefoxUIBaseArguments())
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/arguments/update.py
@@ -0,0 +1,64 @@
+# 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):
+ FirefoxUIArguments.__init__(self, **kwargs)
+
+ self.register_argument_container(UpdateBaseArguments())
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/cli_functional.py
@@ -0,0 +1,20 @@
+#!/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.runtests import cli
+
+from firefox_ui_harness.arguments import FirefoxUIArguments
+from firefox_ui_harness.runners import FirefoxUITestRunner
+
+
+def cli_functional():
+ cli(runner_class=FirefoxUITestRunner,
+ parser_class=FirefoxUIArguments,
+ )
+
+
+if __name__ == '__main__':
+ cli_functional()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/cli_update.py
@@ -0,0 +1,20 @@
+#!/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.runtests import cli
+
+from firefox_ui_harness.arguments import UpdateArguments
+from firefox_ui_harness.runners import UpdateTestRunner
+
+
+def cli_update():
+ cli(runner_class=UpdateTestRunner,
+ parser_class=UpdateArguments,
+ )
+
+
+if __name__ == '__main__':
+ cli_update()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/decorators.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/.
+
+from marionette import SkipTest
+import os
+
+
+def skip_under_xvfb(target):
+ def wrapper(self, *args, **kwargs):
+ if os.environ.get('MOZ_XVFB'):
+ raise SkipTest("Skipping due to running under xvfb")
+ return target(self, *args, **kwargs)
+ return wrapper
new file mode 100644
--- /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
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/runners/base.py
@@ -0,0 +1,47 @@
+# 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 import BaseMarionetteTestRunner
+
+import firefox_ui_tests
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class FirefoxUITestRunner(BaseMarionetteTestRunner):
+ def __init__(self, **kwargs):
+ BaseMarionetteTestRunner.__init__(self, **kwargs)
+ # select the appropriate GeckoInstance
+ self.app = 'fxdesktop'
+ if not self.server_root:
+ self.server_root = firefox_ui_tests.resources
+
+ self.test_handlers = [FirefoxTestCase]
+
+ 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)
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/firefox_ui_harness/runners/update.py
@@ -0,0 +1,94 @@
+# 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
+
+import firefox_ui_tests
+from firefox_puppeteer.testcases import UpdateTestCase
+from firefox_ui_harness.runners import FirefoxUITestRunner
+
+
+DEFAULT_PREFS = {
+ 'app.update.log': True,
+ 'startup.homepage_override_url': 'about:blank',
+}
+
+
+class UpdateTestRunner(FirefoxUITestRunner):
+
+ def __init__(self, **kwargs):
+ FirefoxUITestRunner.__init__(self, **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(manifest):
+ application_folder = None
+
+ try:
+ application_folder = self.duplicate_application(source_folder)
+ self.bin = mozinstall.get_binary(application_folder, 'Firefox')
+
+ FirefoxUITestRunner.run_tests(self, [manifest])
+
+ 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.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(manifest=firefox_ui_tests.manifest_update_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(manifest=firefox_ui_tests.manifest_update_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
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/harness/requirements.txt
@@ -0,0 +1,14 @@
+marionette-client >= 2.0.0
+mozfile >= 1.2
+mozinfo >= 0.8
+mozinstall >= 1.12
+
+# We make use of code of other packages, which should not be required by this package.
+# Make sure that we can kill those dependencies soon. We have to comment them all out
+# to avoid installations of those packages from pypi.
+
+# Necessary because of the testcase classes
+# firefox_puppeteer == 3.0.0
+
+# Necessary because of server-root, and manifest files
+# firefox_ui_tests
new file mode 100644
--- /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
+ """,
+ )
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/MANIFEST.in
@@ -0,0 +1,2 @@
+exclude MANIFEST.in
+recursive-include firefox_ui_tests *
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/__init__.py
@@ -0,0 +1,16 @@
+# 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
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+manifest_all = os.path.join(root, 'manifest.ini')
+
+manifest_functional = os.path.join(root, 'functional', 'manifest.ini')
+
+manifest_update_direct = os.path.join(root, 'update', 'direct', 'manifest.ini')
+manifest_update_fallback = os.path.join(root, 'update', 'fallback', 'manifest.ini')
+
+resources = os.path.join(root, 'resources')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/keyboard_shortcuts/manifest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+tags = local
+
+[test_browser_window.py]
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/keyboard_shortcuts/test_browser_window.py
@@ -0,0 +1,55 @@
+# 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 Wait
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestBrowserWindowShortcuts(FirefoxTestCase):
+
+ def test_addons_manager(self):
+ # If an about:xyz page is visible, no new tab will be opened
+ with self.marionette.using_context('content'):
+ self.marionette.navigate('about:')
+
+ # TODO: To be moved to the upcoming add-ons library
+ def opener(tab):
+ tab.window.send_shortcut(tab.window.get_entity('addons.commandkey'),
+ accel=True, shift=True)
+ self.browser.tabbar.open_tab(opener)
+
+ # TODO: Marionette currently fails to detect the correct tab
+ # with self.marionette.using_content('content'):
+ # self.wait_for_condition(lambda mn: mn.get_url() == "about:addons")
+
+ # TODO: remove extra switch once it is done automatically
+ self.browser.tabbar.tabs[1].switch_to()
+ self.browser.tabbar.close_tab()
+
+ def test_search_field(self):
+ current_name = self.marionette.execute_script("""
+ return window.document.activeElement.localName;
+ """)
+
+ # This doesn't test anything if we're already at input.
+ self.assertNotEqual(current_name, "input")
+
+ # TODO: To be moved to the upcoming search library
+ if self.platform == 'linux':
+ key = 'searchFocusUnix.commandkey'
+ else:
+ key = 'searchFocus.commandkey'
+ self.browser.send_shortcut(self.browser.get_entity(key), accel=True)
+
+ # TODO: Check that the right input box is focused
+ # Located below searchbar as class="autocomplete-textbox textbox-input"
+ # Anon locator has not been released yet (bug 1080764)
+ def has_input_selected(mn):
+ selection_name = mn.execute_script("""
+ return window.document.activeElement.localName;
+ """)
+ return selection_name == "input"
+
+ Wait(self.marionette).until(has_input_selected)
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/manifest.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = local
+
+[test_access_locationbar.py]
+disabled = Bug 1168727 - Timeout when opening auto-complete popup
+[test_escape_autocomplete.py]
+[test_favicon_in_autocomplete.py]
+[test_suggest_bookmarks.py]
+
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_access_locationbar.py
@@ -0,0 +1,62 @@
+# 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 Wait
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestAccessLocationBar(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ # Clear complete history so there's no interference from previous entries.
+ self.places.remove_all_history()
+
+ self.test_urls = [
+ 'layout/mozilla_projects.html',
+ 'layout/mozilla.html',
+ 'layout/mozilla_mission.html'
+ ]
+ self.test_urls = [self.marionette.absolute_url(t)
+ for t in self.test_urls]
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.autocomplete_results = self.locationbar.autocomplete_results
+ self.urlbar = self.locationbar.urlbar
+
+ @skip_under_xvfb
+ def test_access_locationbar_history(self):
+
+ # Open some local pages, then about:blank
+ def load_urls():
+ with self.marionette.using_context('content'):
+ for url in self.test_urls:
+ self.marionette.navigate(url)
+ self.places.wait_for_visited(self.test_urls, load_urls)
+ with self.marionette.using_context('content'):
+ self.marionette.navigate('about:blank')
+
+ # Need to blur url bar or autocomplete won't load - bug 1038614
+ self.marionette.execute_script("""arguments[0].blur();""", script_args=[self.urlbar])
+
+ # Clear contents of url bar to focus, then arrow down for list of visited sites
+ # Verify that autocomplete is open and results are displayed
+ self.locationbar.clear()
+ self.urlbar.send_keys(self.keys.ARROW_DOWN)
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.is_open)
+ Wait(self.marionette).until(lambda _: len(self.autocomplete_results.visible_results) > 1)
+
+ # Arrow down again to select first item in list, appearing in reversed order, as loaded.
+ # Verify first item.
+ self.urlbar.send_keys(self.keys.ARROW_DOWN)
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.selected_index == '0')
+ self.assertIn('mission', self.locationbar.value)
+
+ # Navigate to the currently selected url
+ # Verify it loads by comparing the page url to the test url
+ self.urlbar.send_keys(self.keys.ENTER)
+ self.assertEqual(self.locationbar.value, self.test_urls[-1])
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_escape_autocomplete.py
@@ -0,0 +1,57 @@
+# 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 Wait
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestEscapeAutocomplete(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ # Clear complete history so there's no interference from previous entries.
+ self.places.remove_all_history()
+
+ self.test_urls = [
+ 'layout/mozilla.html',
+ 'layout/mozilla_community.html',
+ ]
+ self.test_urls = [self.marionette.absolute_url(t)
+ for t in self.test_urls]
+
+ self.test_string = 'mozilla'
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.autocomplete_results = self.locationbar.autocomplete_results
+
+ def tearDown(self):
+ self.autocomplete_results.close(force=True)
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_escape_autocomplete(self):
+ # Open some local pages
+ def load_urls():
+ with self.marionette.using_context('content'):
+ for url in self.test_urls:
+ self.marionette.navigate(url)
+ self.places.wait_for_visited(self.test_urls, load_urls)
+
+ # Clear the location bar, type the test string, check that autocomplete list opens
+ self.locationbar.clear()
+ self.locationbar.urlbar.send_keys(self.test_string)
+ self.assertEqual(self.locationbar.value, self.test_string)
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.is_open)
+
+ # Press escape, check location bar value, check autocomplete list closed
+ self.locationbar.urlbar.send_keys(self.keys.ESCAPE)
+ self.assertEqual(self.locationbar.value, self.test_string)
+ Wait(self.marionette).until(lambda _: not self.autocomplete_results.is_open)
+
+ # Press escape again and check that locationbar returns to the page url
+ self.locationbar.urlbar.send_keys(self.keys.ESCAPE)
+ self.assertEqual(self.locationbar.value, self.test_urls[-1])
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_favicon_in_autocomplete.py
@@ -0,0 +1,60 @@
+# 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 Wait
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestFaviconInAutocomplete(FirefoxTestCase):
+
+ PREF_SUGGEST_SEARCHES = 'browser.urlbar.suggest.searches'
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ # Disable search suggestions to get results only for history and bookmarks
+ self.prefs.set_pref(self.PREF_SUGGEST_SEARCHES, False)
+
+ self.places.remove_all_history()
+
+ self.test_urls = [self.marionette.absolute_url('layout/mozilla.html')]
+
+ self.test_string = 'mozilla'
+ self.test_favicon = 'mozilla_favicon.ico'
+
+ self.autocomplete_results = self.browser.navbar.locationbar.autocomplete_results
+
+ def tearDown(self):
+ try:
+ self.autocomplete_results.close(force=True)
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_favicon_in_autocomplete(self):
+ # Open the test page
+ def load_urls():
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.test_urls[0])
+ self.places.wait_for_visited(self.test_urls, load_urls)
+
+ locationbar = self.browser.navbar.locationbar
+
+ # Clear the location bar, type the test string, check that autocomplete list opens
+ locationbar.clear()
+ locationbar.urlbar.send_keys(self.test_string)
+ self.assertEqual(locationbar.value, self.test_string)
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.is_complete)
+
+ result = self.autocomplete_results.visible_results[1]
+
+ result_icon = self.marionette.execute_script("""
+ return arguments[0].image;
+ """, script_args=[result])
+
+ self.assertIn(self.test_favicon, result_icon)
+
+ self.autocomplete_results.close()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_suggest_bookmarks.py
@@ -0,0 +1,88 @@
+# 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_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestStarInAutocomplete(FirefoxTestCase):
+ """ This replaces
+ http://hg.mozilla.org/qa/mozmill-tests/file/default/firefox/tests/functional/testAwesomeBar/testSuggestBookmarks.js
+ Check a star appears in autocomplete list for a bookmarked page.
+ """
+
+ PREF_SUGGEST_SEARCHES = 'browser.urlbar.suggest.searches'
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.test_urls = [self.marionette.absolute_url('layout/mozilla_grants.html')]
+
+ # Disable search suggestions to only get results for history and bookmarks
+ self.prefs.set_pref(self.PREF_SUGGEST_SEARCHES, False)
+
+ with self.marionette.using_context('content'):
+ self.marionette.navigate('about:blank')
+
+ self.places.remove_all_history()
+
+ def tearDown(self):
+ # Close the autocomplete results
+ try:
+ self.browser.navbar.locationbar.autocomplete_results.close()
+ self.places.restore_default_bookmarks()
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_star_in_autocomplete(self):
+ search_string = 'grants'
+
+ def visit_urls():
+ with self.marionette.using_context('content'):
+ for url in self.test_urls:
+ self.marionette.navigate(url)
+
+ # Navigate to all the urls specified in self.test_urls and wait for them to
+ # be registered as visited
+ self.places.wait_for_visited(self.test_urls, visit_urls)
+
+ # Bookmark the current page using the bookmark menu
+ self.browser.menubar.select_by_id('bookmarksMenu',
+ 'menu_bookmarkThisPage')
+
+ # TODO: Replace hard-coded selector with library method when one is available
+ done_button = self.marionette.find_element(By.ID, 'editBookmarkPanelDoneButton')
+ Wait(self.marionette).until(lambda mn: done_button.is_displayed)
+ done_button.click()
+
+ # We must open the blank page so the autocomplete result isn't "Switch to tab"
+ with self.marionette.using_context('content'):
+ self.marionette.navigate('about:blank')
+
+ self.places.remove_all_history()
+
+ # Focus the locationbar, delete any contents there, and type the search string
+ locationbar = self.browser.navbar.locationbar
+ locationbar.clear()
+ locationbar.urlbar.send_keys(search_string)
+ autocomplete_results = locationbar.autocomplete_results
+
+ # Wait for the search string to be present, for the autocomplete results to appear
+ # and for there to be exactly one autocomplete result
+ Wait(self.marionette).until(lambda mn: locationbar.value == search_string)
+ Wait(self.marionette).until(lambda mn: autocomplete_results.is_open)
+ Wait(self.marionette).until(lambda mn: len(autocomplete_results.visible_results) == 2)
+
+ # Compare the highlighted text in the autocomplete result to the search string
+ first_result = autocomplete_results.visible_results[1]
+ matching_titles = autocomplete_results.get_matching_text(first_result, 'title')
+ for title in matching_titles:
+ Wait(self.marionette).until(lambda mn: title.lower() == search_string)
+
+ self.assertIn('bookmark',
+ first_result.get_attribute('type'),
+ 'The auto-complete result is a bookmark')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/manifest.ini
@@ -0,0 +1,4 @@
+[include:keyboard_shortcuts/manifest.ini]
+[include:locationbar/manifest.ini]
+[include:private_browsing/manifest.ini]
+[include:security/manifest.ini]
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/private_browsing/manifest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+tags = local
+
+[test_about_private_browsing.py]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/private_browsing/test_about_private_browsing.py
@@ -0,0 +1,57 @@
+# 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.marionette_test import skip_if_e10s
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+from firefox_puppeteer.ui.browser.window import BrowserWindow
+
+
+class TestAboutPrivateBrowsing(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ # Use a fake local support URL
+ support_url = 'about:blank?'
+ self.prefs.set_pref('app.support.baseURL', support_url)
+
+ self.pb_url = support_url + 'private-browsing'
+
+ @skip_if_e10s
+ def testCheckAboutPrivateBrowsing(self):
+ self.assertFalse(self.browser.is_private)
+
+ with self.marionette.using_context('content'):
+ self.marionette.navigate('about:privatebrowsing')
+
+ status_node = self.marionette.find_element(By.CSS_SELECTOR, 'p.showNormal')
+ self.assertEqual(status_node.text,
+ self.browser.get_entity('aboutPrivateBrowsing.notPrivate'),
+ 'Status text indicates we are not in private browsing mode')
+
+ def window_opener(win):
+ with win.marionette.using_context('content'):
+ button = self.marionette.find_element(By.ID, 'startPrivateBrowsing')
+ button.click()
+
+ pb_window = self.browser.open_window(callback=window_opener,
+ expected_window_class=BrowserWindow)
+
+ try:
+ self.assertTrue(pb_window.is_private)
+
+ def tab_opener(tab):
+ with tab.marionette.using_context('content'):
+ link = tab.marionette.find_element(By.ID, 'learnMore')
+ link.click()
+
+ tab = pb_window.tabbar.open_tab(trigger=tab_opener)
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda _: tab.location == self.pb_url)
+
+ finally:
+ pb_window.close()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/manifest.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+tags = remote
+
+[test_dv_certificate.py]
+[test_enable_privilege.py]
+tags = local
+[test_ev_certificate.py]
+[test_mixed_content_page.py]
+[test_mixed_script_content_blocking.py]
+[test_no_certificate.py]
+tags = local
+[test_safe_browsing_notification.py]
+[test_safe_browsing_warning_pages.py]
+[test_security_notification.py]
+[test_ssl_disabled_error_page.py]
+[test_ssl_status_after_restart.py]
+skip-if = os == "win" # Bug 1167179: Fails to open popups after restart
+[test_submit_unencrypted_info_warning.py]
+[test_unknown_issuer.py]
+[test_untrusted_connection_error_page.py]
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_dv_certificate.py
@@ -0,0 +1,88 @@
+# 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 Wait
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestDVCertificate(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.browser.navbar.locationbar.identity_popup
+
+ self.url = 'https://ssl-dv.mozqa.com'
+
+ def tearDown(self):
+ try:
+ self.browser.switch_to()
+ self.identity_popup.close(force=True)
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_dv_cert(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ # The lock icon should be shown
+ self.assertIn('identity-secure',
+ self.locationbar.connection_icon.value_of_css_property('list-style-image'))
+
+ self.assertEqual(self.locationbar.identity_box.get_attribute('className'),
+ 'verifiedDomain')
+
+ # Open the identity popup
+ self.locationbar.open_identity_popup()
+
+ # Check the identity popup doorhanger
+ self.assertEqual(self.identity_popup.element.get_attribute('connection'), 'secure')
+
+ cert = self.browser.tabbar.selected_tab.certificate
+
+ # The shown host equals to the certificate
+ self.assertEqual(self.identity_popup.host.get_attribute('value'), cert['commonName'])
+
+ # Only the secure label is visible in the main view
+ secure_label = self.identity_popup.view.main.secure_connection_label
+ self.assertNotEqual(secure_label.value_of_css_property('display'), 'none')
+
+ insecure_label = self.identity_popup.view.main.insecure_connection_label
+ self.assertEqual(insecure_label.value_of_css_property('display'), 'none')
+
+ self.identity_popup.view.main.expander.click()
+ Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
+
+ # Only the secure label is visible in the security view
+ secure_label = self.identity_popup.view.security.secure_connection_label
+ self.assertNotEqual(secure_label.value_of_css_property('display'), 'none')
+
+ insecure_label = self.identity_popup.view.security.insecure_connection_label
+ self.assertEqual(insecure_label.value_of_css_property('display'), 'none')
+
+ verifier_label = self.browser.get_property('identity.identified.verifier')
+ self.assertEqual(self.identity_popup.view.security.verifier.get_attribute('textContent'),
+ verifier_label.replace("%S", cert['issuerOrganization']))
+
+ def opener(mn):
+ self.identity_popup.view.security.more_info_button.click()
+
+ page_info_window = self.browser.open_page_info_window(opener)
+ deck = page_info_window.deck
+
+ self.assertEqual(deck.selected_panel, deck.security)
+
+ self.assertEqual(deck.security.domain.get_attribute('value'),
+ cert['commonName'])
+
+ self.assertEqual(deck.security.owner.get_attribute('value'),
+ page_info_window.get_property('securityNoOwner'))
+
+ self.assertEqual(deck.security.verifier.get_attribute('value'),
+ cert['issuerOrganization'])
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_enable_privilege.py
@@ -0,0 +1,22 @@
+# 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.testcases import FirefoxTestCase
+
+
+class TestEnablePrivilege(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.url = self.marionette.absolute_url('security/enable_privilege.html')
+
+ def test_enable_privilege(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ result = self.marionette.find_element(By.ID, 'result')
+ self.assertEqual(result.get_attribute('textContent'), 'PASS')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_ev_certificate.py
@@ -0,0 +1,121 @@
+# 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 Wait
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestEVCertificate(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.locationbar.identity_popup
+
+ self.url = 'https://ssl-ev.mozqa.com/'
+
+ def tearDown(self):
+ try:
+ self.browser.switch_to()
+ self.identity_popup.close(force=True)
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_ev_certificate(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ # The lock icon should be shown
+ self.assertIn('identity-secure',
+ self.locationbar.connection_icon.value_of_css_property('list-style-image'))
+
+ # Check the identity box
+ self.assertEqual(self.locationbar.identity_box.get_attribute('className'),
+ 'verifiedIdentity')
+
+ # Get the information from the certificate
+ cert = self.browser.tabbar.selected_tab.certificate
+ address = self.security.get_address_from_certificate(cert)
+
+ # Check the identity popup label displays
+ self.assertEqual(self.locationbar.identity_organization_label.get_attribute('value'),
+ cert['organization'])
+ self.assertEqual(self.locationbar.identity_country_label.get_attribute('value'),
+ '(' + address['country'] + ')')
+
+ # Open the identity popup
+ self.locationbar.open_identity_popup()
+
+ # Check the idenity popup doorhanger
+ self.assertEqual(self.identity_popup.element.get_attribute('connection'), 'secure-ev')
+
+ # For EV certificates no hostname but the organization name is shown
+ self.assertEqual(self.identity_popup.host.get_attribute('value'),
+ cert['organization'])
+
+ # Only the secure label is visible in the main view
+ secure_label = self.identity_popup.view.main.secure_connection_label
+ self.assertNotEqual(secure_label.value_of_css_property('display'), 'none')
+
+ insecure_label = self.identity_popup.view.main.insecure_connection_label
+ self.assertEqual(insecure_label.value_of_css_property('display'), 'none')
+
+ self.identity_popup.view.main.expander.click()
+ Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
+
+ security_view = self.identity_popup.view.security
+
+ # Only the secure label is visible in the security view
+ secure_label = security_view.secure_connection_label
+ self.assertNotEqual(secure_label.value_of_css_property('display'), 'none')
+
+ insecure_label = security_view.insecure_connection_label
+ self.assertEqual(insecure_label.value_of_css_property('display'), 'none')
+
+ # Check the organization name
+ self.assertEqual(security_view.owner.get_attribute('textContent'),
+ cert['organization'])
+
+ # Check the owner location string
+ # More information:
+ # hg.mozilla.org/mozilla-central/file/eab4a81e4457/browser/base/content/browser.js#l7012
+ location = self.browser.get_property('identity.identified.state_and_country')
+ location = location.replace('%S', address['state'], 1).replace('%S', address['country'])
+ location = address['city'] + '\n' + location
+ self.assertEqual(security_view.owner_location.get_attribute('textContent'),
+ location)
+
+ # Check the verifier
+ l10n_verifier = self.browser.get_property('identity.identified.verifier')
+ l10n_verifier = l10n_verifier.replace('%S', cert['issuerOrganization'])
+ self.assertEqual(security_view.verifier.get_attribute('textContent'),
+ l10n_verifier)
+
+ # Open the Page Info window by clicking the More Information button
+ page_info = self.browser.open_page_info_window(
+ lambda _: self.identity_popup.view.security.more_info_button.click())
+
+ try:
+ # Verify that the current panel is the security panel
+ self.assertEqual(page_info.deck.selected_panel, page_info.deck.security)
+
+ # Verify the domain listed on the security panel
+ self.assertIn(cert['commonName'],
+ page_info.deck.security.domain.get_attribute('value'))
+
+ # Verify the owner listed on the security panel
+ self.assertEqual(page_info.deck.security.owner.get_attribute('value'),
+ cert['organization'])
+
+ # Verify the verifier listed on the security panel
+ self.assertEqual(page_info.deck.security.verifier.get_attribute('value'),
+ cert['issuerOrganization'])
+ finally:
+ page_info.close()
+ self.browser.focus()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_mixed_content_page.py
@@ -0,0 +1,58 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_driver import Wait
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestMixedContentPage(FirefoxTestCase):
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.locationbar.identity_popup
+
+ self.url = 'https://mozqa.com/data/firefox/security/mixedcontent.html'
+
+ def tearDown(self):
+ try:
+ self.identity_popup.close(force=True)
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_mixed_content(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ self.assertIn('identity-mixed-passive-loaded',
+ self.locationbar.connection_icon.value_of_css_property('list-style-image'))
+
+ # Open the identity popup
+ self.locationbar.open_identity_popup()
+
+ # Only the insecure label is visible in the main view
+ secure_label = self.identity_popup.view.main.secure_connection_label
+ self.assertEqual(secure_label.value_of_css_property('display'), 'none')
+
+ insecure_label = self.identity_popup.view.main.insecure_connection_label
+ self.assertNotEqual(insecure_label.value_of_css_property('display'), 'none')
+
+ # TODO: Bug 1177417 - Needs to open and close the security view, but a second
+ # click on the expander doesn't hide the security view
+ # self.identity_popup.view.main.expander.click()
+ # Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
+
+ # Only the insecure label is visible in the security view
+ secure_label = self.identity_popup.view.security.secure_connection_label
+ self.assertEqual(secure_label.value_of_css_property('display'), 'none')
+
+ insecure_label = self.identity_popup.view.security.insecure_connection_label
+ self.assertNotEqual(insecure_label.value_of_css_property('display'), 'none')
+
+ # owner is not visible
+ owner = self.identity_popup.view.security.owner
+ self.assertEqual(owner.value_of_css_property('display'), 'none')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_mixed_script_content_blocking.py
@@ -0,0 +1,90 @@
+# 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_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestMixedScriptContentBlocking(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.url = 'https://mozqa.com/data/firefox/security/mixed_content_blocked/index.html'
+
+ self.test_elements = [
+ ('result1', 'Insecure script one'),
+ ('result2', 'Insecure script from iFrame'),
+ ('result3', 'Insecure plugin'),
+ ('result4', 'Insecure stylesheet'),
+ ]
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.locationbar.identity_popup
+
+ def tearDown(self):
+ try:
+ self.identity_popup.close(force=True)
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def _expect_protection_status(self, enabled):
+ if enabled:
+ color, icon_filename, state = (
+ 'rgb(0, 136, 0)',
+ 'identity-mixed-active-blocked',
+ 'blocked'
+ )
+ else:
+ color, icon_filename, state = (
+ 'rgb(255, 0, 0)',
+ 'identity-mixed-active-loaded',
+ 'unblocked'
+ )
+
+ # First call to Wait() needs a longer timeout due to the reload of the web page.
+ connection_icon = self.locationbar.connection_icon
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda _: icon_filename in connection_icon.value_of_css_property('list-style-image'),
+ message="The correct icon is displayed"
+ )
+
+ with self.marionette.using_context('content'):
+ for identifier, description in self.test_elements:
+ el = self.marionette.find_element(By.ID, identifier)
+ Wait(self.marionette).until(
+ lambda mn: el.value_of_css_property('color') == color,
+ message=("%s has been %s" % (description, state))
+ )
+
+ def expect_protection_enabled(self):
+ self._expect_protection_status(True)
+
+ def expect_protection_disabled(self):
+ self._expect_protection_status(False)
+
+ @skip_under_xvfb
+ def test_mixed_content_page(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ self.expect_protection_enabled()
+
+ # Disable mixed content blocking via identity popup
+ self.locationbar.open_identity_popup()
+ self.identity_popup.view.main.expander.click()
+ Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
+
+ disable_button = self.identity_popup.view.security.disable_mixed_content_blocking_button
+ disable_button.click()
+
+ self.expect_protection_disabled()
+
+ # A reload keeps blocking disabled
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ self.expect_protection_disabled()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_no_certificate.py
@@ -0,0 +1,83 @@
+# 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 urlparse import urlparse
+
+from marionette_driver import expected, Wait
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestNoCertificate(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.locationbar.identity_popup
+
+ self.url = self.marionette.absolute_url('layout/mozilla.html')
+
+ def tearDown(self):
+ try:
+ self.browser.switch_to()
+ self.identity_popup.close(force=True)
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_no_certificate(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ # Check the favicon
+ # TODO: find a better way to check, e.g., mozmill's isDisplayed
+ favicon_hidden = self.marionette.execute_script("""
+ return arguments[0].hasAttribute("hidden");
+ """, script_args=[self.browser.navbar.locationbar.identity_icon])
+ self.assertFalse(favicon_hidden, 'The identity icon is visible')
+
+ # Check that the identity box organization label is blank
+ self.assertEqual(self.locationbar.identity_organization_label.get_attribute('value'), '',
+ 'The organization has no label')
+
+ # Open the identity popup
+ self.locationbar.open_identity_popup()
+
+ # Check the idenity popup doorhanger
+ self.assertEqual(self.identity_popup.element.get_attribute('connection'), 'not-secure')
+
+ # The expander for the security view does not exist
+ expected.element_not_present(lambda m: self.identity_popup.main.expander)
+
+ # Only the insecure label is visible
+ secure_label = self.identity_popup.view.main.secure_connection_label
+ self.assertEqual(secure_label.value_of_css_property('display'), 'none')
+
+ insecure_label = self.identity_popup.view.main.insecure_connection_label
+ self.assertNotEqual(insecure_label.value_of_css_property('display'), 'none')
+
+ self.identity_popup.view.main.expander.click()
+ Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
+
+ # Open the Page Info window by clicking the "More Information" button
+ page_info = self.browser.open_page_info_window(
+ lambda _: self.identity_popup.view.security.more_info_button.click())
+
+ # Verify that the current panel is the security panel
+ self.assertEqual(page_info.deck.selected_panel, page_info.deck.security)
+
+ # Check the domain listed on the security panel contains the url's host name
+ self.assertIn(urlparse(self.url).hostname,
+ page_info.deck.security.domain.get_attribute('value'))
+
+ # Check the owner label equals localized 'securityNoOwner'
+ self.assertEqual(page_info.deck.security.owner.get_attribute('value'),
+ page_info.get_property('securityNoOwner'))
+
+ # Check the verifier label equals localized 'notset'
+ self.assertEqual(page_info.deck.security.verifier.get_attribute('value'),
+ page_info.get_property('notset'))
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_safe_browsing_notification.py
@@ -0,0 +1,138 @@
+# 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 time
+
+from marionette_driver import By, expected, Wait
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestSafeBrowsingNotificationBar(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.test_data = [
+ # Unwanted software URL
+ {
+ # First two properties are not needed,
+ # since these errors are not reported
+ 'button_property': None,
+ 'report_page': None,
+ 'unsafe_page': 'https://www.itisatrap.org/firefox/unwanted.html'
+ },
+ # Phishing URL info
+ {
+ 'button_property': 'safebrowsing.notAForgeryButton.label',
+ 'report_page': 'www.google.com/safebrowsing/report_error',
+ 'unsafe_page': 'https://www.itisatrap.org/firefox/its-a-trap.html'
+ },
+ # Malware URL object
+ {
+ 'button_property': 'safebrowsing.notAnAttackButton.label',
+ 'report_page': 'www.stopbadware.org',
+ 'unsafe_page': 'https://www.itisatrap.org/firefox/its-an-attack.html'
+ }
+ ]
+
+ self.prefs.set_pref('browser.safebrowsing.enabled', True)
+ self.prefs.set_pref('browser.safebrowsing.malware.enabled', True)
+
+ # Give the browser a little time, because SafeBrowsing.jsm takes a while
+ # between start up and adding the example urls to the db.
+ # hg.mozilla.org/mozilla-central/file/46aebcd9481e/browser/base/content/browser.js#l1194
+ time.sleep(3)
+
+ # TODO: Bug 1139544: While we don't have a reliable way to close the safe browsing
+ # notification bar when a test fails, run this test in a new tab.
+ self.browser.tabbar.open_tab()
+
+ def tearDown(self):
+ try:
+ self.utils.remove_perms('https://www.itisatrap.org', 'safe-browsing')
+ self.browser.tabbar.close_all_tabs([self.browser.tabbar.tabs[0]])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_notification_bar(self):
+ with self.marionette.using_context('content'):
+ for item in self.test_data:
+ button_property = item['button_property']
+ report_page, unsafe_page = item['report_page'], item['unsafe_page']
+
+ # Navigate to the unsafe page
+ # Check "ignore warning" link then notification bar's "not badware" button
+ # Only do this if feature supports it
+ if button_property is not None:
+ self.marionette.navigate(unsafe_page)
+ # Wait for the DOM to receive events for about:blocked
+ time.sleep(1)
+ self.check_ignore_warning_button(unsafe_page)
+ self.check_not_badware_button(button_property, report_page)
+
+ # Return to the unsafe page
+ # Check "ignore warning" link then notification bar's "get me out" button
+ self.marionette.navigate(unsafe_page)
+ # Wait for the DOM to receive events for about:blocked
+ time.sleep(1)
+ self.check_ignore_warning_button(unsafe_page)
+ self.check_get_me_out_of_here_button()
+
+ # Return to the unsafe page
+ # Check "ignore warning" link then notification bar's "X" button
+ self.marionette.navigate(unsafe_page)
+ # Wait for the DOM to receive events for about:blocked
+ time.sleep(1)
+ self.check_ignore_warning_button(unsafe_page)
+ self.check_x_button()
+
+ def check_ignore_warning_button(self, unsafe_page):
+ button = self.marionette.find_element(By.ID, 'ignoreWarningButton')
+ button.click()
+
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ expected.element_present(By.ID, 'main-feature'))
+ self.assertEquals(self.marionette.get_url(), self.browser.get_final_url(unsafe_page))
+
+ # Clean up here since the permission gets set in this function
+ self.utils.remove_perms('https://www.itisatrap.org', 'safe-browsing')
+
+ # Check the not a forgery or attack button in the notification bar
+ def check_not_badware_button(self, button_property, report_page):
+ with self.marionette.using_context('chrome'):
+ # TODO: update to use safe browsing notification bar class when bug 1139544 lands
+ label = self.browser.get_property(button_property)
+ button = (self.marionette.find_element(By.ID, 'content')
+ .find_element('anon attribute', {'label': label}))
+
+ self.browser.tabbar.open_tab(lambda _: button.click())
+
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda mn: report_page in mn.get_url())
+ with self.marionette.using_context('chrome'):
+ self.browser.tabbar.close_tab()
+
+ def check_get_me_out_of_here_button(self):
+ with self.marionette.using_context('chrome'):
+ # TODO: update to use safe browsing notification bar class when bug 1139544 lands
+ label = self.browser.get_property('safebrowsing.getMeOutOfHereButton.label')
+ button = (self.marionette.find_element(By.ID, 'content')
+ .find_element('anon attribute', {'label': label}))
+ button.click()
+
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda mn: self.browser.default_homepage in mn.get_url())
+
+ def check_x_button(self):
+ with self.marionette.using_context('chrome'):
+ # TODO: update to use safe browsing notification bar class when bug 1139544 lands
+ button = (self.marionette.find_element(By.ID, 'content')
+ .find_element('anon attribute', {'value': 'blocked-badware-page'})
+ .find_element('anon attribute',
+ {'class': 'messageCloseButton close-icon tabbable'}))
+ button.click()
+
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ expected.element_stale(button))
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_safe_browsing_warning_pages.py
@@ -0,0 +1,109 @@
+# 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 time
+
+from marionette_driver import By, expected, Wait
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestSafeBrowsingWarningPages(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.urls = [
+ # Unwanted software URL
+ 'https://www.itisatrap.org/firefox/unwanted.html',
+ # Phishing URL
+ 'https://www.itisatrap.org/firefox/its-a-trap.html',
+ # Malware URL
+ 'https://www.itisatrap.org/firefox/its-an-attack.html'
+ ]
+
+ self.prefs.set_pref('browser.safebrowsing.enabled', True)
+ self.prefs.set_pref('browser.safebrowsing.malware.enabled', True)
+
+ # Give the browser a little time, because SafeBrowsing.jsm takes a
+ # while between start up and adding the example urls to the db.
+ # hg.mozilla.org/mozilla-central/file/46aebcd9481e/browser/base/content/browser.js#l1194
+ time.sleep(3)
+
+ # TODO: Bug 1139544: While we don't have a reliable way to close the safe browsing
+ # notification bar when a test fails, run this test in a new tab.
+ self.browser.tabbar.open_tab()
+
+ def tearDown(self):
+ try:
+ self.utils.remove_perms('https://www.itisatrap.org', 'safe-browsing')
+ self.browser.tabbar.close_all_tabs([self.browser.tabbar.tabs[0]])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_warning_pages(self):
+ with self.marionette.using_context("content"):
+ for unsafe_page in self.urls:
+ # Load a test page, then test the get me out button
+ self.marionette.navigate(unsafe_page)
+ # Wait for the DOM to receive events for about:blocked
+ time.sleep(1)
+ self.check_get_me_out_of_here_button(unsafe_page)
+
+ # Load the test page again, then test the report button
+ self.marionette.navigate(unsafe_page)
+ # Wait for the DOM to receive events for about:blocked
+ time.sleep(1)
+ self.check_report_button(unsafe_page)
+
+ # Load the test page again, then test the ignore warning button
+ self.marionette.navigate(unsafe_page)
+ # Wait for the DOM to receive events for about:blocked
+ time.sleep(1)
+ self.check_ignore_warning_button(unsafe_page)
+
+ def check_get_me_out_of_here_button(self, unsafe_page):
+ button = self.marionette.find_element(By.ID, "getMeOutButton")
+ button.click()
+
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda mn: self.browser.default_homepage in mn.get_url())
+
+ def check_report_button(self, unsafe_page):
+ # Get the URL of the support site for phishing and malware. This may result in a redirect.
+ with self.marionette.using_context('chrome'):
+ url = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ return Services.urlFormatter.formatURLPref("app.support.baseURL")
+ + "phishing-malware";
+ """)
+
+ button = self.marionette.find_element(By.ID, "reportButton")
+ button.click()
+
+ # Wait for the button to become stale, whereby a longer timeout is needed
+ # here to not fail in case of slow connections.
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ expected.element_stale(button))
+
+ # Wait for page load to be completed, so we can verify the URL even if a redirect happens.
+ # TODO: Bug 1140470: use replacement for mozmill's waitforPageLoad
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda mn: mn.execute_script('return document.readyState == "DOMContentLoaded" ||'
+ ' document.readyState == "complete";')
+ )
+
+ # check that our current url matches the final url we expect
+ self.assertEquals(self.marionette.get_url(), self.browser.get_final_url(url))
+
+ def check_ignore_warning_button(self, unsafe_page):
+ button = self.marionette.find_element(By.ID, 'ignoreWarningButton')
+ button.click()
+
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ expected.element_present(By.ID, 'main-feature'))
+ self.assertEquals(self.marionette.get_url(), self.browser.get_final_url(unsafe_page))
+
+ # Clean up by removing safe browsing permission for unsafe page
+ self.utils.remove_perms('https://www.itisatrap.org', 'safe-browsing')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_security_notification.py
@@ -0,0 +1,62 @@
+# 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 time
+
+from marionette_driver import By, Wait
+from marionette_driver.errors import MarionetteException
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestSecurityNotification(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.urls = [
+ # Invalid cert page
+ 'https://ssl-expired.mozqa.com',
+ # Secure page
+ 'https://ssl-ev.mozqa.com/',
+ # Insecure page
+ 'http://www.mozqa.com'
+ ]
+
+ self.identity_box = self.browser.navbar.locationbar.identity_box
+
+ def test_invalid_cert(self):
+ with self.marionette.using_context('content'):
+ # Go to a site that has an invalid (expired) cert
+ self.assertRaises(MarionetteException, self.marionette.navigate, self.urls[0])
+
+ # Wait for the DOM to receive events
+ time.sleep(1)
+
+ # Verify the text in Technical Content contains the page with invalid cert
+ text = self.marionette.find_element(By.ID, 'technicalContentText')
+ self.assertIn(self.urls[0][8:], text.get_attribute('textContent'))
+
+ # Verify the "Go Back" and "Advanced" buttons appear
+ self.assertIsNotNone(self.marionette.find_element(By.ID, 'returnButton'))
+ self.assertIsNotNone(self.marionette.find_element(By.ID, 'advancedButton'))
+
+ # Verify the error code is correct
+ self.assertIn('SEC_ERROR_EXPIRED_CERTIFICATE', text.get_attribute('textContent'))
+
+ def test_secure_website(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.urls[1])
+
+ Wait(self.marionette).until(lambda _: (
+ self.identity_box.get_attribute('className') == 'verifiedIdentity')
+ )
+
+ def test_insecure_website(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.urls[2])
+
+ Wait(self.marionette).until(lambda _: (
+ self.identity_box.get_attribute('className') == 'unknownIdentity')
+ )
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_ssl_disabled_error_page.py
@@ -0,0 +1,49 @@
+# 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 time
+
+from marionette_driver import By
+from marionette_driver.errors import MarionetteException
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestSSLDisabledErrorPage(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.url = 'https://tlsv1-0.mozqa.com'
+
+ self.utils.sanitize({"sessions": True})
+
+ # Disable SSL 3.0, TLS 1.0 and TLS 1.1 for secure connections
+ # by forcing the use of TLS 1.2
+ # see: http://kb.mozillazine.org/Security.tls.version.*#Possible_values_and_their_effects
+ self.prefs.set_pref('security.tls.version.min', 3)
+ self.prefs.set_pref('security.tls.version.max', 3)
+
+ def test_ssl_disabled_error_page(self):
+ with self.marionette.using_context('content'):
+ # Open the test page
+ self.assertRaises(MarionetteException, self.marionette.navigate, self.url)
+
+ # Wait for the DOM to receive events
+ time.sleep(1)
+
+ # Verify "Secure Connection Failed" error page title
+ title = self.marionette.find_element(By.ID, 'errorTitleText')
+ nss_failure2title = self.browser.get_entity('nssFailure2.title')
+ self.assertEquals(title.get_attribute('textContent'), nss_failure2title)
+
+ # Verify "Try Again" button appears
+ try_again_button = self.marionette.find_element(By.ID, 'errorTryAgain')
+ self.assertTrue(try_again_button.is_displayed())
+
+ # Verify the error message is correct
+ short_description = self.marionette.find_element(By.ID, 'errorShortDescText')
+ self.assertIn('SSL_ERROR_UNSUPPORTED_VERSION',
+ short_description.get_attribute('textContent'))
+ self.assertIn('mozqa.com', short_description.get_attribute('textContent'))
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_ssl_status_after_restart.py
@@ -0,0 +1,126 @@
+# 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 Wait
+from marionette.marionette_test import skip_if_e10s
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestSSLStatusAfterRestart(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.test_data = (
+ {
+ 'url': 'https://ssl-dv.mozqa.com',
+ 'identity': '',
+ 'type': 'secure'
+ },
+ {
+ 'url': 'https://ssl-ev.mozqa.com/',
+ 'identity': 'Mozilla Corporation',
+ 'type': 'secure-ev'
+ },
+ {
+ 'url': 'https://ssl-ov.mozqa.com/',
+ 'identity': '',
+ 'type': 'secure'
+ }
+ )
+
+ # Set browser to restore previous session
+ self.prefs.set_pref('browser.startup.page', 3)
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.locationbar.identity_popup
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ self.browser.tabbar.close_all_tabs([self.browser.tabbar.tabs[0]])
+ self.browser.switch_to()
+ self.identity_popup.close(force=True)
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_if_e10s
+ @skip_under_xvfb
+ def test_ssl_status_after_restart(self):
+ for item in self.test_data:
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(item['url'])
+ self.verify_certificate_status(item)
+ self.browser.tabbar.open_tab()
+
+ self.restart()
+
+ # Refresh references to elements
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.locationbar.identity_popup
+
+ for index, item in enumerate(self.test_data):
+ self.browser.tabbar.tabs[index].select()
+ self.verify_certificate_status(item)
+
+ def verify_certificate_status(self, item):
+ url, identity, cert_type = item['url'], item['identity'], item['type']
+
+ # Check the favicon
+ # TODO: find a better way to check, e.g., mozmill's isDisplayed
+ favicon_hidden = self.marionette.execute_script("""
+ return arguments[0].hasAttribute("hidden");
+ """, script_args=[self.browser.navbar.locationbar.identity_icon])
+ self.assertFalse(favicon_hidden)
+
+ self.locationbar.open_identity_popup()
+
+ # Check the type shown on the idenity popup doorhanger
+ self.assertEqual(self.identity_popup.element.get_attribute('connection'),
+ cert_type)
+
+ self.identity_popup.view.main.expander.click()
+ Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
+
+ # Check the identity label
+ self.assertEqual(self.locationbar.identity_organization_label.get_attribute('value'),
+ identity)
+
+ # Get the information from the certificate
+ cert = self.browser.tabbar.selected_tab.certificate
+
+ # Open the Page Info window by clicking the More Information button
+ page_info = self.browser.open_page_info_window(
+ lambda _: self.identity_popup.view.security.more_info_button.click())
+
+ # Verify that the current panel is the security panel
+ self.assertEqual(page_info.deck.selected_panel, page_info.deck.security)
+
+ # Verify the domain listed on the security panel
+ # If this is a wildcard cert, check only the domain
+ if cert['commonName'].startswith('*'):
+ self.assertIn(self.security.get_domain_from_common_name(cert['commonName']),
+ page_info.deck.security.domain.get_attribute('value'),
+ 'Expected domain found in certificate for ' + url)
+ else:
+ self.assertEqual(page_info.deck.security.domain.get_attribute('value'),
+ cert['commonName'],
+ 'Domain value matches certificate common name.')
+
+ # Verify the owner listed on the security panel
+ if identity != '':
+ owner = cert['organization']
+ else:
+ owner = page_info.get_property('securityNoOwner')
+
+ self.assertEqual(page_info.deck.security.owner.get_attribute('value'), owner,
+ 'Expected owner label found for ' + url)
+
+ # Verify the verifier listed on the security panel
+ self.assertEqual(page_info.deck.security.verifier.get_attribute('value'),
+ cert['issuerOrganization'],
+ 'Verifier matches issuer of certificate for ' + url)
+ page_info.close()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_submit_unencrypted_info_warning.py
@@ -0,0 +1,60 @@
+# 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, expected, Wait
+
+from marionette_driver.errors import NoAlertPresentException
+from marionette_driver.marionette import Alert
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestSubmitUnencryptedInfoWarning(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.url = 'https://ssl-dv.mozqa.com/data/firefox/security/unencryptedsearch.html'
+ self.test_string = 'mozilla'
+
+ self.prefs.set_pref('security.warn_submit_insecure', True)
+
+ def test_submit_unencrypted_info_warning(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ # Get the page's search box and submit button.
+ searchbox = self.marionette.find_element(By.ID, 'q')
+ button = self.marionette.find_element(By.ID, 'submit')
+
+ # Use the page's search box to submit information.
+ searchbox.send_keys(self.test_string)
+ button.click()
+
+ # Get the expected warning text and replace its two instances of "##" with "\n\n".
+ message = self.browser.get_property('formPostSecureToInsecureWarning.message')
+ message = message.replace('##', '\n\n')
+
+ # Wait for the warning, verify the expected text matches warning, accept the warning
+ warning = Alert(self.marionette)
+ try:
+ Wait(self.marionette,
+ ignored_exceptions=NoAlertPresentException,
+ timeout=self.browser.timeout_page_load).until(
+ lambda _: warning.text == message)
+ finally:
+ warning.accept()
+
+ # Wait for the search box to become stale, then wait for the page to be reloaded.
+ Wait(self.marionette).until(expected.element_stale(searchbox))
+
+ # TODO: Bug 1140470: use replacement for mozmill's waitforPageLoad
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda mn: mn.execute_script('return document.readyState == "DOMContentLoaded" ||'
+ ' document.readyState == "complete";')
+ )
+
+ # Check that search_term contains the test string.
+ search_term = self.marionette.find_element(By.ID, 'search-term')
+ self.assertEqual(search_term.get_attribute('textContent'), self.test_string)
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_unknown_issuer.py
@@ -0,0 +1,39 @@
+# 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 time
+
+from marionette_driver import By
+from marionette_driver.errors import MarionetteException
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestUnknownIssuer(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.url = 'https://ssl-unknownissuer.mozqa.com'
+
+ def test_unknown_issuer(self):
+ with self.marionette.using_context('content'):
+ # Go to a site that has a cert with an unknown issuer
+ self.assertRaises(MarionetteException, self.marionette.navigate, self.url)
+
+ # Wait for the DOM to receive events
+ time.sleep(1)
+
+ # Check the link in cert_domain_link
+ link = self.marionette.find_element(By.ID, 'cert_domain_link')
+ self.assertEquals(link.get_attribute('textContent'),
+ 'ssl-selfsigned-unknownissuer.mozqa.com')
+
+ # Verify the "Go Back" and "Advanced" buttons appear
+ self.assertIsNotNone(self.marionette.find_element(By.ID, 'returnButton'))
+ self.assertIsNotNone(self.marionette.find_element(By.ID, 'advancedButton'))
+
+ # Verify the error code is correct
+ text = self.marionette.find_element(By.ID, 'technicalContentText')
+ self.assertIn('SEC_ERROR_UNKNOWN_ISSUER', text.get_attribute('textContent'))
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_untrusted_connection_error_page.py
@@ -0,0 +1,34 @@
+# 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 time
+
+from marionette_driver import By, Wait
+from marionette_driver.errors import MarionetteException
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestUntrustedConnectionErrorPage(FirefoxTestCase):
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.url = 'https://ssl-selfsigned.mozqa.com'
+
+ def test_untrusted_connection_error_page(self):
+ self.marionette.set_context('content')
+
+ # In some localized builds, the default page redirects
+ target_url = self.browser.get_final_url(self.browser.default_homepage)
+
+ self.assertRaises(MarionetteException, self.marionette.navigate, self.url)
+
+ # Wait for the DOM to receive events
+ time.sleep(1)
+
+ button = self.marionette.find_element(By.ID, "returnButton")
+ button.click()
+
+ Wait(self.marionette, timeout=self.browser.timeout_page_load).until(
+ lambda mn: target_url == self.marionette.get_url())
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/manifest.ini
@@ -0,0 +1,2 @@
+[include:puppeteer/manifest.ini]
+[include:functional/manifest.ini]
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/manifest.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+tags = local
+
+# API tests
+[test_l10n.py]
+[test_places.py]
+[test_prefs.py]
+[test_security.py]
+tags = remote
+[test_software_update.py]
+[test_utils.py]
+
+# UI tests
+[test_about_window.py]
+[test_menubar.py]
+[test_page_info_window.py]
+[test_tabbar.py]
+[test_toolbars.py]
+tags = remote
+[test_windows.py]
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_about_window.py
@@ -0,0 +1,73 @@
+# 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.testcases import FirefoxTestCase
+
+
+class TestAboutWindow(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.about_window = self.browser.open_about_window()
+ self.deck = self.about_window.deck
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_basic(self):
+ self.assertEqual(self.about_window.window_type, 'Browser:About')
+
+ def test_elements(self):
+ """Test correct retrieval of elements."""
+ self.assertNotEqual(self.about_window.dtds, [])
+
+ self.assertEqual(self.deck.element.get_attribute('localName'), 'deck')
+
+ # apply panel
+ panel = self.deck.apply
+ self.assertEqual(panel.element.get_attribute('localName'), 'hbox')
+ self.assertEqual(panel.button.get_attribute('localName'), 'button')
+
+ # apply_billboard panel
+ panel = self.deck.apply_billboard
+ self.assertEqual(panel.element.get_attribute('localName'), 'hbox')
+ self.assertEqual(panel.button.get_attribute('localName'), 'button')
+
+ # check_for_updates panel
+ panel = self.deck.check_for_updates
+ self.assertEqual(panel.element.get_attribute('localName'), 'hbox')
+ self.assertEqual(panel.button.get_attribute('localName'), 'button')
+
+ # checking_for_updates panel
+ self.assertEqual(self.deck.checking_for_updates.element.get_attribute('localName'), 'hbox')
+
+ # download_and_install panel
+ panel = self.deck.download_and_install
+ self.assertEqual(panel.element.get_attribute('localName'), 'hbox')
+ self.assertEqual(panel.button.get_attribute('localName'), 'button')
+
+ # download_failed panel
+ self.assertEqual(self.deck.download_failed.element.get_attribute('localName'), 'hbox')
+
+ # downloading panel
+ self.assertEqual(self.deck.downloading.element.get_attribute('localName'), 'hbox')
+
+ def test_open_window(self):
+ """Test various opening strategies."""
+ def opener(win):
+ self.browser.menubar.select_by_id('helpMenu', 'aboutName')
+
+ open_strategies = ('menu',
+ opener,
+ )
+
+ self.about_window.close()
+ for trigger in open_strategies:
+ about_window = self.browser.open_about_window(trigger=trigger)
+ self.assertEquals(about_window, self.windows.current)
+ about_window.close()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_appinfo.py
@@ -0,0 +1,29 @@
+# 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 mozversion
+from marionette_driver.errors import MarionetteException
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestAppInfo(FirefoxTestCase):
+
+ def test_valid_properties(self):
+ binary = self.marionette.bin
+ version_info = mozversion.get_version(binary=binary)
+
+ self.assertEqual(self.appinfo.ID, version_info['application_id'])
+ self.assertEqual(self.appinfo.name, version_info['application_name'])
+ self.assertEqual(self.appinfo.vendor, version_info['application_vendor'])
+ self.assertEqual(self.appinfo.version, version_info['application_version'])
+ self.assertEqual(self.appinfo.platformBuildID, version_info['platform_buildid'])
+ self.assertEqual(self.appinfo.platformVersion, version_info['platform_version'])
+ self.assertIsNotNone(self.appinfo.locale)
+ self.assertIsNotNone(self.appinfo.user_agent)
+ self.assertIsNotNone(self.appinfo.XPCOMABI)
+
+ def test_invalid_properties(self):
+ with self.assertRaises(AttributeError):
+ self.appinfo.unknown
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_l10n.py
@@ -0,0 +1,51 @@
+# 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 MarionetteException
+
+from firefox_puppeteer.api.l10n import L10n
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestL10n(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+ self.l10n = L10n(lambda: self.marionette)
+
+ def tearDown(self):
+ FirefoxTestCase.tearDown(self)
+
+ def test_dtd_entity_chrome(self):
+ dtds = ['chrome://global/locale/filepicker.dtd',
+ 'chrome://browser/locale/baseMenuOverlay.dtd']
+
+ value = self.l10n.get_entity(dtds, 'helpSafeMode.label')
+ elm = self.marionette.find_element(By.ID, 'helpSafeMode')
+ self.assertEqual(value, elm.get_attribute('label'))
+
+ self.assertRaises(MarionetteException, self.l10n.get_entity, dtds, 'notExistent')
+
+ def test_dtd_entity_content(self):
+ dtds = ['chrome://global/locale/filepicker.dtd',
+ 'chrome://global/locale/aboutSupport.dtd']
+
+ value = self.l10n.get_entity(dtds, 'aboutSupport.pageTitle')
+
+ self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
+ self.marionette.navigate('about:support')
+
+ elm = self.marionette.find_element(By.TAG_NAME, 'title')
+ self.assertEqual(value, elm.text)
+
+ def test_properties(self):
+ properties = ['chrome://global/locale/filepicker.properties',
+ 'chrome://global/locale/findbar.properties']
+
+ # TODO: Find a way to verify the retrieved translated string
+ value = self.l10n.get_property(properties, 'NotFound')
+ self.assertNotEqual(value, '')
+
+ self.assertRaises(MarionetteException, self.l10n.get_property, properties, 'notExistent')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_menubar.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.errors import NoSuchElementException
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestMenuBar(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ def test_click_item_in_menubar(self):
+ num_tabs = len(self.browser.tabbar.tabs)
+
+ def opener(_):
+ self.browser.menubar.select_by_id('file-menu',
+ 'menu_newNavigatorTab')
+
+ self.browser.tabbar.open_tab(trigger=opener)
+
+ self.browser.tabbar.tabs[-1].close()
+
+ def test_click_non_existent_menu_and_item(self):
+ with self.assertRaises(NoSuchElementException):
+ self.browser.menubar.select_by_id('foobar-menu',
+ 'menu_newNavigatorTab')
+
+ with self.assertRaises(NoSuchElementException):
+ self.browser.menubar.select_by_id('file-menu', 'menu_foobar')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_page_info_window.py
@@ -0,0 +1,100 @@
+# 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.testcases import FirefoxTestCase
+
+
+class TestPageInfoWindow(FirefoxTestCase):
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_elements(self):
+ """Test correct retrieval of elements."""
+ page_info = self.browser.open_page_info_window()
+
+ self.assertNotEqual(page_info.dtds, [])
+ self.assertNotEqual(page_info.properties, [])
+
+ self.assertEqual(page_info.deck.element.get_attribute('localName'), 'deck')
+
+ # feed panel
+ self.assertEqual(page_info.deck.feed.element.get_attribute('localName'), 'vbox')
+
+ # general panel
+ self.assertEqual(page_info.deck.general.element.get_attribute('localName'), 'vbox')
+
+ # media panel
+ self.assertEqual(page_info.deck.media.element.get_attribute('localName'), 'vbox')
+
+ # permissions panel
+ self.assertEqual(page_info.deck.permissions.element.get_attribute('localName'), 'vbox')
+
+ # security panel
+ panel = page_info.deck.select(page_info.deck.security)
+
+ self.assertEqual(panel.element.get_attribute('localName'), 'vbox')
+
+ self.assertEqual(panel.domain.get_attribute('localName'), 'textbox')
+ self.assertEqual(panel.owner.get_attribute('localName'), 'textbox')
+ self.assertEqual(panel.verifier.get_attribute('localName'), 'textbox')
+
+ self.assertEqual(panel.view_certificate.get_attribute('localName'), 'button')
+ self.assertEqual(panel.view_cookies.get_attribute('localName'), 'button')
+ self.assertEqual(panel.view_passwords.get_attribute('localName'), 'button')
+
+ def test_select(self):
+ """Test properties and methods for switching between panels."""
+ page_info = self.browser.open_page_info_window()
+ deck = page_info.deck
+
+ self.assertEqual(deck.selected_panel, deck.general)
+
+ self.assertEqual(deck.select(deck.security), deck.security)
+ self.assertEqual(deck.selected_panel, deck.security)
+
+ def test_open_window(self):
+ """Test various opening strategies."""
+ def opener(win):
+ self.browser.menubar.select_by_id('tools-menu', 'menu_pageInfo')
+
+ open_strategies = ('menu',
+ 'shortcut',
+ opener,
+ )
+
+ for trigger in open_strategies:
+ if trigger == 'shortcut' and \
+ self.marionette.session_capabilities['platform'] == 'WINDOWS_NT':
+ # The shortcut for page info window does not exist on windows.
+ self.assertRaises(ValueError, self.browser.open_page_info_window,
+ trigger=trigger)
+ continue
+
+ page_info = self.browser.open_page_info_window(trigger=trigger)
+ self.assertEquals(page_info, self.windows.current)
+ page_info.close()
+
+ def test_close_window(self):
+ """Test various closing strategies."""
+ def closer(win):
+ win.send_shortcut(win.get_entity('closeWindow.key'), accel=True)
+
+ # Close a tab by each trigger method
+ close_strategies = ('menu',
+ 'shortcut',
+ closer,
+ )
+ for trigger in close_strategies:
+ # menu only works on OS X
+ if trigger == 'menu' and self.platform != 'Darwin':
+ continue
+
+ page_info = self.browser.open_page_info_window()
+ page_info.close(trigger=trigger)
+ self.assertTrue(page_info.closed)
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_places.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 marionette_driver import By, Wait
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestPlaces(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.urls = [self.marionette.absolute_url('layout/mozilla_governance.html'),
+ self.marionette.absolute_url('layout/mozilla_grants.html'),
+ ]
+
+ def tearDown(self):
+ try:
+ self.places.restore_default_bookmarks()
+ self.places.remove_all_history()
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def get_all_urls_in_history(self):
+ return self.marionette.execute_script("""
+ let hs = Components.classes["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Components.interfaces.nsINavHistoryService);
+ let urls = [];
+
+ let options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_URI;
+
+ let root = hs.executeQuery(hs.getNewQuery(), options).root
+ root.containerOpen = true;
+ for (let i = 0; i < root.childCount; i++) {
+ urls.push(root.getChild(i).uri)
+ }
+ root.containerOpen = false;
+
+ return urls;
+ """)
+
+ def test_plugins(self):
+ # TODO: Once we use a plugin, add a test case to verify that the data will be removed
+ self.places.clear_plugin_data()
+
+ def test_bookmarks(self):
+ star_button = self.marionette.find_element(By.ID, 'bookmarks-menu-button')
+
+ # Visit URLs and bookmark them all
+ for url in self.urls:
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(url)
+
+ Wait(self.marionette).until(lambda _: self.places.is_bookmark_star_button_ready())
+ star_button.click()
+ Wait(self.marionette).until(lambda _: self.places.is_bookmarked(url))
+
+ ids = self.places.get_folder_ids_for_url(url)
+ self.assertEqual(len(ids), 1)
+ self.assertEqual(ids[0], self.places.bookmark_folders.unfiled)
+
+ # Restore default bookmarks, so the added URLs are gone
+ self.places.restore_default_bookmarks()
+ for url in self.urls:
+ self.assertFalse(self.places.is_bookmarked(url))
+
+ def test_history(self):
+ self.assertEqual(len(self.get_all_urls_in_history()), 0)
+
+ # Visit pages and check that they are all present
+ def load_urls():
+ with self.marionette.using_context('content'):
+ for url in self.urls:
+ self.marionette.navigate(url)
+ self.places.wait_for_visited(self.urls, load_urls)
+
+ self.assertEqual(self.get_all_urls_in_history(), self.urls)
+
+ # Check that both pages are no longer in the remove_all_history
+ self.places.remove_all_history()
+ self.assertEqual(len(self.get_all_urls_in_history()), 0)
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_prefs.py
@@ -0,0 +1,156 @@
+# 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.testcases import FirefoxTestCase
+
+
+class testPreferences(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.new_pref = 'marionette.unittest.set_pref'
+ self.unknown_pref = 'marionette.unittest.unknown'
+
+ self.bool_pref = 'browser.tabs.loadBookmarksInBackground'
+ self.int_pref = 'browser.tabs.maxOpenBeforeWarn'
+ self.string_pref = 'browser.newtab.url'
+
+ def test_reset_pref(self):
+ self.prefs.set_pref(self.new_pref, 'unittest')
+ self.assertEqual(self.prefs.get_pref(self.new_pref), 'unittest')
+
+ # Preference gets removed
+ self.assertTrue(self.prefs.reset_pref(self.new_pref))
+ self.assertEqual(self.prefs.get_pref(self.new_pref), None)
+
+ # There is no such preference anymore
+ self.assertFalse(self.prefs.reset_pref(self.new_pref))
+
+ def test_get_pref(self):
+ # check correct types
+ self.assertTrue(isinstance(self.prefs.get_pref(self.bool_pref),
+ bool))
+ self.assertTrue(isinstance(self.prefs.get_pref(self.int_pref),
+ int))
+ self.assertTrue(isinstance(self.prefs.get_pref(self.string_pref),
+ basestring))
+
+ # unknown
+ self.assertIsNone(self.prefs.get_pref(self.unknown_pref))
+
+ # default branch
+ orig_value = self.prefs.get_pref(self.int_pref)
+ self.prefs.set_pref(self.int_pref, 99999)
+ self.assertEqual(self.prefs.get_pref(self.int_pref), 99999)
+ self.assertEqual(self.prefs.get_pref(self.int_pref, True), orig_value)
+
+ # complex value
+ properties_file = 'chrome://branding/locale/browserconfig.properties'
+ self.assertEqual(self.prefs.get_pref('browser.startup.homepage'),
+ properties_file)
+
+ value = self.prefs.get_pref('browser.startup.homepage',
+ interface='nsIPrefLocalizedString')
+ self.assertNotEqual(value, properties_file)
+
+ def test_restore_pref(self):
+ # test with single set_pref call and a new preference
+ self.prefs.set_pref(self.new_pref, True)
+ self.assertTrue(self.prefs.get_pref(self.new_pref))
+ self.prefs.restore_pref(self.new_pref)
+
+ orig_value = self.prefs.get_pref(self.string_pref)
+
+ # test with single set_pref call
+ self.prefs.set_pref(self.string_pref, 'unittest')
+ self.assertEqual(self.prefs.get_pref(self.string_pref), 'unittest')
+ self.prefs.restore_pref(self.string_pref)
+ self.assertEqual(self.prefs.get_pref(self.string_pref), orig_value)
+
+ # test with multiple set_pref calls
+ self.prefs.set_pref(self.string_pref, 'unittest1')
+ self.prefs.set_pref(self.string_pref, 'unittest2')
+ self.assertEqual(self.prefs.get_pref(self.string_pref), 'unittest2')
+ self.prefs.restore_pref(self.string_pref)
+ self.assertEqual(self.prefs.get_pref(self.string_pref), orig_value)
+
+ # test with multiple restore_pref calls
+ self.prefs.set_pref(self.string_pref, 'unittest3')
+ self.prefs.restore_pref(self.string_pref)
+ self.assertRaises(MarionetteException,
+ self.prefs.restore_pref, self.string_pref)
+
+ # test with an unknown pref
+ self.assertRaises(MarionetteException,
+ self.prefs.restore_pref, self.unknown_pref)
+
+ def test_restore_all_prefs(self):
+ orig_bool = self.prefs.get_pref(self.bool_pref)
+ orig_int = self.prefs.get_pref(self.int_pref)
+ orig_string = self.prefs.get_pref(self.string_pref)
+
+ self.prefs.set_pref(self.bool_pref, not orig_bool)
+ self.prefs.set_pref(self.int_pref, 99999)
+ self.prefs.set_pref(self.string_pref, 'unittest')
+
+ self.prefs.restore_all_prefs()
+ self.assertEqual(self.prefs.get_pref(self.bool_pref), orig_bool)
+ self.assertEqual(self.prefs.get_pref(self.int_pref), orig_int)
+ self.assertEqual(self.prefs.get_pref(self.string_pref), orig_string)
+
+ def test_set_pref_casted_values(self):
+ # basestring as boolean
+ self.prefs.set_pref(self.bool_pref, '')
+ self.assertFalse(self.prefs.get_pref(self.bool_pref))
+
+ self.prefs.set_pref(self.bool_pref, 'unittest')
+ self.assertTrue(self.prefs.get_pref(self.bool_pref))
+
+ # int as boolean
+ self.prefs.set_pref(self.bool_pref, 0)
+ self.assertFalse(self.prefs.get_pref(self.bool_pref))
+
+ self.prefs.set_pref(self.bool_pref, 5)
+ self.assertTrue(self.prefs.get_pref(self.bool_pref))
+
+ # boolean as int
+ self.prefs.set_pref(self.int_pref, False)
+ self.assertEqual(self.prefs.get_pref(self.int_pref), 0)
+
+ self.prefs.set_pref(self.int_pref, True)
+ self.assertEqual(self.prefs.get_pref(self.int_pref), 1)
+
+ # int as string
+ self.prefs.set_pref(self.string_pref, 54)
+ self.assertEqual(self.prefs.get_pref(self.string_pref), '54')
+
+ def test_set_pref_invalid(self):
+ self.assertRaises(AssertionError,
+ self.prefs.set_pref, self.new_pref, None)
+
+ def test_set_pref_new_preference(self):
+ self.prefs.set_pref(self.new_pref, True)
+ self.assertTrue(self.prefs.get_pref(self.new_pref))
+ self.prefs.restore_pref(self.new_pref)
+
+ self.prefs.set_pref(self.new_pref, 5)
+ self.assertEqual(self.prefs.get_pref(self.new_pref), 5)
+ self.prefs.restore_pref(self.new_pref)
+
+ self.prefs.set_pref(self.new_pref, 'test')
+ self.assertEqual(self.prefs.get_pref(self.new_pref), 'test')
+ self.prefs.restore_pref(self.new_pref)
+
+ def test_set_pref_new_values(self):
+ self.prefs.set_pref(self.bool_pref, True)
+ self.assertTrue(self.prefs.get_pref(self.bool_pref))
+
+ self.prefs.set_pref(self.int_pref, 99999)
+ self.assertEqual(self.prefs.get_pref(self.int_pref), 99999)
+
+ self.prefs.set_pref(self.string_pref, 'test_string')
+ self.assertEqual(self.prefs.get_pref(self.string_pref), 'test_string')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_security.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.testcases import FirefoxTestCase
+
+from firefox_puppeteer.errors import NoCertificateError
+
+
+class TestSecurity(FirefoxTestCase):
+
+ def test_get_address_from_certificate(self):
+ url = 'https://ssl-ev.mozqa.com'
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(url)
+
+ cert = self.browser.tabbar.tabs[0].certificate
+ self.assertIn(cert['commonName'], url)
+ self.assertEqual(cert['organization'], 'Mozilla Corporation')
+ self.assertEqual(cert['issuerOrganization'], 'DigiCert Inc')
+
+ address = self.security.get_address_from_certificate(cert)
+ self.assertIsNotNone(address)
+ self.assertIsNotNone(address['city'])
+ self.assertIsNotNone(address['country'])
+ self.assertIsNotNone(address['postal_code'])
+ self.assertIsNotNone(address['state'])
+ self.assertIsNotNone(address['street'])
+
+ def test_get_certificate(self):
+ url_http = self.marionette.absolute_url('layout/mozilla.html')
+ url_https = 'https://ssl-ev.mozqa.com'
+
+ # Test EV certificate
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(url_https)
+ cert = self.browser.tabbar.tabs[0].certificate
+ self.assertIn(cert['commonName'], url_https)
+
+ # HTTP connections do not have a SSL certificate
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(url_http)
+ with self.assertRaises(NoCertificateError):
+ self.browser.tabbar.tabs[0].certificate
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_software_update.py
@@ -0,0 +1,126 @@
+# 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
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+from firefox_puppeteer.api.software_update import SoftwareUpdate
+
+
+class TestSoftwareUpdate(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+ self.software_update = SoftwareUpdate(lambda: self.marionette)
+
+ self.saved_mar_channels = self.software_update.mar_channels.channels
+ self.software_update.mar_channels.channels = set(['expected', 'channels'])
+
+ def tearDown(self):
+ try:
+ self.software_update.mar_channels.channels = self.saved_mar_channels
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_abi(self):
+ self.assertTrue(self.software_update.ABI)
+
+ def test_allowed(self):
+ self.assertTrue(self.software_update.allowed)
+
+ def test_build_info(self):
+ build_info = self.software_update.build_info
+ self.assertEqual(build_info['disabled_addons'], None)
+ self.assertIn('Mozilla/', build_info['user_agent'])
+ self.assertEqual(build_info['mar_channels'], set(['expected', 'channels']))
+ self.assertTrue(build_info['version'])
+ self.assertTrue(build_info['buildid'].isdigit())
+ self.assertTrue(build_info['locale'])
+ self.assertIn('force=1', build_info['url_aus'])
+ self.assertEqual(build_info['channel'], self.software_update.update_channel.channel)
+
+ def test_force_fallback(self):
+ status_file = os.path.join(self.software_update.staging_directory, 'update.status')
+
+ try:
+ self.software_update.force_fallback()
+ with open(status_file, 'r') as f:
+ content = f.read()
+ self.assertEqual(content, 'failed: 6\n')
+ finally:
+ os.remove(status_file)
+
+ def test_get_update_url(self):
+ update_url = self.software_update.get_update_url()
+ self.assertIn('Firefox', update_url)
+ self.assertNotIn('force=1', update_url)
+ update_url = self.software_update.get_update_url(True)
+ self.assertIn('Firefox', update_url)
+ self.assertIn('force=1', update_url)
+
+ def test_os_version(self):
+ self.assertTrue(self.software_update.os_version)
+
+ def test_staging_directory(self):
+ self.assertTrue(self.software_update.staging_directory)
+
+
+class TestUpdateChannel(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+ self.software_update = SoftwareUpdate(lambda: self.marionette)
+
+ self.saved_channel = self.software_update.update_channel.default_channel
+ self.software_update.update_channel.default_channel = 'expected_channel'
+
+ def tearDown(self):
+ try:
+ self.software_update.update_channel.default_channel = self.saved_channel
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_update_channel_channel(self):
+ self.assertEqual(self.software_update.update_channel.channel, self.saved_channel)
+
+ def test_update_channel_default_channel(self):
+ self.assertEqual(self.software_update.update_channel.default_channel, 'expected_channel')
+
+ def test_update_channel_set_default_channel(self):
+ self.software_update.update_channel.default_channel = 'new_channel'
+ self.assertEqual(self.software_update.update_channel.default_channel, 'new_channel')
+
+
+class TestMARChannels(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+ self.software_update = SoftwareUpdate(lambda: self.marionette)
+
+ self.saved_mar_channels = self.software_update.mar_channels.channels
+ self.software_update.mar_channels.channels = set(['expected', 'channels'])
+
+ def tearDown(self):
+ try:
+ self.software_update.mar_channels.channels = self.saved_mar_channels
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_mar_channels_channels(self):
+ self.assertEqual(self.software_update.mar_channels.channels, set(['expected', 'channels']))
+
+ def test_mar_channels_set_channels(self):
+ self.software_update.mar_channels.channels = set(['a', 'b', 'c'])
+ self.assertEqual(self.software_update.mar_channels.channels, set(['a', 'b', 'c']))
+
+ def test_mar_channels_add_channels(self):
+ self.software_update.mar_channels.add_channels(set(['some', 'new', 'channels']))
+ self.assertEqual(
+ self.software_update.mar_channels.channels,
+ set(['expected', 'channels', 'some', 'new']))
+
+ def test_mar_channels_remove_channels(self):
+ self.software_update.mar_channels.remove_channels(set(['expected']))
+ self.assertEqual(self.software_update.mar_channels.channels, set(['channels']))
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_tabbar.py
@@ -0,0 +1,191 @@
+# 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.testcases import FirefoxTestCase
+
+from firefox_puppeteer.errors import NoCertificateError
+
+
+class TestTabBar(FirefoxTestCase):
+
+ def tearDown(self):
+ try:
+ self.browser.tabbar.close_all_tabs([self.browser.tabbar.tabs[0]])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_basics(self):
+ tabbar = self.browser.tabbar
+
+ self.assertEqual(tabbar.window, self.browser)
+
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+
+ self.assertEqual(tabbar.newtab_button.get_attribute('localName'), 'toolbarbutton')
+ self.assertEqual(tabbar.toolbar.get_attribute('localName'), 'tabs')
+
+ def test_open_close(self):
+ tabbar = self.browser.tabbar
+
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(tabbar.selected_index, 0)
+
+ # Open with default trigger, and force closing the tab
+ tabbar.open_tab()
+ tabbar.close_tab(force=True)
+
+ # Open a new tab by each trigger method
+ open_strategies = ('button',
+ 'menu',
+ 'shortcut',
+ lambda tab: tabbar.newtab_button.click()
+ )
+ for trigger in open_strategies:
+ new_tab = tabbar.open_tab(trigger=trigger)
+ self.assertEqual(len(tabbar.tabs), 2)
+ self.assertEqual(new_tab.handle, self.marionette.current_window_handle)
+ self.assertEqual(new_tab.handle, tabbar.tabs[1].handle)
+
+ tabbar.close_tab()
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+ self.assertNotEqual(new_tab.handle, tabbar.tabs[0].handle)
+
+ # Close a tab by each trigger method
+ close_strategies = ('button',
+ 'menu',
+ 'shortcut',
+ lambda tab: tab.close_button.click())
+ for trigger in close_strategies:
+ new_tab = tabbar.open_tab()
+ self.assertEqual(len(tabbar.tabs), 2)
+ self.assertEqual(new_tab.handle, self.marionette.current_window_handle)
+ self.assertEqual(new_tab.handle, tabbar.tabs[1].handle)
+
+ tabbar.close_tab(trigger=trigger)
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+ self.assertNotEqual(new_tab.handle, tabbar.tabs[0].handle)
+
+ def test_close_not_selected_tab(self):
+ tabbar = self.browser.tabbar
+
+ new_tab = tabbar.open_tab()
+ tabbar.close_tab(tabbar.tabs[0])
+
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(new_tab, tabbar.tabs[0])
+
+ def test_close_all_tabs_except_first(self):
+ tabbar = self.browser.tabbar
+
+ orig_tab = tabbar.tabs[0]
+
+ for i in range(0, 3):
+ tabbar.open_tab()
+
+ tabbar.close_all_tabs([orig_tab])
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(orig_tab.handle, self.marionette.current_window_handle)
+
+ def test_switch_to(self):
+ tabbar = self.browser.tabbar
+
+ # Open a new tab in the foreground (will be auto-selected)
+ new_tab = tabbar.open_tab()
+ self.assertEqual(new_tab.handle, self.marionette.current_window_handle)
+ self.assertEqual(tabbar.selected_index, 1)
+ self.assertEqual(tabbar.selected_tab, new_tab)
+
+ # Switch by index
+ tabbar.switch_to(0)
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+
+ # Switch by tab
+ tabbar.switch_to(new_tab)
+ self.assertEqual(new_tab.handle, self.marionette.current_window_handle)
+
+ # Switch by callback
+ tabbar.switch_to(lambda tab: tab.window.tabbar.selected_tab != tab)
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+
+ tabbar.close_tab(tabbar.tabs[1])
+
+
+class TestTab(FirefoxTestCase):
+
+ def tearDown(self):
+ try:
+ self.browser.tabbar.close_all_tabs([self.browser.tabbar.tabs[0]])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_basic(self):
+ tab = self.browser.tabbar.tabs[0]
+
+ self.assertEqual(tab.window, self.browser)
+
+ self.assertEqual(tab.tab_element.get_attribute('localName'), 'tab')
+ self.assertEqual(tab.close_button.get_attribute('localName'), 'toolbarbutton')
+
+ def test_certificate(self):
+ url = self.marionette.absolute_url('layout/mozilla.html')
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(url)
+ with self.assertRaises(NoCertificateError):
+ self.browser.tabbar.tabs[0].certificate
+
+ def test_close(self):
+ tabbar = self.browser.tabbar
+
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(tabbar.selected_index, 0)
+
+ # Force closing the tab
+ new_tab = tabbar.open_tab()
+ new_tab.close(force=True)
+
+ # Close a tab by each trigger method
+ close_strategies = ('button',
+ 'menu',
+ 'shortcut',
+ lambda tab: tab.close_button.click())
+ for trigger in close_strategies:
+ new_tab = tabbar.open_tab()
+ self.assertEqual(len(tabbar.tabs), 2)
+ self.assertEqual(new_tab.handle, self.marionette.current_window_handle)
+ self.assertEqual(new_tab.handle, tabbar.tabs[1].handle)
+
+ new_tab.close(trigger=trigger)
+ self.assertEqual(len(tabbar.tabs), 1)
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+ self.assertNotEqual(new_tab.handle, tabbar.tabs[0].handle)
+
+ def test_location(self):
+ url = self.marionette.absolute_url('layout/mozilla.html')
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(url)
+ self.assertEqual(self.browser.tabbar.tabs[0].location, url)
+
+ def test_switch_to(self):
+ tabbar = self.browser.tabbar
+
+ new_tab = tabbar.open_tab()
+
+ # Switch to the first tab, which will not select it
+ tabbar.tabs[0].switch_to()
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+ # Bug 1128656: We cannot test as long as switch_to_window() auto-selects the tab
+ # self.assertEqual(tabbar.selected_index, 1)
+ # self.assertEqual(tabbar.selected_tab, new_tab)
+
+ # Now select the first tab
+ tabbar.tabs[0].select()
+ self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
+ self.assertTrue(tabbar.tabs[0].selected)
+ self.assertFalse(tabbar.tabs[1].selected)
+
+ new_tab.close()
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_toolbars.py
@@ -0,0 +1,287 @@
+# 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 expected, By, Wait
+from marionette_driver.errors import NoSuchElementException
+
+from firefox_ui_harness.decorators import skip_under_xvfb
+from firefox_puppeteer.testcases import FirefoxTestCase
+
+
+class TestNavBar(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.navbar = self.browser.navbar
+ self.url = self.marionette.absolute_url('layout/mozilla.html')
+
+ with self.marionette.using_context('content'):
+ self.marionette.navigate('about:blank')
+
+ # TODO: check why self.places.remove_all_history() does not work here
+ self.marionette.execute_script("""
+ let count = gBrowser.sessionHistory.count;
+ gBrowser.sessionHistory.PurgeHistory(count);
+ """)
+
+ def test_elements(self):
+ self.assertEqual(self.navbar.back_button.get_attribute('localName'), 'toolbarbutton')
+ self.assertEqual(self.navbar.forward_button.get_attribute('localName'), 'toolbarbutton')
+ self.assertEqual(self.navbar.home_button.get_attribute('localName'), 'toolbarbutton')
+ self.assertEqual(self.navbar.menu_button.get_attribute('localName'), 'toolbarbutton')
+ self.assertEqual(self.navbar.toolbar.get_attribute('localName'), 'toolbar')
+
+ def test_buttons(self):
+ self.marionette.set_context('content')
+
+ # Load initial web page
+ self.marionette.navigate(self.url)
+ Wait(self.marionette).until(expected.element_present(lambda m:
+ m.find_element(By.ID, 'mozilla_logo')))
+
+ with self.marionette.using_context('chrome'):
+ # Both buttons are disabled
+ self.assertFalse(self.navbar.back_button.is_enabled())
+ self.assertFalse(self.navbar.forward_button.is_enabled())
+
+ # Go to the homepage
+ self.navbar.home_button.click()
+
+ Wait(self.marionette).until(expected.element_not_present(lambda m:
+ m.find_element(By.ID, 'mozilla_logo')))
+ self.assertEqual(self.marionette.get_url(), self.browser.default_homepage)
+
+ with self.marionette.using_context('chrome'):
+ # Only back button is enabled
+ self.assertTrue(self.navbar.back_button.is_enabled())
+ self.assertFalse(self.navbar.forward_button.is_enabled())
+
+ # Navigate back
+ self.navbar.back_button.click()
+
+ Wait(self.marionette).until(expected.element_present(lambda m:
+ m.find_element(By.ID, 'mozilla_logo')))
+ self.assertEqual(self.marionette.get_url(), self.url)
+
+ with self.marionette.using_context('chrome'):
+ # Only forward button is enabled
+ self.assertFalse(self.navbar.back_button.is_enabled())
+ self.assertTrue(self.navbar.forward_button.is_enabled())
+
+ # Navigate forward
+ self.navbar.forward_button.click()
+
+ Wait(self.marionette).until(expected.element_not_present(lambda m:
+ m.find_element(By.ID, 'mozilla_logo')))
+ self.assertEqual(self.marionette.get_url(), self.browser.default_homepage)
+
+
+class TestLocationBar(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.locationbar = self.browser.navbar.locationbar
+
+ def test_elements(self):
+ self.assertEqual(self.locationbar.urlbar.get_attribute('localName'), 'textbox')
+ self.assertIn('urlbar-input', self.locationbar.urlbar_input.get_attribute('className'))
+
+ self.assertEqual(self.locationbar.connection_icon.get_attribute('localName'), 'image')
+ self.assertEqual(self.locationbar.identity_box.get_attribute('localName'), 'box')
+ self.assertEqual(self.locationbar.identity_country_label.get_attribute('localName'),
+ 'label')
+ self.assertEqual(self.locationbar.identity_organization_label.get_attribute('localName'),
+ 'label')
+ self.assertEqual(self.locationbar.identity_icon.get_attribute('localName'), 'image')
+ self.assertEqual(self.locationbar.history_drop_marker.get_attribute('localName'),
+ 'dropmarker')
+ self.assertEqual(self.locationbar.reload_button.get_attribute('localName'),
+ 'toolbarbutton')
+ self.assertEqual(self.locationbar.stop_button.get_attribute('localName'),
+ 'toolbarbutton')
+
+ self.assertEqual(self.locationbar.contextmenu.get_attribute('localName'), 'menupopup')
+ self.assertEqual(self.locationbar.get_contextmenu_entry('paste').get_attribute('cmd'),
+ 'cmd_paste')
+
+ def test_reload(self):
+ event_types = ["shortcut", "shortcut2", "button"]
+ for event in event_types:
+ # TODO: Until we have waitForPageLoad, this only tests API
+ # compatibility.
+ self.locationbar.reload_url(event, force=True)
+ self.locationbar.reload_url(event, force=False)
+
+ def test_focus_and_clear(self):
+ self.locationbar.urlbar.send_keys("zyx")
+ self.locationbar.clear()
+ self.assertEqual(self.locationbar.value, '')
+
+ self.locationbar.urlbar.send_keys("zyx")
+ self.assertEqual(self.locationbar.value, 'zyx')
+
+ self.locationbar.clear()
+ self.assertEqual(self.locationbar.value, '')
+
+ def test_load_url(self):
+ data_uri = 'data:text/html,<title>Title</title>'
+ self.locationbar.load_url(data_uri)
+
+ with self.marionette.using_context('content'):
+ Wait(self.marionette).until(lambda mn: mn.get_url() == data_uri)
+
+
+class TestAutoCompleteResults(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+ self.browser.navbar.locationbar.clear()
+
+ self.autocomplete_results = self.browser.navbar.locationbar.autocomplete_results
+
+ def tearDown(self):
+ try:
+ self.autocomplete_results.close(force=True)
+ except NoSuchElementException:
+ # TODO: A NoSuchElementException is thrown here when tests accessing the
+ # autocomplete_results element are skipped.
+ pass
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_popup_elements(self):
+ # TODO: This test is not very robust because it relies on the history
+ # in the default profile.
+ self.assertFalse(self.autocomplete_results.is_open)
+ self.browser.navbar.locationbar.urlbar.send_keys('a')
+ results = self.autocomplete_results.results
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.is_complete)
+ visible_result_count = len(self.autocomplete_results.visible_results)
+ self.assertTrue(visible_result_count > 0)
+ self.assertEqual(visible_result_count,
+ int(results.get_attribute('itemCount')))
+
+ @skip_under_xvfb
+ def test_close(self):
+ self.browser.navbar.locationbar.urlbar.send_keys('a')
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.is_open)
+ # The Wait in the library implementation will fail this if this doesn't
+ # end up closing.
+ self.autocomplete_results.close()
+
+ @skip_under_xvfb
+ def test_force_close(self):
+ self.browser.navbar.locationbar.urlbar.send_keys('a')
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.is_open)
+ # The Wait in the library implementation will fail this if this doesn't
+ # end up closing.
+ self.autocomplete_results.close(force=True)
+
+ @skip_under_xvfb
+ def test_matching_text(self):
+ # The default profile always has links to mozilla.org. So multiple results
+ # will be found with 'moz'.
+ input_text = 'moz'
+
+ self.browser.navbar.locationbar.urlbar.send_keys(input_text)
+ Wait(self.marionette).until(lambda _: self.autocomplete_results.is_complete)
+ visible_results = self.autocomplete_results.visible_results
+ self.assertTrue(len(visible_results) > 0)
+
+ for result in visible_results:
+ # check matching text only for results of type bookmark
+ if result.get_attribute('type') != 'bookmark':
+ continue
+ title_matches = self.autocomplete_results.get_matching_text(result, "title")
+ url_matches = self.autocomplete_results.get_matching_text(result, "url")
+ all_matches = title_matches + url_matches
+ self.assertTrue(len(all_matches) > 0)
+ for match_fragment in all_matches:
+ self.assertIn(match_fragment.lower(), input_text)
+
+
+class TestIdentityPopup(FirefoxTestCase):
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ self.locationbar = self.browser.navbar.locationbar
+ self.identity_popup = self.locationbar.identity_popup
+
+ self.url = 'https://ssl-ev.mozqa.com'
+
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ def tearDown(self):
+ try:
+ self.identity_popup.close(force=True)
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ @skip_under_xvfb
+ def test_elements(self):
+ self.locationbar.open_identity_popup()
+
+ self.assertEqual(self.identity_popup.host.get_attribute('localName'), 'broadcaster')
+
+ # Test main view elements
+ main = self.identity_popup.view.main
+ self.assertEqual(main.element.get_attribute('localName'), 'panelview')
+
+ self.assertEqual(main.expander.get_attribute('localName'), 'button')
+ self.assertEqual(main.insecure_connection_label.get_attribute('localName'),
+ 'description')
+ self.assertEqual(main.internal_connection_label.get_attribute('localName'),
+ 'description')
+ self.assertEqual(main.secure_connection_label.get_attribute('localName'),
+ 'description')
+
+ self.assertEqual(main.permissions.get_attribute('localName'), 'vbox')
+
+ # Test security view elements
+ security = self.identity_popup.view.security
+ self.assertEqual(security.element.get_attribute('localName'), 'panelview')
+
+ self.assertEqual(security.insecure_connection_label.get_attribute('localName'),
+ 'description')
+ self.assertEqual(security.secure_connection_label.get_attribute('localName'),
+ 'description')
+
+ self.assertEqual(security.owner.get_attribute('localName'), 'description')
+ self.assertEqual(security.owner_location.get_attribute('localName'), 'description')
+ self.assertEqual(security.verifier.get_attribute('localName'), 'description')
+
+ self.assertEqual(security.disable_mixed_content_blocking_button.get_attribute('localName'),
+ 'button')
+ self.assertEqual(security.enable_mixed_content_blocking_button.get_attribute('localName'),
+ 'button')
+
+ self.assertEqual(security.more_info_button.get_attribute('localName'), 'button')
+
+ @skip_under_xvfb
+ def test_open_close(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ self.assertFalse(self.identity_popup.is_open)
+
+ self.locationbar.open_identity_popup()
+
+ self.identity_popup.close()
+ self.assertFalse(self.identity_popup.is_open)
+
+ @skip_under_xvfb
+ def test_force_close(self):
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(self.url)
+
+ self.assertFalse(self.identity_popup.is_open)
+
+ self.locationbar.open_identity_popup()
+
+ self.identity_popup.close(force=True)
+ self.assertFalse(self.identity_popup.is_open)
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_update_wizard.py
@@ -0,0 +1,67 @@
+# 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.testcases import FirefoxTestCase
+from firefox_puppeteer.ui.update_wizard import UpdateWizardDialog
+
+
+class TestUpdateWizard(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ def opener(win):
+ self.marionette.execute_script("""
+ let updatePrompt = Components.classes["@mozilla.org/updates/update-prompt;1"]
+ .createInstance(Components.interfaces.nsIUpdatePrompt);
+ updatePrompt.checkForUpdates();
+ """)
+
+ self.dialog = self.browser.open_window(callback=opener,
+ expected_window_class=UpdateWizardDialog)
+ self.wizard = self.dialog.wizard
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_basic(self):
+ self.assertEqual(self.dialog.window_type, 'Update:Wizard')
+ self.assertNotEqual(self.dialog.dtds, [])
+ self.assertNotEqual(self.dialog.properties, [])
+
+ def test_elements(self):
+ """Test correct retrieval of elements."""
+ self.assertEqual(self.wizard.element.get_attribute('localName'), 'wizard')
+
+ buttons = ('cancel_button', 'extra1_button', 'extra2_button',
+ 'finish_button', 'next_button', 'previous_button',
+ )
+ for button in buttons:
+ self.assertEqual(getattr(self.wizard, button).get_attribute('localName'),
+ 'button')
+
+ panels = ('checking', 'downloading', 'dummy', 'error_patching', 'error',
+ 'error_extra', 'finished', 'finished_background', 'incompatible_check',
+ 'incompatible_list', 'installed', 'license', 'manual_update',
+ 'no_updates_found', 'plugin_updates_found', 'updates_found_basic',
+ 'updates_found_billboard',
+ )
+ for panel in panels:
+ self.assertEqual(getattr(self.wizard, panel).element.get_attribute('localName'),
+ 'wizardpage')
+
+ # elements of the checking panel
+ self.assertEqual(self.wizard.checking.progress.get_attribute('localName'),
+ 'progressmeter')
+
+ # elements of the downloading panel
+ self.assertEqual(self.wizard.downloading.progress.get_attribute('localName'),
+ 'progressmeter')
+
+ # elements of the incompatible check panel
+ self.assertEqual(self.wizard.incompatible_check.progress.get_attribute('localName'),
+ 'progressmeter')
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_utils.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.testcases import FirefoxTestCase
+
+
+class TestSanitize(FirefoxTestCase):
+
+ def setUp(self):
+ FirefoxTestCase.setUp(self)
+
+ # Clear all previous history and cookies.
+ self.places.remove_all_history()
+ self.marionette.delete_all_cookies()
+
+ self.urls = [
+ 'layout/mozilla_projects.html',
+ 'layout/mozilla.html',
+ 'layout/mozilla_mission.html',
+ 'cookies/cookie_single.html'
+ ]
+ self.urls = [self.marionette.absolute_url(url) for url in self.urls]
+
+ # Open the test urls, including the single cookie setting page.
+ def load_urls():
+ with self.marionette.using_context('content'):
+ for url in self.urls:
+ self.marionette.navigate(url)
+ self.places.wait_for_visited(self.urls, load_urls)
+
+ def tearDown(self):
+ FirefoxTestCase.tearDown(self)
+
+ def test_sanitize_history(self):
+ """ Clears history. """
+ self.assertEqual(self.places.get_all_urls_in_history(), self.urls)
+ self.utils.sanitize(data_type={"history": True})
+ self.assertEqual(self.places.get_all_urls_in_history(), [])
+
+ def test_sanitize_cookies(self):
+ """ Clears cookies. """
+ self.assertIsNotNone(self.marionette.get_cookie('litmus_1'))
+ self.utils.sanitize(data_type={"cookies": True})
+ self.assertIsNone(self.marionette.get_cookie('litmus_1'))
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_windows.py
@@ -0,0 +1,222 @@
+# 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, TimeoutException
+
+import firefox_puppeteer.errors as errors
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+from firefox_puppeteer.ui.windows import BaseWindow
+
+
+class TestWindows(FirefoxTestCase):
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_windows(self):
+ url = self.marionette.absolute_url('layout/mozilla.html')
+
+ # Open two more windows
+ for index in range(0, 2):
+ self.marionette.execute_script(""" window.open(); """)
+
+ windows = self.windows.all
+ self.assertEquals(len(windows), 3)
+
+ # Switch to the 2nd window
+ self.windows.switch_to(windows[1].handle)
+ self.assertEquals(windows[1].handle, self.marionette.current_chrome_window_handle)
+
+ # TODO: Needs updated tabs module for improved navigation
+ with self.marionette.using_context('content'):
+ self.marionette.navigate(url)
+
+ # Switch to the last window and find 2nd window by URL
+ self.windows.switch_to(windows[2].handle)
+
+ # TODO: A window can have multiple tabs, so this may need an update
+ # when the tabs module gets implemented
+ def find_by_url(win):
+ with win.marionette.using_context('content'):
+ return win.marionette.get_url() == url
+
+ self.windows.switch_to(find_by_url)
+ self.assertEquals(windows[1].handle, self.marionette.current_chrome_window_handle)
+
+ self.windows.switch_to(find_by_url)
+
+ # Switching to an unknown handles has to fail
+ self.assertRaises(NoSuchWindowException,
+ self.windows.switch_to, "humbug")
+ self.assertRaises(NoSuchWindowException,
+ self.windows.switch_to, lambda win: False)
+
+ self.windows.close_all([self.browser])
+ self.browser.switch_to()
+
+ self.assertEqual(len(self.windows.all), 1)
+
+
+class TestBaseWindow(FirefoxTestCase):
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_basics(self):
+ # force BaseWindow instance
+ win1 = BaseWindow(lambda: self.marionette, self.browser.handle)
+
+ self.assertEquals(win1.handle, self.marionette.current_chrome_window_handle)
+ self.assertEquals(win1.window_element,
+ self.marionette.find_element(By.CSS_SELECTOR, ':root'))
+ self.assertEquals(win1.window_element.get_attribute('windowtype'),
+ self.marionette.get_window_type())
+ self.assertFalse(win1.closed)
+
+ # Test invalid parameters for BaseWindow constructor
+ self.assertRaises(TypeError,
+ BaseWindow, self.marionette, self.browser.handle)
+ self.assertRaises(errors.UnknownWindowError,
+ BaseWindow, lambda: self.marionette, 10)
+
+ # Test invalid shortcuts
+ self.assertRaises(KeyError,
+ win1.send_shortcut, 'l', acel=True)
+
+ def test_open_close(self):
+ # force BaseWindow instance
+ win1 = BaseWindow(lambda: self.marionette, self.browser.handle)
+
+ # Open a new window (will be focused), and check states
+ win2 = win1.open_window()
+
+ # force BaseWindow instance
+ win2 = BaseWindow(lambda: self.marionette, win2.handle)
+
+ self.assertEquals(len(self.marionette.chrome_window_handles), 2)
+ self.assertNotEquals(win1.handle, win2.handle)
+ self.assertEquals(win2.handle, self.marionette.current_chrome_window_handle)
+
+ win2.close()
+
+ self.assertTrue(win2.closed)
+ self.assertEquals(len(self.marionette.chrome_window_handles), 1)
+ self.assertEquals(win2.handle, self.marionette.current_chrome_window_handle)
+ Wait(self.marionette).until(lambda _: win1.focused) # catch the no focused window
+
+ win1.focus()
+
+ # Open and close a new window by a custom callback
+ def opener(window):
+ window.marionette.execute_script(""" window.open(); """)
+
+ def closer(window):
+ window.marionette.execute_script(""" window.close(); """)
+
+ win2 = win1.open_window(callback=opener)
+
+ # force BaseWindow instance
+ win2 = BaseWindow(lambda: self.marionette, win2.handle)
+
+ self.assertEquals(len(self.marionette.chrome_window_handles), 2)
+ win2.close(callback=closer)
+
+ win1.focus()
+
+ # Check for an unexpected window class
+ self.assertRaises(errors.UnexpectedWindowTypeError,
+ win1.open_window, expected_window_class=BaseWindow)
+ self.windows.close_all([win1])
+
+ def test_switch_to_and_focus(self):
+ # force BaseWindow instance
+ win1 = BaseWindow(lambda: self.marionette, self.browser.handle)
+
+ # Open a new window (will be focused), and check states
+ win2 = win1.open_window()
+
+ # force BaseWindow instance
+ win2 = BaseWindow(lambda: self.marionette, win2.handle)
+
+ self.assertEquals(win2.handle, self.marionette.current_chrome_window_handle)
+ self.assertEquals(win2.handle, self.windows.focused_chrome_window_handle)
+ self.assertFalse(win1.focused)
+ self.assertTrue(win2.focused)
+
+ # Switch back to win1 without moving the focus, but focus separately
+ win1.switch_to()
+ self.assertEquals(win1.handle, self.marionette.current_chrome_window_handle)
+ self.assertTrue(win2.focused)
+
+ win1.focus()
+ self.assertTrue(win1.focused)
+
+ # Switch back to win2 by focusing it directly
+ win2.focus()
+ self.assertEquals(win2.handle, self.marionette.current_chrome_window_handle)
+ self.assertEquals(win2.handle, self.windows.focused_chrome_window_handle)
+ self.assertTrue(win2.focused)
+
+ # Close win2, and check that it keeps active but looses focus
+ win2.switch_to()
+ win2.close()
+
+ win1.switch_to()
+
+
+class TestBrowserWindow(FirefoxTestCase):
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ def test_basic(self):
+ self.assertNotEqual(self.browser.dtds, [])
+ self.assertNotEqual(self.browser.properties, [])
+
+ self.assertFalse(self.browser.is_private)
+
+ self.assertIsNotNone(self.browser.menubar)
+ self.assertIsNotNone(self.browser.navbar)
+ self.assertIsNotNone(self.browser.tabbar)
+
+ def test_open_close(self):
+ # open and close a new browser windows by menu
+ win2 = self.browser.open_browser(trigger='menu')
+ self.assertEquals(win2, self.windows.current)
+ self.assertFalse(self.browser.is_private)
+ win2.close(trigger='menu')
+
+ # open and close a new browser window by shortcut
+ win2 = self.browser.open_browser(trigger='shortcut')
+ self.assertEquals(win2, self.windows.current)
+ self.assertFalse(self.browser.is_private)
+ win2.close(trigger='shortcut')
+
+ # open and close a new private browsing window
+ win2 = self.browser.open_browser(is_private=True)
+ self.assertEquals(win2, self.windows.current)
+ self.assertTrue(win2.is_private)
+ win2.close()
+
+ # open and close a new private browsing window
+ win2 = self.browser.open_browser(trigger='shortcut', is_private=True)
+ self.assertEquals(win2, self.windows.current)
+ self.assertTrue(win2.is_private)
+ win2.close()
+
+ # force closing a window
+ win2 = self.browser.open_browser()
+ self.assertEquals(win2, self.windows.current)
+ win2.close(force=True)
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/cookies/cookie_single.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+<script type="text/javascript">
+ function setCookie()
+ {
+ var date = new Date();
+ date.setDate(new Date().getDate() + 36);
+ document.cookie = "litmus_1=true;expires=" + date.toGMTString();
+ }
+</script>
+</head>
+
+<body onload="setCookie()">
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/mozilla_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#community">Community</a> |
+ <a href="#project">Project</a> |
+ <a href="#organization">Organization</a>
+
+ <div id="content">
+ <h1 id="page-title">
+ <strong>We believe</strong> that the internet should be public,
+ open and accessible.
+ </h1>
+
+ <h2><a name="community">Community</a></h2>
+ <p id="community">
+ We're a global community of thousands who believe in the power
+ of technology to enrich people's lives.
+ <a href="mozilla_community.html">More</a>
+ </p>
+
+ <h2><a name="project">Project</a></h2>
+ <p id="project">
+ We're an open source project whose code is used for some of the
+ Internet's most innovative applications.
+ <a href="mozilla_projects.html">More</a>
+ </p>
+
+ <h2><a name="organization">Organization</a></h2>
+ <p id="organization">
+ We're a public benefit organization dedicated to making the
+ Internet better for everyone.
+ <a href="mozilla_mission.html">More</a>
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_community.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla Community</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/seamonkey_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#history">History</a> |
+ <a href="#communicate">Communicate</a> |
+ <a href="#more">More</a>
+
+ <div id="content">
+ <h1 id="page-title" name="page-title">Our Community</h1>
+
+ <h2><a name="history">History</a></h2>
+ <p id="history">
+ When www.mozilla.org was launched in 1998 all community activity
+ occurred right here on this site. Since then the community has
+ grown much bigger and there are now many different sites,
+ forums, blogs and newsgroups in different places that track
+ different parts of the project. These pages aim to be a
+ comprehensive list to all of the different community resources
+ available. If you know of something that's not on these lists
+ that should be, please contact us and we'll update these
+ pages.
+ </p>
+
+ <h2><a name="communicate">Communicate</a></h2>
+ <p id="communicate">
+ There are a number of different ways community members
+ communicate and coordinate (people use mailing lists and
+ newsgroups, blogs, forums, wikis and they even meet in real
+ life sometimes too) and all of these options might be
+ overwhelming at first. Hopefully this set of links will provide
+ some useful pointers to help you figure out where to go to find
+ what you're looking for. If you do get lost though and need
+ some help, feel free to ask for more information.
+ </p>
+
+ <h2><a name="more">More</a></h2>
+ <p id="more">
+ Please note that this is intended to be an entry point that
+ provides a high-level overview of the different community areas.
+ If you're looking for more detailed information about a specific
+ topic, please look at our Developer,
+ <a href="mozilla_contribute.html">Contribute</a> and Support
+ pages or take a look at the other information referenced
+ throughout this site.
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_contribute.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla Contribute</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/thunderbird_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#summary">Summary</a> |
+ <a href="#contribute">Contribute</a>
+
+ <div id="content">
+ <h1 id="page-title">Get Involved</h1>
+
+ <h2><a name="summary">Summary</a></h2>
+ <p id="summary">
+ You can <a href="mozilla_mission.html">build a better Internet</a>
+ by getting involved with Mozilla. You don't have to be a C++
+ guru (or even know what that means!) and you don't need to spend
+ lots of time. Take a look at the opportunities below and feel
+ free to ask if you have any questions.
+ </p>
+
+ <h2><a name="contribute">Contribute</a></h2>
+ <p id="contribute">
+ <h3>Area of Interest</h3>
+ <i>Browse contribution opportunities by area of interest.</i>
+
+ <ul id="areas_of_interest">
+ <li id="browser_choice">
+ <h4>Web Browser Choice</h4>
+ <p>
+ Mozilla has always believed that the freedom to
+ make informed choices should be central to making
+ the Web, and the world, a better place. Tell us
+ why having a choice of browser is important to you
+ and help us spread the word about how others can
+ take control of their online lives.
+ </p>
+ </li>
+ <li id="helping_users">
+ <h4>Helping Users</h4>
+ <p>
+ Interested in helping others get the most out of
+ using Firefox and other Mozilla projects? Our
+ support process relies on enthusiastic
+ contributors like you. Find out more about
+ supporting Firefox, Thunderbird and other Mozilla
+ projects.
+ </p>
+ </li>
+ <li id="localization">
+ <h4>Localization</h4>
+ <p>
+ Get involved with Mozilla by making Firefox,
+ Thunderbird and other projects available in your
+ language. Also help us tell the world about how
+ Mozilla is building a better Internet by
+ translating content on our web sites.
+ </p>
+ </li>
+ </ul>
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_governance.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla Governance</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/firefox_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#summary">Summary</a> |
+ <a href="#more">More</a>
+
+ <div id="content">
+ <h1 id="page-title">Governance</h1>
+
+ <h2><a name="summary">Summary</a></h2>
+ <p id="summary">
+ Mozilla is an open source project governed as a meritocracy. Our
+ community is structured as a virtual organization where
+ authority is distributed to both volunteer and employed
+ community members as they show their abilities through
+ contributions to the project.
+ </p>
+
+ <h2><a name="more">More</a></h2>
+ <p id="more">
+ <ul id="list">
+ <li id="roles">Roles and Responsibilities</li>
+ <li id="policies">Policies</li>
+ <li id="discussion">Discussion</li>
+ </ul>
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_grants.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla Grants</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/mozilla_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#summary">Summary</a> |
+ <a href="#goals">Goals</a>
+
+ <div id="content">
+ <h1 id="page-title">Mozilla Grants</h1>
+
+ <h2><a name="summary">Summary</a></h2>
+ <p id="summary">
+ Since 2006, Mozilla has awarded over two million dollars to fund
+ projects that contribute to the health of the Open Web. The
+ Mozilla Grants program is jointly funded by the Mozilla
+ Corporation and the Mozilla Foundation, and awards financial
+ support to individuals and organizations whose work supports and
+ enhances the mission and values of the Mozilla Project.
+ </p>
+
+ <h2><a name="goals">Goals</a></h2>
+ <p id="goals">
+ Mozilla makes grants to individuals and organizations all over
+ the world. We mainly fund activity that supports the Mozilla
+ Grants program's four target areas:
+
+ <ul id="goal_list">
+ <li id="accessibility">
+ <strong>Accessibility:</strong> Mozilla believes that
+ the Internet truly is for everyone, and that those with
+ disabilities should be able to participate on the Web
+ along with their sighted and hearing peers. As part of
+ our accessibility strategy, we are funding the
+ development of free, open source options for those with
+ visual and auditory impairments.
+ </li>
+
+ <li id="community">
+ <strong>Community:</strong> Mozilla offers suppport to
+ the broader free culture and open source community, as
+ part of Mozilla's general effort to 'give back', aiding
+ in the creation of technologies and projects that
+ increase the health of the open Web ecosystem.
+ </li>
+
+ <li id="education">
+ <strong>Education:</strong> As part of Mozilla's broader
+ education initiative, we support educational
+ institutions that are producing the next generation of
+ innovative creators of software.
+ </li>
+
+ <li id="open_source">
+ <strong>Open Source:</strong> These grants support the
+ creation and adoption of Web standards, open source
+ principles, and the overall principles of transparency,
+ collaboration, and openness that free and open source
+ software projects adhere to.
+ </li>
+ </ul>
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_mission.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla Mission</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/seamonkey_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#mission">Mission</a> |
+ <a href="#organization">Organization</a> |
+ <a href="#goal">Goal</a>
+
+ <div id="content" name="content">
+ <h1 id="page-title" name="page-title">Mission</h1>
+
+ <h2><a name="mission">Mission</a></h2>
+ <p id="mission_statement">
+ Mozilla's mission is to <strong>promote openness, innovation,
+ and opportunity on the web</strong>. We do this by creating
+ great software, like the Firefox browser, and building
+ movements, like Drumbeat, that give people tools to take control
+ of their online lives.
+ </p>
+
+ <h2><a name="organization">Organization</a></h2>
+ <p id="organization">
+ As a non-profit organization, we define success in terms of
+ building communities and enriching people's lives instead of
+ benefiting our shareholders (guess what: we don't even have
+ shareholders). We believe in the power and potential of the
+ Internet and want to see it thrive for everyone, everywhere.
+ </p>
+
+ <h2><a name="goal">Goal</a></h2>
+ <p id="goal">
+ <strong>
+ Building a better Internet is an ambitious goal, but we
+ believe that it is possible
+ </strong>
+ when people who share our passion get involved. Coders, artists,
+ writers, testers, surfers, students, grandparents; anyone who
+ uses and cares about the web can help make it even better.
+ <a href="mozilla_contribute.html">Find out how you can help</a>.
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_organizations.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla Organizations</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/thunderbird_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#summary">Summary</a> |
+ <a href="#organization">Organization</a>
+
+ <div id="content">
+ <h1 id="page-title">Mozilla Organizations</h1>
+
+ <h2><a name="summary">Summary</a></h2>
+ <p id="summary">
+ Mozilla is a global community of people creating a better
+ Internet. We build public benefit into the Internet by creating
+ free, open source products and technologies that improve the
+ online experience for people everywhere.
+ </p>
+
+ <h2><a name="organization">Organization</a></h2>
+ <p id="organization">
+ There are several organizations that support the Mozilla
+ community and Mozilla's principles. They include the non-profit
+ Mozilla Foundation as well as two wholly owned taxable
+ subsidiaries, the Mozilla Corporation and Mozilla Messaging.
+ Mozilla considers itself a hybrid organization, combining non-
+ profit and market strategies to ensure the Internet remains a
+ shared public resource.
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_projects.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla Projects</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/firefox_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#summary">Summary</a> |
+ <a href="#applications">Applications</a>
+
+ <div id="content">
+ <h1 id="page-title">Our Projects</h1>
+
+ <h2><a name="summary">Summary</a></h2>
+ <p id="summary">
+ The Mozilla community produces a lot of great software and acts
+ as an incubator for innovative ideas as a way to advance our
+ <a href="mozilla_mission.html">mission</a> of building a better
+ Internet.
+ </p>
+
+ <h2><a name="applications">Applications</a></h2>
+ <p id="applications">
+ <p>
+ These applications are developed by the Mozilla community
+ and their code is hosted on mozilla.org.
+ </p>
+
+ <ul id="product_list">
+ <li id="bugzilla">
+ <h3><strong>Bugzilla</strong></h3>
+ Bugzilla is a bug tracking system designed to help teams
+ manage software development. Hundreds of organizations
+ across the globe are using this powerful tool to get
+ organized and communicate effectively.
+ </li>
+
+ <li id="camino">
+ <h3><strong>Camino</strong></h3>
+ Camino is a Web browser optimized for Mac OS X with a
+ Cocoa user interface, and powerful Gecko layout engine.
+ It's the simple, secure, and fast browser for Mac OS X.
+ </li>
+
+ <li id="firefox">
+ <h3><strong>Firefox for Desktop</strong></h3>
+ The award-winning Firefox Web browser has security,
+ speed and new features that will change the way you use
+ the Web. Don’t settle for anything less.
+ </li>
+ </ul>
+ </p>
+ </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/private_browsing/about.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+</head>
+
+<body>
+ <div id="about_pb">About Private Browsing</div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/resources/security/enable_privilege.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <title>Test page for enablePrivilege</title>
+ <script>
+ function init() {
+ var result = document.getElementById("result");
+ try {
+ netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+ result.textContent = "FAIL";
+ }
+ catch (ex) {
+ result.textContent = "PASS";
+ }
+ }
+ </script>
+ </head>
+ <body onload="init();">
+ <p id="result"></p>
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/update/direct/manifest.ini
@@ -0,0 +1,1 @@
+[test_direct_update.py]
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/update/direct/test_direct_update.py
@@ -0,0 +1,31 @@
+# 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.testcases import UpdateTestCase
+
+
+class TestDirectUpdate(UpdateTestCase):
+
+ def setUp(self):
+ UpdateTestCase.setUp(self, is_fallback=False)
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ UpdateTestCase.tearDown(self)
+
+ def _test_update(self):
+ self.download_and_apply_available_update(force_fallback=False)
+
+ self.check_update_applied()
+
+ def test_update(self):
+ try:
+ self._test_update()
+ except:
+ # Switch context to the main browser window before embarking
+ # down the failure code path to work around bug 1141519.
+ self.browser.switch_to()
+ raise
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/update/fallback/manifest.ini
@@ -0,0 +1,1 @@
+[test_fallback_update.py]
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/firefox_ui_tests/update/fallback/test_fallback_update.py
@@ -0,0 +1,33 @@
+# 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.testcases import UpdateTestCase
+
+
+class TestFallbackUpdate(UpdateTestCase):
+
+ def setUp(self):
+ UpdateTestCase.setUp(self, is_fallback=True)
+
+ def tearDown(self):
+ try:
+ self.windows.close_all([self.browser])
+ finally:
+ UpdateTestCase.tearDown(self)
+
+ def _test_update(self):
+ self.download_and_apply_available_update(force_fallback=True)
+
+ self.download_and_apply_forced_update()
+
+ self.check_update_applied()
+
+ def test_update(self):
+ try:
+ self._test_update()
+ except:
+ # Switch context to the main browser window before embarking
+ # down the failure code path to work around bug 1141519.
+ self.browser.switch_to()
+ raise
new file mode 100644
--- /dev/null
+++ b/testing/firefox-ui/tests/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/.
+
+from setuptools import setup, find_packages
+
+PACKAGE_VERSION = '0.3'
+
+deps = [
+ 'firefox_puppeteer >= 3.0.0, <4.0.0',
+ 'firefox_ui_harness == 1.1.0',
+]
+
+setup(name='firefox_ui_tests',
+ version=PACKAGE_VERSION,
+ description='A collection of Mozilla Firefox UI tests run with Marionette',
+ long_description='See https://github.com/mozilla/firefox-ui-tests',
+ classifiers=['Environment :: Console',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+ keywords='mozilla',
+ author='Mozilla Automation and Tools Team',
+ author_email='tools@lists.mozilla.org',
+ url='https://github.com/mozilla/firefox-ui-tests',
+ license='MPL 2.0',
+ packages=find_packages(),
+ include_package_data=True,
+ package_data={
+ '': [
+ '*.html',
+ '*.ico',
+ '*.ini',
+ '*.jpg',
+ '*.js',
+ ]
+ },
+ zip_safe=False,
+ install_requires=deps,
+ )
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/MANIFEST.in
@@ -0,0 +1,2 @@
+exclude MANIFEST.in
+include requirements.txt
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FirefoxPuppeteer.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FirefoxPuppeteer.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/FirefoxPuppeteer"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FirefoxPuppeteer"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/appinfo.rst
@@ -0,0 +1,15 @@
+.. py:currentmodule:: firefox_puppeteer.api.appinfo
+
+AppInfo
+=======
+
+The appinfo class is a wrapper around the nsIXULAppInfo_ interface in
+Firefox and provides access to a subset of its members.
+
+.. _nsIXULAppInfo: https://developer.mozilla.org/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIXULAppInfo
+
+AppInfo
+-------
+
+.. autoclass:: AppInfo
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/keys.rst
@@ -0,0 +1,12 @@
+.. py:currentmodule:: firefox_puppeteer.api.keys
+
+Keys
+====
+
+Keys
+----
+
+.. autoclass:: Keys
+ :members:
+ :inherited-members:
+ :undoc-members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/l10n.rst
@@ -0,0 +1,11 @@
+.. py:currentmodule:: firefox_puppeteer.api.l10n
+
+Localization
+============
+
+Localization
+------------
+
+.. autoclass:: L10n
+ :members:
+ :undoc-members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/places.rst
@@ -0,0 +1,13 @@
+.. py:currentmodule:: firefox_puppeteer.api.places
+
+Places
+======
+
+The Places class provides low-level access for several bookmark and history
+related methods.
+
+Places
+------
+
+.. autoclass:: Places
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/prefs.rst
@@ -0,0 +1,18 @@
+.. py:currentmodule:: firefox_puppeteer.api.prefs
+
+Preferences
+===========
+
+The Preferences class is a wrapper around the nsIPrefBranch_ interface in
+Firefox and allows you to interact with the preferences system. It only
+includes the most commonly used methods of that interface, whereby it also
+enhances the logic in terms of e.g. restoring the original value of modified
+preferences.
+
+.. _nsIPrefBranch: https://developer.mozilla.org/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefBranch
+
+Preferences
+-----------
+
+.. autoclass:: Preferences
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/security.rst
@@ -0,0 +1,13 @@
+.. py:currentmodule:: firefox_puppeteer.api.security
+
+Security
+===========
+
+The Security class gives access to various helper methods, which assist in working with
+certificates or accessing specific security related information.
+
+Security
+--------
+
+.. autoclass:: Security
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/software_update.rst
@@ -0,0 +1,30 @@
+.. py:currentmodule:: firefox_puppeteer.api.software_update
+
+SoftwareUpdate
+==============
+
+The SoftwareUpdate class provides helpers for update tests.
+
+SoftwareUpdate
+--------------
+
+.. autoclass:: SoftwareUpdate
+ :members:
+
+ActiveUpdate
+------------
+
+.. autoclass:: ActiveUpdate
+ :members:
+
+MARChannels
+-----------
+
+.. autoclass:: MARChannels
+ :members:
+
+UpdateChannel
+-------------
+
+.. autoclass:: UpdateChannel
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/api/utils.rst
@@ -0,0 +1,12 @@
+.. py:currentmodule:: firefox_puppeteer.api.utils
+
+Utils
+===========
+
+The Utils class gives access to various helper methods.
+
+Utils
+-----
+
+.. autoclass:: Utils
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/conf.py
@@ -0,0 +1,270 @@
+# -*- coding: utf-8 -*-
+#
+# Firefox Puppeteer documentation build configuration file, created by
+# sphinx-quickstart on Thu Nov 20 10:35:33 2014.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.todo',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Firefox Puppeteer'
+copyright = u'2014-2015, Mozilla'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1'
+# The full version, including alpha/beta/rc tags.
+release = '0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ''
+# Else, today_fmt is used as the format for a strftime call.
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+
+if not on_rtd:
+ try:
+ import sphinx_rtd_theme
+ html_theme = 'sphinx_rtd_theme'
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+ except ImportError:
+ pass
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+# html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+# html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+# html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+# html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+# html_domain_indices = True
+
+# If false, no index is generated.
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'FirefoxPuppeteerdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ # 'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'FirefoxPuppeteer.tex', u'Firefox Puppeteer Documentation',
+ u'Mozilla', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+# latex_appendices = []
+
+# If false, no module index is generated.
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'firefoxpuppeteer', u'Firefox Puppeteer Documentation',
+ [u'Mozilla'], 1)
+]
+
+# If true, show URL addresses after external links.
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'FirefoxPuppeteer', u'Firefox Puppeteer Documentation',
+ u'Mozilla', 'FirefoxPuppeteer', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+# texinfo_no_detailmenu = False
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/index.rst
@@ -0,0 +1,60 @@
+.. py:currentmodule:: firefox_puppeteer
+
+Firefox Puppeteer
+=================
+
+Firefox Puppeteer is a library built on top of the `Marionette python client`_.
+It aims to make automation of Firefox's browser UI simpler. It does **not**
+make sense to use Firefox Puppeteer if:
+
+* You are manipulating something other than Firefox (like Firefox OS)
+* You are only manipulating elements in content scope (like a webpage)
+
+Roughly speaking, Firefox Puppeteer provides a library to manipulate each
+visual section of Firefox's browser UI. For example, there are different
+libraries for the tab bar, the navigation bar, etc.
+
+
+Installation
+------------
+
+Currently Firefox Puppeteer lives in the `firefox-ui-tests`_ repository,
+along with instructions for installation and usage.
+There are plans to move it alongside the `Marionette python client`_.
+
+.. _Marionette python client: http://marionette-client.readthedocs.org/en/latest/
+.. _firefox-ui-tests: https://github.com/mozilla/firefox-ui-tests/tree/mozilla-central/firefox_puppeteer
+
+
+Libraries
+---------
+
+The following libraries are currently implemented. More will be added in the
+future. Each library is available from an instance of the FirefoxTestCase class.
+
+.. toctree::
+
+ ui/about_window/window
+ ui/menu
+ ui/pageinfo/window
+ ui/browser/tabbar
+ ui/browser/toolbars
+ ui/browser/window
+ ui/update_wizard/dialog
+ ui/windows
+ api/appinfo
+ api/keys
+ api/l10n
+ api/places
+ api/prefs
+ api/security
+ api/software_update
+ api/utils
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\FirefoxPuppeteer.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\FirefoxPuppeteer.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/about_window/window.rst
@@ -0,0 +1,66 @@
+About Window
+============
+
+AboutWindow
+--------------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.window.AboutWindow
+ :members:
+ :inherited-members:
+
+Deck
+----
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.Deck
+ :members:
+ :inherited-members:
+
+ApplyBillboardPanel
+-------------------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.ApplyBillboardPanel
+ :members:
+ :inherited-members:
+
+ApplyPanel
+----------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.ApplyPanel
+ :members:
+ :inherited-members:
+
+CheckForUpdatesPanel
+--------------------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.CheckForUpdatesPanel
+ :members:
+ :inherited-members:
+
+CheckingForUpdatesPanel
+-----------------------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.CheckingForUpdatesPanel
+ :members:
+ :inherited-members:
+
+DownloadAndInstallPanel
+-----------------------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.DownloadAndInstallPanel
+ :members:
+ :inherited-members:
+
+DownloadFailedPanel
+-------------------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.DownloadFailedPanel
+ :members:
+ :inherited-members:
+
+DownloadingPanel
+----------------
+
+.. autoclass:: firefox_puppeteer.ui.about_window.deck.DownloadingPanel
+ :members:
+ :inherited-members:
+
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/browser/tabbar.rst
@@ -0,0 +1,22 @@
+.. py:currentmodule:: firefox_puppeteer.ui.browser.tabbar
+
+Tabbar
+======
+
+TabBar
+------
+
+.. autoclass:: TabBar
+ :members:
+
+Tab
+---
+
+.. autoclass:: Tab
+ :members:
+
+MenuPanel
+----------
+
+.. autoclass:: MenuPanel
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/browser/toolbars.rst
@@ -0,0 +1,28 @@
+.. py:currentmodule:: firefox_puppeteer.ui.browser.toolbars
+
+Toolbars
+========
+
+NavBar
+------
+
+.. autoclass:: NavBar
+ :members:
+
+LocationBar
+-----------
+
+.. autoclass:: LocationBar
+ :members:
+
+AutocompleteResults
+-------------------
+
+.. autoclass:: AutocompleteResults
+ :members:
+
+IdentityPopup
+-------------
+
+.. autoclass:: IdentityPopup
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/browser/window.rst
@@ -0,0 +1,11 @@
+.. py:currentmodule:: firefox_puppeteer.ui.browser.window
+
+BrowserWindow
+=============
+
+BrowserWindow
+-------------
+
+.. autoclass:: BrowserWindow
+ :members:
+ :inherited-members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/menu.rst
@@ -0,0 +1,10 @@
+.. py:currentmodule:: firefox_puppeteer.ui.menu
+
+Menu
+====
+
+Menu Bar
+--------
+
+.. autoclass:: MenuBar
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/pageinfo/window.rst
@@ -0,0 +1,23 @@
+Page Info Window
+================
+
+PageInfoWindow
+--------------
+
+.. autoclass:: firefox_puppeteer.ui.pageinfo.window.PageInfoWindow
+ :members:
+ :inherited-members:
+
+Deck
+----
+
+.. autoclass:: firefox_puppeteer.ui.pageinfo.deck.Deck
+ :members:
+ :inherited-members:
+
+SecurityPanel
+-------------
+
+.. autoclass:: firefox_puppeteer.ui.pageinfo.deck.SecurityPanel
+ :members:
+ :inherited-members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/update_wizard/dialog.rst
@@ -0,0 +1,136 @@
+Update Wizard Dialog
+====================
+
+UpdateWizardDialog
+------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.dialog.UpdateWizardDialog
+ :members:
+ :inherited-members:
+
+Wizard
+------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.Wizard
+ :members:
+ :inherited-members:
+
+CheckingPanel
+-------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.CheckingPanel
+ :members:
+ :inherited-members:
+
+DownloadingPanel
+----------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.DownloadingPanel
+ :members:
+ :inherited-members:
+
+DummyPanel
+----------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.DummyPanel
+ :members:
+ :inherited-members:
+
+ErrorPatchingPanel
+------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.ErrorPatchingPanel
+ :members:
+ :inherited-members:
+
+ErrorPanel
+----------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.ErrorPanel
+ :members:
+ :inherited-members:
+
+ErrorExtraPanel
+---------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.ErrorExtraPanel
+ :members:
+ :inherited-members:
+
+FinishedPanel
+-------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.FinishedPanel
+ :members:
+ :inherited-members:
+
+FinishedBackgroundPanel
+-----------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.FinishedBackgroundPanel
+ :members:
+ :inherited-members:
+
+IncompatibleCheckPanel
+----------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.IncompatibleCheckPanel
+ :members:
+ :inherited-members:
+
+IncompatibleListPanel
+---------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.IncompatibleListPanel
+ :members:
+ :inherited-members:
+
+InstalledPanel
+--------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.InstalledPanel
+ :members:
+ :inherited-members:
+
+LicensePanel
+------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.LicensePanel
+ :members:
+ :inherited-members:
+
+ManualUpdatePanel
+-----------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.ManualUpdatePanel
+ :members:
+ :inherited-members:
+
+NoUpdatesFoundPanel
+-------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.NoUpdatesFoundPanel
+ :members:
+ :inherited-members:
+
+PluginUpdatesFoundPanel
+-----------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.PluginUpdatesFoundPanel
+ :members:
+ :inherited-members:
+
+UpdatesFoundBasicPanel
+----------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.UpdatesFoundBasicPanel
+ :members:
+ :inherited-members:
+
+
+UpdatesFoundBillboardPanel
+--------------------------
+
+.. autoclass:: firefox_puppeteer.ui.update_wizard.wizard.UpdatesFoundBillboardPanel
+ :members:
+ :inherited-members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/docs/ui/windows.rst
@@ -0,0 +1,16 @@
+.. py:currentmodule:: firefox_puppeteer.ui.windows
+
+Windows
+=======
+
+Windows
+-------
+
+.. autoclass:: Windows
+ :members:
+
+BaseWindow
+----------
+
+.. autoclass:: BaseWindow
+ :members:
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/__init__.py
@@ -0,0 +1,117 @@
+# 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
+
+from marionette_driver.marionette import HTMLElement
+
+from decorators import use_class_as_property
+
+
+__version__ = '3.0.0'
+
+root = os.path.abspath(os.path.dirname(__file__))
+manifest = os.path.join(root, 'tests', 'manifest.ini')
+
+
+class Puppeteer(object):
+ """The puppeteer class is used to expose libraries to test cases.
+
+ Each library can be referenced by its puppeteer name as a member of a
+ FirefoxTestCase instance. For example, from within a test method, the
+ "current_window" member of the "Browser" class can be accessed via
+ "self.browser.current_window".
+ """
+
+ def __init__(self):
+ self.marionette = None
+
+ def get_marionette(self):
+ return self.marionette
+
+ def set_marionette(self, marionette):
+ self.marionette = 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'].lower()
+
+ @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.
+ """
+
+
+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
+
+ def get_marionette(self):
+ return self.marionette
new file mode 100644
--- /dev/null
+++ b/testing/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;
+ """)
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/api/keys.py
@@ -0,0 +1,20 @@
+# 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_getter):
+ self.marionette_getter = marionette_getter
+
+ caps = self.marionette_getter().session_capabilities
+ self.isDarwin = caps['platformName'] == 'DARWIN'
+
+ @property
+ def ACCEL(self):
+ return self.META if self.isDarwin else self.CONTROL
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/api/l10n.py
@@ -0,0 +1,92 @@
+# 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 copy
+
+from marionette_driver.errors import MarionetteException
+
+from firefox_puppeteer.base import BaseLib
+
+
+class L10n(BaseLib):
+
+ def get_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 MarionetteException: When entity id is not found in dtd_urls.
+ """
+ # Add xhtml11.dtd to prevent missing entity errors with XHTML files
+ 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 MarionetteException('DTD Entity not found: %s' % entity_id)
+
+ return value
+
+ def get_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 MarionetteException: When property id is not found in
+ property_urls.
+ """
+
+ 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 MarionetteException('Property not found: %s' % property_id)
+
+ return value
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/api/places.py
@@ -0,0 +1,183 @@
+# 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, TimeoutException
+
+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', 'tags', 'unfiled'])
+ bookmark_folders = BookmarkFolders(1, 2, 3, 4, 5)
+
+ # Bookmark related helpers #
+
+ def is_bookmarked(self, url):
+ """Checks if the given URL is bookmarked.
+
+ :param url: The URL to Check
+
+ :returns: True, if the URL is a bookmark
+ """
+ return self.marionette.execute_script("""
+ let url = arguments[0];
+
+ let bs = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]
+ .getService(Components.interfaces.nsINavBookmarksService);
+ let ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+
+ let uri = ios.newURI(url, null, null);
+ let results = bs.getBookmarkIdsForURI(uri, {});
+
+ return results.length == 1;
+ """, script_args=[url])
+
+ def get_folder_ids_for_url(self, url):
+ """Retrieves 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_script("""
+ let url = arguments[0];
+
+ let bs = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]
+ .getService(Components.interfaces.nsINavBookmarksService);
+ let ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+
+ let bookmarkIds = bs.getBookmarkIdsForURI(ios.newURI(url, null, null), {});
+ let folderIds = [];
+
+ for (let i = 0; i < bookmarkIds.length; i++) {
+ folderIds.push(bs.getFolderIdForItem(bookmarkIds[i]));
+ }
+
+ return folderIds;
+ """, script_args=[url])
+
+ def is_bookmark_star_button_ready(self):
+ """Checks 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):
+ """Restores the default bookmarks for the current profile."""
+ retval = self.marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ // Default bookmarks.html file is stored inside omni.jar,
+ // so get it via a resource URI
+ let defaultBookmarks = 'resource:///defaults/profile/bookmarks.html';
+
+ let observer = {
+ observe: function (aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, "bookmarks-restore-success");
+ Services.obs.removeObserver(observer, "bookmarks-restore-failed");
+
+ marionetteScriptFinished(aTopic == "bookmarks-restore-success");
+ }
+ };
+
+ // Trigger the import of the default bookmarks
+ Services.obs.addObserver(observer, "bookmarks-restore-success", false);
+ Services.obs.addObserver(observer, "bookmarks-restore-failed", false);
+ BookmarkHTMLUtils.importFromURL(defaultBookmarks, true);
+ """, script_timeout=10000)
+
+ if not retval:
+ raise MarionetteException("Restore Default Bookmarks failed")
+
+ # Browser history related helpers #
+
+ def get_all_urls_in_history(self):
+ return self.marionette.execute_script("""
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService);
+ let urls = [];
+
+ let options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_URI;
+
+ let root = hs.executeQuery(hs.getNewQuery(), options).root
+ 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):
+ """Removes all history items."""
+ with self.marionette.using_context('chrome'):
+ try:
+ self.marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ let hs = Components.classes["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Components.interfaces.nsIBrowserHistory);
+
+ let observer = {
+ observe: function (aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, 'places-expiration-finished');
+
+ marionetteScriptFinished(true);
+ }
+ };
+
+ // Remove the pages, then block until we're done or until timeout is reached
+ Services.obs.addObserver(observer, 'places-expiration-finished', false);
+
+ hs.removeAllPages();
+ """, script_timeout=10000)
+ except TimeoutException:
+ # TODO: In case of a timeout clean-up the registered topic
+ pass
+
+ def wait_for_visited(self, urls, callback):
+ """Waits 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):
+ """Clears 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) {
+ }
+ });
+ """)
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/api/prefs.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.errors import MarionetteException
+
+from firefox_puppeteer.base import BaseLib
+
+
+class Preferences(BaseLib):
+ archive = {}
+
+ def get_pref(self, pref_name, default_branch=False, interface=None):
+ """Retrieves the value of a preference.
+
+ To retrieve the value of a preference its name has to be specified. By
+ default it will be read from the `user` branch, and returns the current
+ value. If the default value is wanted instead, ensure to flag that by
+ passing `True` via default_branch.
+
+ It is also possible to retrieve the value of complex preferences, which
+ do not represent an atomic type like `basestring`, `int`, or `boolean`.
+ Specify the interface of the represented XPCOM object via `interface`.
+
+ :param pref_name: The preference name
+ :param default_branch: Optional, flag to use the default branch,
+ default to `False`
+ :param interface: Optional, interface of the complex preference,
+ default to `None`. Possible values are: `nsILocalFile`,
+ `nsISupportsString`, and `nsIPrefLocalizedString`
+
+ :returns: The preference value.
+ """
+ assert pref_name is not None
+
+ with self.marionette.using_context('chrome'):
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ let pref_name = arguments[0];
+ let default_branch = arguments[1];
+ let interface = arguments[2];
+
+ let prefBranch;
+ if (default_branch) {
+ prefBranch = Services.prefs.getDefaultBranch("");
+ }
+ else {
+ prefBranch = Services.prefs;
+ }
+
+ // If an interface has been set, handle it differently
+ if (interface !== null) {
+ return prefBranch.getComplexValue(pref_name,
+ Components.interfaces[interface]).data;
+ }
+
+ let type = prefBranch.getPrefType(pref_name);
+
+ switch (type) {
+ case prefBranch.PREF_STRING:
+ return prefBranch.getCharPref(pref_name);
+ case prefBranch.PREF_BOOL:
+ return prefBranch.getBoolPref(pref_name);
+ case prefBranch.PREF_INT:
+ return prefBranch.getIntPref(pref_name);
+ case prefBranch.PREF_INVALID:
+ return null;
+ }
+ """, script_args=[pref_name, default_branch, interface])
+
+ def reset_pref(self, pref_name):
+ """Resets a user set preference.
+
+ Every modification of a preference will turn its status into a user-set
+ preference. To restore the default value of the preference, call this
+ function once. Further calls have no effect as long as the value hasn't
+ changed again.
+
+ In case the preference is not present on the default branch, it will be
+ completely removed.
+
+ :param pref_name: The preference to reset
+
+ :returns: `True` if a user preference has been removed
+ """
+ assert pref_name is not None
+
+ with self.marionette.using_context('chrome'):
+ return self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let prefBranch = Services.prefs;
+
+ let pref_name = arguments[0];
+
+ if (prefBranch.prefHasUserValue(pref_name)) {
+ prefBranch.clearUserPref(pref_name);
+ return true;
+ }
+ else {
+ return false;
+ }
+ """, script_args=[pref_name])
+
+ def restore_all_prefs(self):
+ """Restores all previously modified preferences to their former values.
+
+ Please see :func:`~Preferences.restore_pref` for details.
+ """
+ while len(self.archive):
+ self.restore_pref(self.archive.keys()[0])
+
+ def restore_pref(self, pref_name):
+ """Restores a previously set preference to its former value.
+
+ The first time a preference gets modified a backup of its value is
+ made. By calling this method, exactly this value will be restored,
+ whether how often the preference has been modified again afterward.
+
+ If the preference did not exist before and has been newly created, it
+ will be reset to its original value. Please see
+ :func:`~Preferences.reset_pref` for details.
+
+ :param pref_name: The preference to restore
+ """
+ assert pref_name is not None
+
+ try:
+ # in case it is a newly set preference, reset it. Otherwise restore
+ # its original value.
+ if self.archive[pref_name] is None:
+ self.reset_pref(pref_name)
+ else:
+ self.set_pref(pref_name, self.archive[pref_name])
+
+ del self.archive[pref_name]
+ except KeyError:
+ raise MarionetteException('Nothing to restore for preference "%s"',
+ pref_name)
+
+ def set_pref(self, pref_name, value):
+ """Sets a preference to a specified value.
+
+ To set the value of a preference its name has to be specified.
+
+ The first time a new value for a preference is set, its value will be
+ automatically archived. It allows to restore the original value by
+ calling :func:`~Preferences.restore_pref`.
+
+ :param pref_name: The preference to set
+ :param value: The value to set the preference to
+ """
+ assert pref_name is not None
+ assert value is not None
+
+ with self.marionette.using_context('chrome'):
+ # Backup original value only once
+ if pref_name not in self.archive:
+ self.archive[pref_name] = self.get_pref(pref_name)
+
+ retval = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let prefBranch = Services.prefs;
+
+ let pref_name = arguments[0];
+ let value = arguments[1];
+
+ let type = prefBranch.getPrefType(pref_name);
+
+ // If the pref does not exist yet, get the type from the value
+ if (type == prefBranch.PREF_INVALID) {
+ switch (typeof value) {
+ case "boolean":
+ type = prefBranch.PREF_BOOL;
+ break;
+ case "number":
+ type = prefBranch.PREF_INT;
+ break;
+ case "string":
+ type = prefBranch.PREF_STRING;
+ break;
+ default:
+ type = prefBranch.PREF_INVALID;
+ }
+ }
+
+ switch (type) {
+ case prefBranch.PREF_BOOL:
+ prefBranch.setBoolPref(pref_name, value);
+ break;
+ case prefBranch.PREF_STRING:
+ prefBranch.setCharPref(pref_name, value);
+ break;
+ case prefBranch.PREF_INT:
+ prefBranch.setIntPref(pref_name, value);
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ """, script_args=[pref_name, value])
+
+ assert retval
new file mode 100644
--- /dev/null
+++ b/testing/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])
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/api/software_update.py
@@ -0,0 +1,424 @@
+# 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
+
+import mozinfo
+
+from firefox_puppeteer.base import BaseLib
+from firefox_puppeteer.api.appinfo import AppInfo
+from firefox_puppeteer.api.prefs import Preferences
+
+
+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'
+
+ def __init__(self, marionette_getter):
+ BaseLib.__init__(self, marionette_getter)
+
+ self._ini_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;
+ """)
+
+ @property
+ def config_file_path(self):
+ """The path to the update-settings.ini file."""
+ return self._ini_file_path
+
+ @property
+ def config_file_contents(self):
+ """The contents of the update-settings.ini file."""
+ with open(self.config_file_path) as f:
+ return f.read()
+
+ @property
+ def channels(self):
+ """The channels as found in the ACCEPTED_MAR_CHANNEL_IDS option
+ of the update-settings.ini file.
+
+ :returns: A set of channel names
+ """
+ channels = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/FileUtils.jsm");
+ let iniFactory = Components.classes['@mozilla.org/xpcom/ini-processor-factory;1']
+ .getService(Components.interfaces.nsIINIParserFactory);
+
+ let file = new FileUtils.File(arguments[0]);
+ let parser = iniFactory.createINIParser(file);
+
+ return parser.getString(arguments[1], arguments[2]);
+ """, script_args=[self.config_file_path, self.INI_SECTION, self.INI_OPTION])
+ return set(channels.split(','))
+
+ @channels.setter
+ def channels(self, channels):
+ """Set the channels in the update-settings.ini file.
+
+ :param channels: A set of channel names
+ """
+ new_channels = ','.join(channels)
+ self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/FileUtils.jsm");
+ let iniFactory = Components.classes['@mozilla.org/xpcom/ini-processor-factory;1']
+ .getService(Components.interfaces.nsIINIParserFactory);
+
+ let file = new FileUtils.File(arguments[0]);
+
+ let writer = iniFactory.createINIParser(file)
+ .QueryInterface(Components.interfaces.nsIINIParserWriter);
+
+ writer.setString(arguments[1], arguments[2], arguments[3]);
+ writer.writeFile(null, Components.interfaces.nsIINIParserWriter.WRITE_UTF16);
+ """, script_args=[self.config_file_path, self.INI_SECTION, self.INI_OPTION, new_channels])
+
+ def add_channels(self, channels):
+ """Add channels to the update-settings.ini file.
+
+ :param channels: A set of channel names to add
+ """
+ self.channels = self.channels | set(channels)
+
+ def remove_channels(self, channels):
+ """Remove channels from the update-settings.ini file.
+
+ :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_URL = 'app.update.url'
+ PREF_APP_UPDATE_URL_OVERRIDE = 'app.update.url.override'
+ PREF_DISABLED_ADDONS = 'extensions.disabledAddons'
+
+ def __init__(self, marionette_getter):
+ BaseLib.__init__(self, marionette_getter)
+
+ self.app_info = AppInfo(marionette_getter)
+ self.prefs = Preferences(marionette_getter)
+
+ self._update_channel = UpdateChannel(marionette_getter)
+ self._mar_channels = MARChannels(marionette_getter)
+ self._active_update = ActiveUpdate(marionette_getter)
+
+ @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
+ """
+ return {
+ 'buildid': self.app_info.appBuildID,
+ 'channel': self.update_channel.channel,
+ 'disabled_addons': self.prefs.get_pref(self.PREF_DISABLED_ADDONS),
+ 'locale': self.app_info.locale,
+ 'mar_channels': self.mar_channels.channels,
+ 'url_aus': self.get_update_url(True),
+ '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.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):
+ """ Holds a reference to an :class:`UpdateChannel` object."""
+ return self._update_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_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.prefs.get_pref(self.PREF_APP_UPDATE_URL_OVERRIDE)
+
+ if not url:
+ url = self.prefs.get_pref(self.PREF_APP_UPDATE_URL)
+
+ # get the next two prefs from the default branch
+ dist = self.prefs.get_pref(self.PREF_APP_DISTRIBUTION, True) or 'default'
+ dist_version = self.prefs.get_pref(self.PREF_APP_DISTRIBUTION_VERSION,
+ True) or 'default'
+
+ # Not all placeholders are getting replaced correctly by formatURL
+ url = url.replace('%PRODUCT%', self.app_info.name)
+ url = url.replace('%BUILD_ID%', self.app_info.appBuildID)
+ url = url.replace('%BUILD_TARGET%', self.app_info.OS + '_' + self.ABI)
+ url = url.replace('%OS_VERSION%', self.os_version)
+ url = url.replace('%CHANNEL%', self.update_channel.channel)
+ url = url.replace('%DISTRIBUTION%', dist)
+ url = url.replace('%DISTRIBUTION_VERSION%', dist_version)
+
+ url = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ return Services.urlFormatter.formatURL(arguments[0]);
+ """, script_args=[url])
+
+ if force:
+ if '?' in url:
+ url += '&'
+ else:
+ url += '?'
+ url += 'force=1'
+
+ return url
+
+
+class UpdateChannel(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, marionette_getter):
+ BaseLib.__init__(self, marionette_getter)
+
+ self.prefs = Preferences(marionette_getter)
+
+ 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;
+ """)
+
+ @property
+ def file_contents(self):
+ """The contents of the channel-prefs.js file."""
+ with open(self.file_path) as f:
+ return f.read()
+
+ @property
+ def channel(self):
+ """The name of the update channel as stored in the
+ app.update.channel pref."""
+ return self.prefs.get_pref('app.update.channel', True)
+
+ @property
+ def default_channel(self):
+ """Get the default update channel
+
+ :returns: Current default update channel
+ """
+ matches = re.search(self.REGEX_UPDATE_CHANNEL, self.file_contents).groups()
+ assert len(matches) == 2, 'Update channel value has been found'
+
+ return matches[1]
+
+ @default_channel.setter
+ def default_channel(self, channel):
+ """Set default update channel.
+
+ :param channel: New default update channel
+ """
+ assert channel, 'Update channel has been specified'
+ new_content = re.sub(
+ self.REGEX_UPDATE_CHANNEL, r'\g<1>' + channel, self.file_contents)
+ with open(self.file_path, 'w') as f:
+ f.write(new_content)
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/api/utils.py
@@ -0,0 +1,93 @@
+# 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 remove_perms(self, host, permission):
+ """Remove 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])
+
+ 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.')
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/base.py
@@ -0,0 +1,23 @@
+# 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_getter):
+ if not callable(marionette_getter):
+ raise TypeError('Invalid callback for "marionette_getter": %s' % marionette_getter)
+
+ self._marionette = None
+ self._marionette_getter = marionette_getter
+
+ @property
+ def marionette(self):
+ if self._marionette is None:
+ self._marionette = self._marionette_getter()
+ return self._marionette
+
+ def get_marionette(self):
+ return self.marionette
new file mode 100644
--- /dev/null
+++ b/testing/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.get_marionette)
+ setattr(cls, tag, prop)
+ func(cls, *args, **kwargs)
+ return prop
+ return _
new file mode 100644
--- /dev/null
+++ b/testing/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
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/testcases/__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_puppeteer.testcases.base import FirefoxTestCase
+from firefox_puppeteer.testcases.update import UpdateTestCase
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/testcases/base.py
@@ -0,0 +1,89 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette import MarionetteTestCase
+
+from firefox_puppeteer import Puppeteer
+from firefox_puppeteer.ui.browser.window import BrowserWindow
+
+
+class FirefoxTestCase(MarionetteTestCase, Puppeteer):
+ """Base testcase class for Firefox Desktop tests.
+
+ It enhances the Marionette testcase by inserting the Puppeteer mixin class,
+ so Firefox specific API modules are exposed to test scope.
+ """
+ def __init__(self, *args, **kwargs):
+ MarionetteTestCase.__init__(self, *args, **kwargs)
+
+ def _check_and_fix_leaked_handles(self):
+ handle_count = len(self.marionette.window_handles)
+
+ try:
+ self.assertEqual(handle_count, self._start_handle_count,
+ 'A test must not leak window handles. This test started with '
+ '%s open top level browsing contexts, but ended with %s.' %
+ (self._start_handle_count, handle_count))
+ 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.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.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, flags=None):
+ """Restart Firefox and re-initialize data.
+
+ :param flags: Specific restart flags for Firefox
+ """
+ # TODO: Bug 1148220 Marionette's in_app restart has to send 'quit-application-requested'
+ # observer notification before an in_app restart
+ self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Components.interfaces.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+ """)
+ self.marionette.restart(in_app=True)
+
+ # Marionette doesn't keep the former context, so restore to chrome
+ self.marionette.set_context('chrome')
+
+ # Ensure that we always have a valid browser instance available
+ self.browser = self.windows.switch_to(lambda win: type(win) is BrowserWindow)
+
+ def setUp(self, *args, **kwargs):
+ MarionetteTestCase.setUp(self, *args, **kwargs)
+ Puppeteer.set_marionette(self, self.marionette)
+
+ self._start_handle_count = len(self.marionette.window_handles)
+ self.marionette.set_context('chrome')
+
+ self.browser = self.windows.current
+ self.browser.focus()
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ # Ensure that we have a default page opened
+ self.marionette.navigate(self.prefs.get_pref('browser.newtab.url'))
+
+ def tearDown(self, *args, **kwargs):
+ self.marionette.set_context('chrome')
+
+ try:
+ self.prefs.restore_all_prefs()
+
+ # 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:
+ MarionetteTestCase.tearDown(self, *args, **kwargs)
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/testcases/update.py
@@ -0,0 +1,421 @@
+# 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 pprint
+from datetime import datetime
+
+from marionette_driver import Wait
+
+from firefox_puppeteer.api.prefs import Preferences
+from firefox_puppeteer.api.software_update import SoftwareUpdate
+from firefox_puppeteer.testcases import FirefoxTestCase
+from firefox_puppeteer.ui.update_wizard import UpdateWizardDialog
+
+
+class UpdateTestCase(FirefoxTestCase):
+
+ TIMEOUT_UPDATE_APPLY = 300
+ TIMEOUT_UPDATE_CHECK = 30
+ TIMEOUT_UPDATE_DOWNLOAD = 360
+
+ # 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):
+ FirefoxTestCase.__init__(self, *args, **kwargs)
+
+ self.target_buildid = kwargs.pop('update_target_buildid')
+ self.target_version = kwargs.pop('update_target_version')
+
+ self.update_channel = kwargs.pop('update_channel')
+ self.default_update_channel = None
+
+ self.update_mar_channels = set(kwargs.pop('update_mar_channels'))
+ self.default_mar_channels = None
+
+ self.updates = []
+
+ def setUp(self, is_fallback=False):
+ FirefoxTestCase.setUp(self)
+
+ self.software_update = SoftwareUpdate(lambda: self.marionette)
+ self.download_duration = None
+
+ # Bug 604364 - Preparation to test multiple update steps
+ self.current_update_index = 0
+
+ self.staging_directory = self.software_update.staging_directory
+
+ # If requested modify the default update channel. It will be active
+ # after the next restart of the application
+ # Bug 1142805 - Modify file via Python directly
+ if self.update_channel:
+ # Backup the original content and the path of the channel-prefs.js file
+ self.default_update_channel = {
+ 'content': self.software_update.update_channel.file_contents,
+ 'path': self.software_update.update_channel.file_path,
+ }
+ self.software_update.update_channel.default_channel = self.update_channel
+
+ # If requested modify the list of allowed MAR channels
+ # Bug 1142805 - Modify file via Python directly
+ if self.update_mar_channels:
+ # Backup the original content and the path of the update-settings.ini file
+ self.default_mar_channels = {
+ 'content': self.software_update.mar_channels.config_file_contents,
+ 'path': self.software_update.mar_channels.config_file_path,
+ }
+ self.software_update.mar_channels.add_channels(self.update_mar_channels)
+
+ # Bug 1142805 - Until we don't modify the channel-prefs.js and update-settings.ini
+ # files before Firefox gets started, a restart of Firefox is necessary to
+ # accept the new update channel.
+ self.restart()
+
+ # Dictionary which holds the information for each update
+ self.updates = [{
+ 'build_pre': self.software_update.build_info,
+ 'build_post': None,
+ 'fallback': is_fallback,
+ 'patch': {},
+ 'success': False,
+ }]
+
+ self.assertEqual(self.software_update.update_channel.default_channel,
+ self.software_update.update_channel.channel)
+
+ 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)))
+
+ # 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])
+
+ # Print results for now until we have treeherder integration
+ output = pprint.pformat(self.updates)
+ self.logger.info('Update test results: \n{}'.format(output))
+
+ finally:
+ FirefoxTestCase.tearDown(self)
+
+ self.restore_config_files()
+
+ @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):
+ self.updates[self.current_update_index]['build_post'] = self.software_update.build_info
+
+ about_window = self.browser.open_about_window()
+ try:
+ update_available = self.check_for_updates(about_window)
+
+ # No further updates should be offered now with the same update type
+ if update_available:
+ about_window.download(wait_for_finish=False)
+
+ self.assertNotEqual(self.software_update.active_update.type,
+ self.updates[self.current_update_index].type)
+
+ # Check that the update has been applied correctly
+ update = self.updates[self.current_update_index]
+
+ # The upgraded version should be identical with the version given by
+ # the update and we shouldn't have run a downgrade
+ check = self.marionette.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ return Services.vc.compare(arguments[0], arguments[1]);
+ """, script_args=[update['build_post']['version'], update['build_pre']['version']])
+
+ self.assertGreaterEqual(check, 0,
+ 'The version of the upgraded build is higher or equal')
+
+ # If a target version has been specified, check if it matches the updated build
+ if self.target_version:
+ self.assertEqual(update['build_post']['version'], self.target_version)
+
+ # The post buildid should be identical with the buildid contained in the patch
+ self.assertEqual(update['build_post']['buildid'], update['patch']['buildid'])
+
+ # If a target buildid has been specified, check if it matches the updated build
+ if self.target_buildid:
+ self.assertEqual(update['build_post']['buildid'], self.target_buildid)
+
+ # An upgrade should not change the builds locale
+ self.assertEqual(update['build_post']['locale'], update['build_pre']['locale'])
+
+ # Check that no application-wide add-ons have been disabled
+ self.assertEqual(update['build_post']['disabled_addons'],
+ update['build_pre']['disabled_addons'])
+
+ update['success'] = True
+
+ finally:
+ about_window.close()
+
+ 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`.
+ """
+ prefs = Preferences(lambda: self.marionette)
+ prefs.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.updates_found_billboard,
+ dialog.wizard.error_patching,
+ ]:
+ dialog.select_next_page()
+
+ # If incompatible add-on are installed, skip over the wizard page
+ 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:
+ prefs.restore_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))
+
+ # If there are incompatible addons, handle the update via the old software update dialog
+ if window.deck.selected_panel == window.deck.apply_billboard:
+ # Clicking the update button will open the old update wizard dialog
+ dialog = self.browser.open_window(
+ callback=lambda _: window.deck.update_button.click(),
+ expected_window_class=UpdateWizardDialog
+ )
+ Wait(self.marionette).until(
+ lambda _: dialog.wizard.selected_panel == dialog.wizard.updates_found_basic)
+
+ download_via_update_wizard(dialog)
+ dialog.close()
+
+ return
+
+ 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.updates[self.current_update_index]['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):
+ # The update wizard dialog opens automatically after the restart
+ dialog = self.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.updates[self.current_update_index]['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(about_window)
+ self.wait_for_update_applied(about_window)
+
+ finally:
+ if about_window:
+ self.updates[self.current_update_index]['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)
+ dialog.close()
+
+ finally:
+ self.updates[self.current_update_index]['patch'] = self.patch_info
+
+ # Restart Firefox to apply the update
+ self.restart()
+
+ def restore_config_files(self):
+ # Reset channel-prefs.js file if modified
+ try:
+ if self.default_update_channel:
+ path = self.default_update_channel['path']
+ self.logger.info('Restoring channel defaults for: {}'.format(path))
+ with open(path, 'w') as f:
+ f.write(self.default_update_channel['content'])
+ except IOError:
+ self.logger.error('Failed to reset the default update channel.',
+ exc_info=True)
+
+ # Reset update-settings.ini file if modified
+ try:
+ if self.default_mar_channels:
+ path = self.default_mar_channels['path']
+ self.logger.info('Restoring mar channel defaults for: {}'.format(path))
+ with open(path, 'w') as f:
+ f.write(self.default_mar_channels['content'])
+ except IOError:
+ self.logger.error('Failed to reset the default mar channels.',
+ exc_info=True)
+
+ 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.')
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py
@@ -0,0 +1,205 @@
+# 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_lib import UIBaseLib
+
+
+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,
+ 'applyBillboard': ApplyBillboardPanel,
+ '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)(lambda: 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 apply_billboard(self):
+ """The :class:`ApplyBillboardPanel` instance for the apply billboard panel.
+
+ :returns: :class:`ApplyBillboardPanel` instance.
+ """
+ return self._create_panel_for_id('applyBillboard')
+
+ @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_attribute('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 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')
+
+
+class ApplyBillboardPanel(Panel):
+
+ @property
+ def button(self):
+ """The DOM element which represents the Apply Billboard button.
+
+ :returns: Reference to the button element.
+ """
+ return self.element.find_element(By.ID, 'applyButtonBillboard')
+
+
+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
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.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 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',
+ ]
+
+ def __init__(self, *args, **kwargs):
+ BaseWindow.__init__(self, *args, **kwargs)
+
+ @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(lambda: self.marionette, self, deck)
+
+
+Windows.register_window(AboutWindow.window_type, AboutWindow)
new file mode 100644
--- /dev/null
+++ b/testing/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/.
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py
@@ -0,0 +1,385 @@
+# 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 import DOMElement
+from firefox_puppeteer.api.security import Security
+from firefox_puppeteer.ui_base_lib import UIBaseLib
+
+
+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(lambda: 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(lambda: 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_attribute('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.get_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)
+
+ 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.contentWindowAsCPOW;
+ if (!win) {
+ return null;
+ }
+ return win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .outerWindowID.toString();
+ """, script_args=[tab_element])
+
+ return handle
+
+
+class Tab(UIBaseLib):
+ """Wraps a tab DOM element."""
+
+ def __init__(self, marionette_getter, window, element):
+ UIBaseLib.__init__(self, marionette_getter, window, element)
+
+ self._security = Security(lambda: 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`.
+ """
+ def get_handle(_):
+ self._handle = TabBar.get_handle_for_tab(self.marionette, self.element)
+ return self._handle is not None
+
+ # With e10s enabled, contentWindowAsCPOW isn't available right away.
+ if self._handle is None:
+ Wait(self.marionette).until(get_handle)
+ 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`.
+ """
+ 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.get_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)
+
+ # 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)
+
+ 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))
new file mode 100644
--- /dev/null
+++ b/testing/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py
@@ -0,0 +1,630 @@
+# 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_lib 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):
+ UIBaseLib.__init__(self, *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(lambda: 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):
+ UIBaseLib.__init__(self, *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(lambda: 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.urlbar.get_attribute('value') == '')
+
+ 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.get_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)
+
+ 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(lambda: 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):
+ self.identity_box.click()
+ Wait(self.marionette).until(lambda _: self.identity_popup.is_open)
+
+ @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.get_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.
+