Bug 1212609 - Move all python packages for firefox ui tests into mozilla-central. r=ato, r=ted
authorHenrik Skupin <mail@hskupin.info>
Mon, 21 Dec 2015 13:05:03 +0100
changeset 317083 40fa8a5cb4260317686352e95889fb71204bc3da
parent 317082 bb5becd378f40a9be14e4e635d7034f2835fc7b5
child 317084 05086686c328162f2a965a5ddd1ba27f46df4b95
push id8642
push userarmenzg@mozilla.com
push dateTue, 22 Dec 2015 17:52:25 +0000
reviewersato, ted
bugs1212609
milestone46.0a1
Bug 1212609 - Move all python packages for firefox ui tests into mozilla-central. r=ato, r=ted
testing/firefox-ui/harness/MANIFEST.in
testing/firefox-ui/harness/firefox_ui_harness/__init__.py
testing/firefox-ui/harness/firefox_ui_harness/arguments/__init__.py
testing/firefox-ui/harness/firefox_ui_harness/arguments/base.py
testing/firefox-ui/harness/firefox_ui_harness/arguments/update.py
testing/firefox-ui/harness/firefox_ui_harness/cli_functional.py
testing/firefox-ui/harness/firefox_ui_harness/cli_update.py
testing/firefox-ui/harness/firefox_ui_harness/decorators.py
testing/firefox-ui/harness/firefox_ui_harness/runners/__init__.py
testing/firefox-ui/harness/firefox_ui_harness/runners/base.py
testing/firefox-ui/harness/firefox_ui_harness/runners/update.py
testing/firefox-ui/harness/requirements.txt
testing/firefox-ui/harness/setup.py
testing/firefox-ui/tests/MANIFEST.in
testing/firefox-ui/tests/firefox_ui_tests/__init__.py
testing/firefox-ui/tests/firefox_ui_tests/functional/keyboard_shortcuts/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/functional/keyboard_shortcuts/test_browser_window.py
testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_access_locationbar.py
testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_escape_autocomplete.py
testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_favicon_in_autocomplete.py
testing/firefox-ui/tests/firefox_ui_tests/functional/locationbar/test_suggest_bookmarks.py
testing/firefox-ui/tests/firefox_ui_tests/functional/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/functional/private_browsing/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/functional/private_browsing/test_about_private_browsing.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_dv_certificate.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_enable_privilege.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_ev_certificate.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_mixed_content_page.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_mixed_script_content_blocking.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_no_certificate.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_safe_browsing_notification.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_safe_browsing_warning_pages.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_security_notification.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_ssl_disabled_error_page.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_ssl_status_after_restart.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_submit_unencrypted_info_warning.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_unknown_issuer.py
testing/firefox-ui/tests/firefox_ui_tests/functional/security/test_untrusted_connection_error_page.py
testing/firefox-ui/tests/firefox_ui_tests/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_about_window.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_appinfo.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_l10n.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_menubar.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_page_info_window.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_places.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_prefs.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_security.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_software_update.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_tabbar.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_toolbars.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_update_wizard.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_utils.py
testing/firefox-ui/tests/firefox_ui_tests/puppeteer/test_windows.py
testing/firefox-ui/tests/firefox_ui_tests/resources/cookies/cookie_single.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_community.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_contribute.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_governance.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_grants.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_mission.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_organizations.html
testing/firefox-ui/tests/firefox_ui_tests/resources/layout/mozilla_projects.html
testing/firefox-ui/tests/firefox_ui_tests/resources/private_browsing/about.html
testing/firefox-ui/tests/firefox_ui_tests/resources/security/enable_privilege.html
testing/firefox-ui/tests/firefox_ui_tests/update/direct/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/update/direct/test_direct_update.py
testing/firefox-ui/tests/firefox_ui_tests/update/fallback/manifest.ini
testing/firefox-ui/tests/firefox_ui_tests/update/fallback/test_fallback_update.py
testing/firefox-ui/tests/setup.py
testing/puppeteer/firefox/MANIFEST.in
testing/puppeteer/firefox/docs/Makefile
testing/puppeteer/firefox/docs/api/appinfo.rst
testing/puppeteer/firefox/docs/api/keys.rst
testing/puppeteer/firefox/docs/api/l10n.rst
testing/puppeteer/firefox/docs/api/places.rst
testing/puppeteer/firefox/docs/api/prefs.rst
testing/puppeteer/firefox/docs/api/security.rst
testing/puppeteer/firefox/docs/api/software_update.rst
testing/puppeteer/firefox/docs/api/utils.rst
testing/puppeteer/firefox/docs/conf.py
testing/puppeteer/firefox/docs/index.rst
testing/puppeteer/firefox/docs/make.bat
testing/puppeteer/firefox/docs/ui/about_window/window.rst
testing/puppeteer/firefox/docs/ui/browser/tabbar.rst
testing/puppeteer/firefox/docs/ui/browser/toolbars.rst
testing/puppeteer/firefox/docs/ui/browser/window.rst
testing/puppeteer/firefox/docs/ui/menu.rst
testing/puppeteer/firefox/docs/ui/pageinfo/window.rst
testing/puppeteer/firefox/docs/ui/update_wizard/dialog.rst
testing/puppeteer/firefox/docs/ui/windows.rst
testing/puppeteer/firefox/firefox_puppeteer/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/api/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/api/appinfo.py
testing/puppeteer/firefox/firefox_puppeteer/api/keys.py
testing/puppeteer/firefox/firefox_puppeteer/api/l10n.py
testing/puppeteer/firefox/firefox_puppeteer/api/places.py
testing/puppeteer/firefox/firefox_puppeteer/api/prefs.py
testing/puppeteer/firefox/firefox_puppeteer/api/security.py
testing/puppeteer/firefox/firefox_puppeteer/api/software_update.py
testing/puppeteer/firefox/firefox_puppeteer/api/utils.py
testing/puppeteer/firefox/firefox_puppeteer/base.py
testing/puppeteer/firefox/firefox_puppeteer/decorators.py
testing/puppeteer/firefox/firefox_puppeteer/errors.py
testing/puppeteer/firefox/firefox_puppeteer/testcases/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/testcases/base.py
testing/puppeteer/firefox/firefox_puppeteer/testcases/update.py
testing/puppeteer/firefox/firefox_puppeteer/ui/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py
testing/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py
testing/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py
testing/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py
testing/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py
testing/puppeteer/firefox/firefox_puppeteer/ui/menu.py
testing/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py
testing/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py
testing/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py
testing/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py
testing/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py
testing/puppeteer/firefox/firefox_puppeteer/ui/windows.py
testing/puppeteer/firefox/firefox_puppeteer/ui_base_lib.py
testing/puppeteer/firefox/requirements.txt
testing/puppeteer/firefox/setup.py
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
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
new file mode 100644
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.
+