summaryrefslogtreecommitdiffstats
path: root/testing/marionette/client/marionette_driver
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/client/marionette_driver')
-rw-r--r--testing/marionette/client/marionette_driver/__init__.py26
-rw-r--r--testing/marionette/client/marionette_driver/addons.py70
-rw-r--r--testing/marionette/client/marionette_driver/by.py27
-rw-r--r--testing/marionette/client/marionette_driver/date_time_value.py49
-rw-r--r--testing/marionette/client/marionette_driver/decorators.py69
-rw-r--r--testing/marionette/client/marionette_driver/errors.py179
-rw-r--r--testing/marionette/client/marionette_driver/expected.py311
-rw-r--r--testing/marionette/client/marionette_driver/geckoinstance.py467
-rw-r--r--testing/marionette/client/marionette_driver/gestures.py93
-rw-r--r--testing/marionette/client/marionette_driver/keys.py84
-rw-r--r--testing/marionette/client/marionette_driver/localization.py54
-rw-r--r--testing/marionette/client/marionette_driver/marionette.py2153
-rw-r--r--testing/marionette/client/marionette_driver/selection.py227
-rw-r--r--testing/marionette/client/marionette_driver/timeout.py98
-rw-r--r--testing/marionette/client/marionette_driver/transport.py300
-rw-r--r--testing/marionette/client/marionette_driver/wait.py167
16 files changed, 4374 insertions, 0 deletions
diff --git a/testing/marionette/client/marionette_driver/__init__.py b/testing/marionette/client/marionette_driver/__init__.py
new file mode 100644
index 000000000..d947d9c27
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/__init__.py
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+__version__ = '2.2.0'
+
+from marionette_driver import (
+ addons,
+ by,
+ date_time_value,
+ decorators,
+ errors,
+ expected,
+ geckoinstance,
+ gestures,
+ keys,
+ localization,
+ marionette,
+ selection,
+ wait,
+)
+from marionette_driver.by import By
+from marionette_driver.date_time_value import DateTimeValue
+from marionette_driver.gestures import smooth_scroll, pinch
+from marionette_driver.marionette import Actions
+from marionette_driver.wait import Wait
diff --git a/testing/marionette/client/marionette_driver/addons.py b/testing/marionette/client/marionette_driver/addons.py
new file mode 100644
index 000000000..0a8baef4f
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/addons.py
@@ -0,0 +1,70 @@
+# 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 . import errors
+
+__all__ = ["Addons", "AddonInstallException"]
+
+
+class AddonInstallException(errors.MarionetteException):
+ pass
+
+
+class Addons(object):
+ """An API for installing and inspecting addons during Gecko
+ runtime. This is a partially implemented wrapper around Gecko's
+ `AddonManager API`_.
+
+ For example::
+
+ from marionette_driver.addons import Addons
+ addons = Addons(marionette)
+ addons.install("/path/to/extension.xpi")
+
+ .. _AddonManager API: https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager
+
+ """
+ def __init__(self, marionette):
+ self._mn = marionette
+
+ def install(self, path, temp=False):
+ """Install a Firefox addon.
+
+ If the addon is restartless, it can be used right away. Otherwise
+ a restart using :func:`~marionette_driver.marionette.Marionette.restart`
+ will be needed.
+
+ :param path: A file path to the extension to be installed.
+ :param temp: Install a temporary addon. Temporary addons will
+ automatically be uninstalled on shutdown and do not need
+ to be signed, though they must be restartless.
+
+ :returns: The addon ID string of the newly installed addon.
+
+ :raises: :exc:`AddonInstallException`
+
+ """
+ body = {"path": path, "temporary": temp}
+ try:
+ return self._mn._send_message("addon:install", body, key="value")
+ except errors.UnknownException as e:
+ raise AddonInstallException(e)
+
+ def uninstall(self, addon_id):
+ """Uninstall a Firefox addon.
+
+ If the addon is restartless, it will be uninstalled right away.
+ Otherwise a restart using :func:`~marionette_driver.marionette.Marionette.restart`
+ will be needed.
+
+ If the call to uninstall is resulting in a `ScriptTimeoutException`,
+ an invalid ID is likely being passed in. Unfortunately due to
+ AddonManager's implementation, it's hard to retrieve this error from
+ Python.
+
+ :param addon_id: The addon ID string to uninstall.
+
+ """
+ body = {"id": addon_id}
+ self._mn._send_message("addon:uninstall", body)
diff --git a/testing/marionette/client/marionette_driver/by.py b/testing/marionette/client/marionette_driver/by.py
new file mode 100644
index 000000000..8232be145
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/by.py
@@ -0,0 +1,27 @@
+# Copyright 2008-2009 WebDriver committers
+# Copyright 2008-2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class By(object):
+ ID = "id"
+ XPATH = "xpath"
+ LINK_TEXT = "link text"
+ PARTIAL_LINK_TEXT = "partial link text"
+ NAME = "name"
+ TAG_NAME = "tag name"
+ CLASS_NAME = "class name"
+ CSS_SELECTOR = "css selector"
+ ANON_ATTRIBUTE = "anon attribute"
+ ANON = "anon"
diff --git a/testing/marionette/client/marionette_driver/date_time_value.py b/testing/marionette/client/marionette_driver/date_time_value.py
new file mode 100644
index 000000000..35c541dc0
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/date_time_value.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/.
+
+
+class DateTimeValue(object):
+ """
+ Interface for setting the value of HTML5 "date" and "time" input elements.
+
+ Simple usage example:
+
+ ::
+
+ element = marionette.find_element(By.ID, "date-test")
+ dt_value = DateTimeValue(element)
+ dt_value.date = datetime(1998, 6, 2)
+
+ """
+
+ def __init__(self, element):
+ self.element = element
+
+ @property
+ def date(self):
+ """
+ Retrieve the element's string value
+ """
+ return self.element.get_attribute('value')
+
+ # As per the W3C "date" element specification
+ # (http://dev.w3.org/html5/markup/input.date.html), this value is formatted
+ # according to RFC 3339: http://tools.ietf.org/html/rfc3339#section-5.6
+ @date.setter
+ def date(self, date_value):
+ self.element.send_keys(date_value.strftime('%Y-%m-%d'))
+
+ @property
+ def time(self):
+ """
+ Retrieve the element's string value
+ """
+ return self.element.get_attribute('value')
+
+ # As per the W3C "time" element specification
+ # (http://dev.w3.org/html5/markup/input.time.html), this value is formatted
+ # according to RFC 3339: http://tools.ietf.org/html/rfc3339#section-5.6
+ @time.setter
+ def time(self, time_value):
+ self.element.send_keys(time_value.strftime('%H:%M:%S'))
diff --git a/testing/marionette/client/marionette_driver/decorators.py b/testing/marionette/client/marionette_driver/decorators.py
new file mode 100644
index 000000000..f65152471
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/decorators.py
@@ -0,0 +1,69 @@
+# 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
+import socket
+
+
+def _find_marionette_in_args(*args, **kwargs):
+ try:
+ m = [a for a in args + tuple(kwargs.values()) if hasattr(a, 'session')][0]
+ except IndexError:
+ print("Can only apply decorator to function using a marionette object")
+ raise
+ return m
+
+
+def do_process_check(func):
+ """Decorator which checks the process status after the function has run."""
+ @wraps(func)
+ def _(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except (socket.error, socket.timeout):
+ # In case of socket failures which will also include crashes of the
+ # application, make sure to handle those correctly.
+ m = _find_marionette_in_args(*args, **kwargs)
+ m._handle_socket_failure()
+
+ return _
+
+
+def uses_marionette(func):
+ """Decorator which creates a marionette session and deletes it
+ afterwards if one doesn't already exist.
+ """
+ @wraps(func)
+ def _(*args, **kwargs):
+ m = _find_marionette_in_args(*args, **kwargs)
+ delete_session = False
+ if not m.session:
+ delete_session = True
+ m.start_session()
+
+ m.set_context(m.CONTEXT_CHROME)
+ ret = func(*args, **kwargs)
+
+ if delete_session:
+ m.delete_session()
+
+ return ret
+ return _
+
+
+def using_context(context):
+ """Decorator which allows a function to execute in certain scope
+ using marionette.using_context functionality and returns to old
+ scope once the function exits.
+ :param context: Either 'chrome' or 'content'.
+ """
+ def wrap(func):
+ @wraps(func)
+ def inner(*args, **kwargs):
+ m = _find_marionette_in_args(*args, **kwargs)
+ with m.using_context(context):
+ return func(*args, **kwargs)
+
+ return inner
+ return wrap
diff --git a/testing/marionette/client/marionette_driver/errors.py b/testing/marionette/client/marionette_driver/errors.py
new file mode 100644
index 000000000..8fb1d564e
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/errors.py
@@ -0,0 +1,179 @@
+# 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 traceback
+
+
+class MarionetteException(Exception):
+
+ """Raised when a generic non-recoverable exception has occured."""
+
+ status = "webdriver error"
+
+ def __init__(self, message=None, cause=None, stacktrace=None):
+ """Construct new MarionetteException instance.
+
+ :param message: An optional exception message.
+
+ :param cause: An optional tuple of three values giving
+ information about the root exception cause. Expected
+ tuple values are (type, value, traceback).
+
+ :param stacktrace: Optional string containing a stacktrace
+ (typically from a failed JavaScript execution) that will
+ be displayed in the exception's string representation.
+
+ """
+ self.cause = cause
+ self.stacktrace = stacktrace
+
+ super(MarionetteException, self).__init__(message)
+
+ def __str__(self):
+ msg = str(self.message)
+ tb = None
+
+ if self.cause:
+ if type(self.cause) is tuple:
+ msg += ", caused by {0!r}".format(self.cause[0])
+ tb = self.cause[2]
+ else:
+ msg += ", caused by {}".format(self.cause)
+ if self.stacktrace:
+ st = "".join(["\t{}\n".format(x)
+ for x in self.stacktrace.splitlines()])
+ msg += "\nstacktrace:\n{}".format(st)
+
+ if tb:
+ msg += ': ' + "".join(traceback.format_tb(tb))
+
+ return msg
+
+
+class ElementNotSelectableException(MarionetteException):
+ status = "element not selectable"
+
+
+class ElementClickInterceptedException(MarionetteException):
+ status = "element click intercepted"
+
+
+class InsecureCertificateException(MarionetteException):
+ status = "insecure certificate"
+
+
+class InvalidArgumentException(MarionetteException):
+ status = "invalid argument"
+
+
+class InvalidSessionIdException(MarionetteException):
+ status = "invalid session id"
+
+
+class TimeoutException(MarionetteException):
+ status = "timeout"
+
+
+class JavascriptException(MarionetteException):
+ status = "javascript error"
+
+
+class NoSuchElementException(MarionetteException):
+ status = "no such element"
+
+
+class NoSuchWindowException(MarionetteException):
+ status = "no such window"
+
+
+class StaleElementException(MarionetteException):
+ status = "stale element reference"
+
+
+class ScriptTimeoutException(MarionetteException):
+ status = "script timeout"
+
+
+class ElementNotVisibleException(MarionetteException):
+ """Deprecated. Will be removed with the release of Firefox 54."""
+
+ status = "element not visible"
+
+ def __init__(self,
+ message="Element is not currently visible and may not be manipulated",
+ stacktrace=None, cause=None):
+ super(ElementNotVisibleException, self).__init__(
+ message, cause=cause, stacktrace=stacktrace)
+
+
+class ElementNotAccessibleException(MarionetteException):
+ status = "element not accessible"
+
+
+class ElementNotInteractableException(MarionetteException):
+ status = "element not interactable"
+
+
+class NoSuchFrameException(MarionetteException):
+ status = "no such frame"
+
+
+class InvalidElementStateException(MarionetteException):
+ status = "invalid element state"
+
+
+class NoAlertPresentException(MarionetteException):
+ status = "no such alert"
+
+
+class InvalidCookieDomainException(MarionetteException):
+ status = "invalid cookie domain"
+
+
+class UnableToSetCookieException(MarionetteException):
+ status = "unable to set cookie"
+
+
+class InvalidElementCoordinates(MarionetteException):
+ status = "invalid element coordinates"
+
+
+class InvalidSelectorException(MarionetteException):
+ status = "invalid selector"
+
+
+class MoveTargetOutOfBoundsException(MarionetteException):
+ status = "move target out of bounds"
+
+
+class SessionNotCreatedException(MarionetteException):
+ status = "session not created"
+
+
+class UnexpectedAlertOpen(MarionetteException):
+ status = "unexpected alert open"
+
+
+class UnknownCommandException(MarionetteException):
+ status = "unknown command"
+
+
+class UnknownException(MarionetteException):
+ status = "unknown error"
+
+
+class UnsupportedOperationException(MarionetteException):
+ status = "unsupported operation"
+
+
+es_ = [e for e in locals().values() if type(e) == type and issubclass(e, MarionetteException)]
+by_string = {e.status: e for e in es_}
+
+
+def lookup(identifier):
+ """Finds error exception class by associated Selenium JSON wire
+ protocol number code, or W3C WebDriver protocol string.
+
+ """
+ return by_string.get(identifier, MarionetteException)
diff --git a/testing/marionette/client/marionette_driver/expected.py b/testing/marionette/client/marionette_driver/expected.py
new file mode 100644
index 000000000..d8c47e708
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/expected.py
@@ -0,0 +1,311 @@
+# 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 errors
+import types
+
+from marionette import HTMLElement
+
+"""This file provides a set of expected conditions for common use
+cases when writing Marionette tests.
+
+The conditions rely on explicit waits that retries conditions a number
+of times until they are either successfully met, or they time out.
+
+"""
+
+
+class element_present(object):
+ """Checks that a web element is present in the DOM of the current
+ context. This does not necessarily mean that the element is
+ visible.
+
+ You can select which element to be checked for presence by
+ supplying a locator::
+
+ el = Wait(marionette).until(expected.element_present(By.ID, "foo"))
+
+ Or by using a function/lambda returning an element::
+
+ el = Wait(marionette).\
+ until(expected.element_present(lambda m: m.find_element(By.ID, "foo")))
+
+ :param args: locator or function returning web element
+ :returns: the web element once it is located, or False
+
+ """
+
+ def __init__(self, *args):
+ if len(args) == 1 and isinstance(args[0], types.FunctionType):
+ self.locator = args[0]
+ else:
+ self.locator = lambda m: m.find_element(*args)
+
+ def __call__(self, marionette):
+ return _find(marionette, self.locator)
+
+
+class element_not_present(element_present):
+ """Checks that a web element is not present in the DOM of the current
+ context.
+
+ You can select which element to be checked for lack of presence by
+ supplying a locator::
+
+ r = Wait(marionette).until(expected.element_not_present(By.ID, "foo"))
+
+ Or by using a function/lambda returning an element::
+
+ r = Wait(marionette).\
+ until(expected.element_present(lambda m: m.find_element(By.ID, "foo")))
+
+ :param args: locator or function returning web element
+ :returns: True if element is not present, or False if it is present
+
+ """
+
+ def __init__(self, *args):
+ super(element_not_present, self).__init__(*args)
+
+ def __call__(self, marionette):
+ return not super(element_not_present, self).__call__(marionette)
+
+
+class element_stale(object):
+ """Check that the given element is no longer attached to DOM of the
+ current context.
+
+ This can be useful for waiting until an element is no longer
+ present.
+
+ Sample usage::
+
+ el = marionette.find_element(By.ID, "foo")
+ # ...
+ Wait(marionette).until(expected.element_stale(el))
+
+ :param element: the element to wait for
+ :returns: False if the element is still attached to the DOM, True
+ otherwise
+
+ """
+
+ def __init__(self, element):
+ self.el = element
+
+ def __call__(self, marionette):
+ try:
+ # Calling any method forces a staleness check
+ self.el.is_enabled()
+ return False
+ except errors.StaleElementException:
+ return True
+
+
+class elements_present(object):
+ """Checks that web elements are present in the DOM of the current
+ context. This does not necessarily mean that the elements are
+ visible.
+
+ You can select which elements to be checked for presence by
+ supplying a locator::
+
+ els = Wait(marionette).until(expected.elements_present(By.TAG_NAME, "a"))
+
+ Or by using a function/lambda returning a list of elements::
+
+ els = Wait(marionette).\
+ until(expected.elements_present(lambda m: m.find_elements(By.TAG_NAME, "a")))
+
+ :param args: locator or function returning a list of web elements
+ :returns: list of web elements once they are located, or False
+
+ """
+
+ def __init__(self, *args):
+ if len(args) == 1 and isinstance(args[0], types.FunctionType):
+ self.locator = args[0]
+ else:
+ self.locator = lambda m: m.find_elements(*args)
+
+ def __call__(self, marionette):
+ return _find(marionette, self.locator)
+
+
+class elements_not_present(elements_present):
+ """Checks that web elements are not present in the DOM of the
+ current context.
+
+ You can select which elements to be checked for not being present
+ by supplying a locator::
+
+ r = Wait(marionette).until(expected.elements_not_present(By.TAG_NAME, "a"))
+
+ Or by using a function/lambda returning a list of elements::
+
+ r = Wait(marionette).\
+ until(expected.elements_not_present(lambda m: m.find_elements(By.TAG_NAME, "a")))
+
+ :param args: locator or function returning a list of web elements
+ :returns: True if elements are missing, False if one or more are
+ present
+
+ """
+
+ def __init__(self, *args):
+ super(elements_not_present, self).__init__(*args)
+
+ def __call__(self, marionette):
+ return not super(elements_not_present, self).__call__(marionette)
+
+
+class element_displayed(object):
+ """An expectation for checking that an element is visible.
+
+ Visibility means that the element is not only displayed, but also
+ has a height and width that is greater than 0 pixels.
+
+ Stale elements, meaning elements that have been detached from the
+ DOM of the current context are treated as not being displayed,
+ meaning this expectation is not analogous to the behaviour of
+ calling `is_displayed()` on an `HTMLElement`.
+
+ You can select which element to be checked for visibility by
+ supplying a locator::
+
+ displayed = Wait(marionette).until(expected.element_displayed(By.ID, "foo"))
+
+ Or by supplying an element::
+
+ el = marionette.find_element(By.ID, "foo")
+ displayed = Wait(marionette).until(expected.element_displayed(el))
+
+ :param args: locator or web element
+ :returns: True if element is displayed, False if hidden
+
+ """
+
+ def __init__(self, *args):
+ self.el = None
+ if len(args) == 1 and isinstance(args[0], HTMLElement):
+ self.el = args[0]
+ else:
+ self.locator = lambda m: m.find_element(*args)
+
+ def __call__(self, marionette):
+ if self.el is None:
+ self.el = _find(marionette, self.locator)
+ if not self.el:
+ return False
+ try:
+ return self.el.is_displayed()
+ except errors.StaleElementException:
+ return False
+
+
+class element_not_displayed(element_displayed):
+ """An expectation for checking that an element is not visible.
+
+ Visibility means that the element is not only displayed, but also
+ has a height and width that is greater than 0 pixels.
+
+ Stale elements, meaning elements that have been detached fom the
+ DOM of the current context are treated as not being displayed,
+ meaning this expectation is not analogous to the behaviour of
+ calling `is_displayed()` on an `HTMLElement`.
+
+ You can select which element to be checked for visibility by
+ supplying a locator::
+
+ hidden = Wait(marionette).until(expected.element_not_displayed(By.ID, "foo"))
+
+ Or by supplying an element::
+
+ el = marionette.find_element(By.ID, "foo")
+ hidden = Wait(marionette).until(expected.element_not_displayed(el))
+
+ :param args: locator or web element
+ :returns: True if element is hidden, False if displayed
+
+ """
+
+ def __init__(self, *args):
+ super(element_not_displayed, self).__init__(*args)
+
+ def __call__(self, marionette):
+ return not super(element_not_displayed, self).__call__(marionette)
+
+
+class element_selected(object):
+ """An expectation for checking that the given element is selected.
+
+ :param element: the element to be selected
+ :returns: True if element is selected, False otherwise
+
+ """
+
+ def __init__(self, element):
+ self.el = element
+
+ def __call__(self, marionette):
+ return self.el.is_selected()
+
+
+class element_not_selected(element_selected):
+ """An expectation for checking that the given element is not
+ selected.
+
+ :param element: the element to not be selected
+ :returns: True if element is not selected, False if selected
+
+ """
+
+ def __init__(self, element):
+ super(element_not_selected, self).__init__(element)
+
+ def __call__(self, marionette):
+ return not super(element_not_selected, self).__call__(marionette)
+
+
+class element_enabled(object):
+ """An expectation for checking that the given element is enabled.
+
+ :param element: the element to check if enabled
+ :returns: True if element is enabled, False otherwise
+
+ """
+
+ def __init__(self, element):
+ self.el = element
+
+ def __call__(self, marionette):
+ return self.el.is_enabled()
+
+
+class element_not_enabled(element_enabled):
+ """An expectation for checking that the given element is disabled.
+
+ :param element: the element to check if disabled
+ :returns: True if element is disabled, False if enabled
+
+ """
+
+ def __init__(self, element):
+ super(element_not_enabled, self).__init__(element)
+
+ def __call__(self, marionette):
+ return not super(element_not_enabled, self).__call__(marionette)
+
+
+def _find(marionette, func):
+ el = None
+
+ try:
+ el = func(marionette)
+ except errors.NoSuchElementException:
+ pass
+
+ if el is None:
+ return False
+ return el
diff --git a/testing/marionette/client/marionette_driver/geckoinstance.py b/testing/marionette/client/marionette_driver/geckoinstance.py
new file mode 100644
index 000000000..7e2048187
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -0,0 +1,467 @@
+# 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 sys
+import tempfile
+import time
+
+from copy import deepcopy
+
+import mozversion
+
+from mozprofile import Profile
+from mozrunner import Runner, FennecEmulatorRunner
+
+
+class GeckoInstance(object):
+ required_prefs = {
+ # Increase the APZ content response timeout in tests to 1 minute.
+ # This is to accommodate the fact that test environments tends to be slower
+ # than production environments (with the b2g emulator being the slowest of them
+ # all), resulting in the production timeout value sometimes being exceeded
+ # and causing false-positive test failures. See bug 1176798, bug 1177018,
+ # bug 1210465.
+ "apz.content_response_timeout": 60000,
+
+ # Do not send Firefox health reports to the production server
+ "datareporting.healthreport.documentServerURI": "http://%(server)s/dummy/healthreport/",
+ "datareporting.healthreport.about.reportUrl": "http://%(server)s/dummy/abouthealthreport/",
+
+ # Do not show datareporting policy notifications which can interfer with tests
+ "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
+
+ # Until Bug 1238095 is fixed, we have to enable CPOWs in order
+ # for Marionette tests to work properly.
+ "dom.ipc.cpows.forbid-unsafe-from-browser": False,
+ "dom.ipc.reportProcessHangs": False,
+
+ # No slow script dialogs
+ "dom.max_chrome_script_run_time": 0,
+ "dom.max_script_run_time": 0,
+
+ # Only load extensions from the application and user profile
+ # AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
+ "extensions.autoDisableScopes": 0,
+ "extensions.enabledScopes": 5,
+ # don't block add-ons for e10s
+ "extensions.e10sBlocksEnabling": False,
+ # Disable metadata caching for installed add-ons by default
+ "extensions.getAddons.cache.enabled": False,
+ # Disable intalling any distribution add-ons
+ "extensions.installDistroAddons": False,
+ "extensions.showMismatchUI": False,
+ # Turn off extension updates so they don't bother tests
+ "extensions.update.enabled": False,
+ "extensions.update.notifyUser": False,
+ # Make sure opening about:addons won"t hit the network
+ "extensions.webservice.discoverURL": "http://%(server)s/dummy/discoveryURL",
+
+ # Allow the application to have focus even it runs in the background
+ "focusmanager.testmode": True,
+
+ # Disable useragent updates
+ "general.useragent.updates.enabled": False,
+
+ # Always use network provider for geolocation tests
+ # so we bypass the OSX dialog raised by the corelocation provider
+ "geo.provider.testing": True,
+ # Do not scan Wifi
+ "geo.wifi.scan": False,
+
+ # No hang monitor
+ "hangmonitor.timeout": 0,
+
+ "javascript.options.showInConsole": True,
+ "marionette.defaultPrefs.enabled": True,
+ "media.volume_scale": "0.01",
+
+ # Make sure the disk cache doesn't get auto disabled
+ "network.http.bypass-cachelock-threshold": 200000,
+ # Do not prompt for temporary redirects
+ "network.http.prompt-temp-redirect": False,
+ # Disable speculative connections so they aren"t reported as leaking when they"re
+ # hanging around
+ "network.http.speculative-parallel-limit": 0,
+ # Do not automatically switch between offline and online
+ "network.manage-offline-status": False,
+ # Make sure SNTP requests don't hit the network
+ "network.sntp.pools": "%(server)s",
+
+ # Tests don't wait for the notification button security delay
+ "security.notification_enable_delay": 0,
+
+ # Ensure blocklist updates don't hit the network
+ "services.settings.server": "http://%(server)s/dummy/blocklist/",
+
+ # Disable password capture, so that tests that include forms aren"t
+ # influenced by the presence of the persistent doorhanger notification
+ "signon.rememberSignons": False,
+
+ # Prevent starting into safe mode after application crashes
+ "toolkit.startup.max_resumed_crashes": -1,
+
+ # We want to collect telemetry, but we don't want to send in the results
+ "toolkit.telemetry.server": "https://%(server)s/dummy/telemetry/",
+ }
+
+ def __init__(self, host=None, port=None, bin=None, profile=None, addons=None,
+ app_args=None, symbols_path=None, gecko_log=None, prefs=None,
+ workspace=None, verbose=0):
+ self.runner_class = Runner
+ self.app_args = app_args or []
+ self.runner = None
+ self.symbols_path = symbols_path
+ self.binary = bin
+
+ self.marionette_host = host
+ self.marionette_port = port
+ # Alternative to default temporary directory
+ self.workspace = workspace
+ self.addons = addons
+ # Check if it is a Profile object or a path to profile
+ self.profile = None
+ if isinstance(profile, Profile):
+ self.profile = profile
+ else:
+ self.profile_path = profile
+ self.prefs = prefs
+ self.required_prefs = deepcopy(self.required_prefs)
+ if prefs:
+ self.required_prefs.update(prefs)
+
+ self._gecko_log_option = gecko_log
+ self._gecko_log = None
+ self.verbose = verbose
+
+ @property
+ def gecko_log(self):
+ if self._gecko_log:
+ return self._gecko_log
+
+ path = self._gecko_log_option
+ if path != "-":
+ if path is None:
+ path = "gecko.log"
+ elif os.path.isdir(path):
+ fname = "gecko-{}.log".format(time.time())
+ path = os.path.join(path, fname)
+
+ path = os.path.realpath(path)
+ if os.access(path, os.F_OK):
+ os.remove(path)
+
+ self._gecko_log = path
+ return self._gecko_log
+
+ def _update_profile(self):
+ profile_args = {"preferences": deepcopy(self.required_prefs)}
+ profile_args["preferences"]["marionette.defaultPrefs.port"] = self.marionette_port
+ if self.prefs:
+ profile_args["preferences"].update(self.prefs)
+ if self.verbose:
+ level = "TRACE" if self.verbose >= 2 else "DEBUG"
+ profile_args["preferences"]["marionette.logging"] = level
+ if "-jsdebugger" in self.app_args:
+ profile_args["preferences"].update({
+ "devtools.browsertoolbox.panel": "jsdebugger",
+ "devtools.debugger.remote-enabled": True,
+ "devtools.chrome.enabled": True,
+ "devtools.debugger.prompt-connection": False,
+ "marionette.debugging.clicktostart": True,
+ })
+ if self.addons:
+ profile_args["addons"] = self.addons
+
+ if hasattr(self, "profile_path") and self.profile is None:
+ if not self.profile_path:
+ if self.workspace:
+ profile_args["profile"] = tempfile.mkdtemp(
+ suffix=".mozrunner-{:.0f}".format(time.time()),
+ dir=self.workspace)
+ self.profile = Profile(**profile_args)
+ else:
+ profile_args["path_from"] = self.profile_path
+ profile_name = "{}-{:.0f}".format(
+ os.path.basename(self.profile_path),
+ time.time()
+ )
+ if self.workspace:
+ profile_args["path_to"] = os.path.join(self.workspace,
+ profile_name)
+ self.profile = Profile.clone(**profile_args)
+
+ @classmethod
+ def create(cls, app=None, *args, **kwargs):
+ try:
+ if not app and kwargs["bin"] is not None:
+ app_id = mozversion.get_version(binary=kwargs["bin"])["application_id"]
+ app = app_ids[app_id]
+
+ instance_class = apps[app]
+ except (IOError, KeyError):
+ exc, val, tb = sys.exc_info()
+ msg = 'Application "{0}" unknown (should be one of {1})'
+ raise NotImplementedError, msg.format(app, apps.keys()), tb
+
+ return instance_class(*args, **kwargs)
+
+ def start(self):
+ self._update_profile()
+ self.runner = self.runner_class(**self._get_runner_args())
+ self.runner.start()
+
+ def _get_runner_args(self):
+ process_args = {
+ "processOutputLine": [NullOutput()],
+ }
+
+ if self.gecko_log == "-":
+ process_args["stream"] = sys.stdout
+ else:
+ process_args["logfile"] = self.gecko_log
+
+ env = os.environ.copy()
+
+ # environment variables needed for crashreporting
+ # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
+ env.update({"MOZ_CRASHREPORTER": "1",
+ "MOZ_CRASHREPORTER_NO_REPORT": "1",
+ "MOZ_CRASHREPORTER_SHUTDOWN": "1",
+ })
+
+ return {
+ "binary": self.binary,
+ "profile": self.profile,
+ "cmdargs": ["-no-remote", "-marionette"] + self.app_args,
+ "env": env,
+ "symbols_path": self.symbols_path,
+ "process_args": process_args
+ }
+
+ def close(self, restart=False):
+ if not restart:
+ self.profile = None
+
+ if self.runner:
+ self.runner.stop()
+ self.runner.cleanup()
+
+ def restart(self, prefs=None, clean=True):
+ self.close(restart=True)
+
+ if clean and self.profile:
+ self.profile.cleanup()
+ self.profile = None
+
+ if prefs:
+ self.prefs = prefs
+ else:
+ self.prefs = None
+ self.start()
+
+
+class FennecInstance(GeckoInstance):
+ fennec_prefs = {
+ # Enable output of dump()
+ "browser.dom.window.dump.enabled": True,
+
+ # Disable Android snippets
+ "browser.snippets.enabled": False,
+ "browser.snippets.syncPromo.enabled": False,
+ "browser.snippets.firstrunHomepage.enabled": False,
+
+ # Disable safebrowsing components
+ "browser.safebrowsing.downloads.enabled": False,
+
+ # Do not restore the last open set of tabs if the browser has crashed
+ "browser.sessionstore.resume_from_crash": False,
+
+ # Disable e10s by default
+ "browser.tabs.remote.autostart.1": False,
+ "browser.tabs.remote.autostart.2": False,
+ "browser.tabs.remote.autostart": False,
+
+ # Do not allow background tabs to be zombified, otherwise for tests that
+ # open additional tabs, the test harness tab itself might get unloaded
+ "browser.tabs.disableBackgroundZombification": True,
+ }
+
+ def __init__(self, emulator_binary=None, avd_home=None, avd=None,
+ adb_path=None, serial=None, connect_to_running_emulator=False,
+ package_name=None, *args, **kwargs):
+ super(FennecInstance, self).__init__(*args, **kwargs)
+ self.required_prefs.update(FennecInstance.fennec_prefs)
+
+ self.runner_class = FennecEmulatorRunner
+ # runner args
+ self._package_name = package_name
+ self.emulator_binary = emulator_binary
+ self.avd_home = avd_home
+ self.adb_path = adb_path
+ self.avd = avd
+ self.serial = serial
+ self.connect_to_running_emulator = connect_to_running_emulator
+
+ @property
+ def package_name(self):
+ """
+ Name of app to run on emulator.
+
+ Note that FennecInstance does not use self.binary
+ """
+ if self._package_name is None:
+ self._package_name = "org.mozilla.fennec"
+ user = os.getenv("USER")
+ if user:
+ self._package_name += "_" + user
+ return self._package_name
+
+ def start(self):
+ self._update_profile()
+ self.runner = self.runner_class(**self._get_runner_args())
+ try:
+ if self.connect_to_running_emulator:
+ self.runner.device.connect()
+ self.runner.start()
+ except Exception as e:
+ exc, val, tb = sys.exc_info()
+ message = "Error possibly due to runner or device args: {}"
+ raise exc, message.format(e.message), tb
+ # gecko_log comes from logcat when running with device/emulator
+ logcat_args = {
+ "filterspec": "Gecko",
+ "serial": self.runner.device.dm._deviceSerial
+ }
+ if self.gecko_log == "-":
+ logcat_args["stream"] = sys.stdout
+ else:
+ logcat_args["logfile"] = self.gecko_log
+ self.runner.device.start_logcat(**logcat_args)
+
+ # forward marionette port (localhost:2828)
+ self.runner.device.dm.forward(
+ local="tcp:{}".format(self.marionette_port),
+ remote="tcp:{}".format(self.marionette_port))
+
+ def _get_runner_args(self):
+ process_args = {
+ "processOutputLine": [NullOutput()],
+ }
+
+ runner_args = {
+ "app": self.package_name,
+ "avd_home": self.avd_home,
+ "adb_path": self.adb_path,
+ "binary": self.emulator_binary,
+ "profile": self.profile,
+ "cmdargs": ["-marionette"] + self.app_args,
+ "symbols_path": self.symbols_path,
+ "process_args": process_args,
+ "logdir": self.workspace or os.getcwd(),
+ "serial": self.serial,
+ }
+ if self.avd:
+ runner_args["avd"] = self.avd
+
+ return runner_args
+
+ def close(self, restart=False):
+ super(FennecInstance, self).close(restart)
+ if self.runner and self.runner.device.connected:
+ self.runner.device.dm.remove_forward(
+ "tcp:{}".format(self.marionette_port))
+
+
+class DesktopInstance(GeckoInstance):
+ desktop_prefs = {
+ # Disable application updates
+ "app.update.enabled": False,
+
+ # Enable output of dump()
+ "browser.dom.window.dump.enabled": True,
+
+ # Indicate that the download panel has been shown once so that whichever
+ # download test runs first doesn"t show the popup inconsistently
+ "browser.download.panel.shown": True,
+
+ # Do not show the EULA notification which can interfer with tests
+ "browser.EULA.override": True,
+
+ # Turn off about:newtab and make use of about:blank instead for new opened tabs
+ "browser.newtabpage.enabled": False,
+ # Assume the about:newtab page"s intro panels have been shown to not depend on
+ # which test runs first and happens to open about:newtab
+ "browser.newtabpage.introShown": True,
+
+ # Background thumbnails in particular cause grief, and disabling thumbnails
+ # in general can"t hurt - we re-enable them when tests need them
+ "browser.pagethumbnails.capturing_disabled": True,
+
+ # Avoid performing Reader Mode intros during tests
+ "browser.reader.detectedFirstArticle": True,
+
+ # Disable safebrowsing components
+ "browser.safebrowsing.blockedURIs.enabled": False,
+ "browser.safebrowsing.downloads.enabled": False,
+ "browser.safebrowsing.forbiddenURIs.enabled": False,
+ "browser.safebrowsing.malware.enabled": False,
+ "browser.safebrowsing.phishing.enabled": False,
+
+ # Disable updates to search engines
+ "browser.search.update": False,
+
+ # Do not restore the last open set of tabs if the browser has crashed
+ "browser.sessionstore.resume_from_crash": False,
+
+ # Don't check for the default web browser during startup
+ "browser.shell.checkDefaultBrowser": False,
+
+ # Disable e10s by default
+ "browser.tabs.remote.autostart.1": False,
+ "browser.tabs.remote.autostart.2": False,
+ "browser.tabs.remote.autostart": False,
+
+ # Needed for branded builds to prevent opening a second tab on startup
+ "browser.startup.homepage_override.mstone": "ignore",
+ # Start with a blank page by default
+ "browser.startup.page": 0,
+
+ # Disable tab animation
+ "browser.tabs.animate": False,
+
+ # Do not warn on exit when multiple tabs are open
+ "browser.tabs.warnOnClose": False,
+ # Do not warn when closing all other open tabs
+ "browser.tabs.warnOnCloseOtherTabs": False,
+ # Do not warn when multiple tabs will be opened
+ "browser.tabs.warnOnOpen": False,
+
+ # Disable the UI tour
+ "browser.uitour.enabled": False,
+
+ # Disable first-run welcome page
+ "startup.homepage_welcome_url": "about:blank",
+ "startup.homepage_welcome_url.additional": "",
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(DesktopInstance, self).__init__(*args, **kwargs)
+ self.required_prefs.update(DesktopInstance.desktop_prefs)
+
+
+class NullOutput(object):
+ def __call__(self, line):
+ pass
+
+
+apps = {
+ 'fennec': FennecInstance,
+ 'fxdesktop': DesktopInstance,
+}
+
+app_ids = {
+ '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'fennec',
+ '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'fxdesktop',
+}
diff --git a/testing/marionette/client/marionette_driver/gestures.py b/testing/marionette/client/marionette_driver/gestures.py
new file mode 100644
index 000000000..55b27ec83
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/gestures.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 import MultiActions, Actions
+
+
+def smooth_scroll(marionette_session, start_element, axis, direction,
+ length, increments=None, wait_period=None, scroll_back=None):
+ """
+ :param axis: y or x
+ :param direction: 0 for positive, and -1 for negative
+ :param length: total length of scroll scroll
+ :param increments: Amount to be moved per scrolling
+ :param wait_period: Seconds to wait between scrolling
+ :param scroll_back: Scroll back to original view?
+ """
+ if axis not in ["x", "y"]:
+ raise Exception("Axis must be either 'x' or 'y'")
+ if direction not in [-1, 0]:
+ raise Exception("Direction must either be -1 negative or 0 positive")
+ increments = increments or 100
+ wait_period = wait_period or 0.05
+ scroll_back = scroll_back or False
+ current = 0
+ if axis is "x":
+ if direction is -1:
+ offset = [-increments, 0]
+ else:
+ offset = [increments, 0]
+ else:
+ if direction is -1:
+ offset = [0, -increments]
+ else:
+ offset = [0, increments]
+ action = Actions(marionette_session)
+ action.press(start_element)
+ while (current < length):
+ current += increments
+ action.move_by_offset(*offset).wait(wait_period)
+ if scroll_back:
+ offset = [-value for value in offset]
+ while (current > 0):
+ current -= increments
+ action.move_by_offset(*offset).wait(wait_period)
+ action.release()
+ action.perform()
+
+
+def pinch(marionette_session, element, x1, y1, x2, y2, x3, y3, x4, y4, duration=200):
+ """
+ :param element: target
+ :param x1, y1: 1st finger starting position relative to the target
+ :param x3, y3: 1st finger ending position relative to the target
+ :param x2, y2: 2nd finger starting position relative to the target
+ :param x4, y4: 2nd finger ending position relative to the target
+ :param duration: Amount of time in milliseconds to complete the pinch.
+ """
+ time = 0
+ time_increment = 10
+ if time_increment >= duration:
+ time_increment = duration
+ move_x1 = time_increment*1.0/duration * (x3 - x1)
+ move_y1 = time_increment*1.0/duration * (y3 - y1)
+ move_x2 = time_increment*1.0/duration * (x4 - x2)
+ move_y2 = time_increment*1.0/duration * (y4 - y2)
+ multiAction = MultiActions(marionette_session)
+ action1 = Actions(marionette_session)
+ action2 = Actions(marionette_session)
+ action1.press(element, x1, y1)
+ action2.press(element, x2, y2)
+ while (time < duration):
+ time += time_increment
+ action1.move_by_offset(move_x1, move_y1).wait(time_increment/1000)
+ action2.move_by_offset(move_x2, move_y2).wait(time_increment/1000)
+ action1.release()
+ action2.release()
+ multiAction.add(action1).add(action2).perform()
+
+
+def long_press_without_contextmenu(marionette_session, element, time_in_seconds, x=None, y=None):
+ """
+ :param element: The element to press.
+ :param time_in_seconds: Time in seconds to wait before releasing the press.
+ #x: Optional, x-coordinate to tap, relative to the top-left corner of the element.
+ #y: Optional, y-coordinate to tap, relative to the top-leftcorner of the element.
+ """
+ action = Actions(marionette_session)
+ action.press(element, x, y)
+ action.move_by_offset(0, 0)
+ action.wait(time_in_seconds)
+ action.release()
+ action.perform()
diff --git a/testing/marionette/client/marionette_driver/keys.py b/testing/marionette/client/marionette_driver/keys.py
new file mode 100644
index 000000000..2a998c089
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/keys.py
@@ -0,0 +1,84 @@
+# copyright 2008-2009 WebDriver committers
+# Copyright 2008-2009 Google Inc.
+#
+# Licensed under the Apache License Version 2.0 = uthe "License")
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http //www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class Keys(object):
+
+ NULL = u'\ue000'
+ CANCEL = u'\ue001' # ^break
+ HELP = u'\ue002'
+ BACK_SPACE = u'\ue003'
+ TAB = u'\ue004'
+ CLEAR = u'\ue005'
+ RETURN = u'\ue006'
+ ENTER = u'\ue007'
+ SHIFT = u'\ue008'
+ LEFT_SHIFT = u'\ue008' # alias
+ CONTROL = u'\ue009'
+ LEFT_CONTROL = u'\ue009' # alias
+ ALT = u'\ue00a'
+ LEFT_ALT = u'\ue00a' # alias
+ PAUSE = u'\ue00b'
+ ESCAPE = u'\ue00c'
+ SPACE = u'\ue00d'
+ PAGE_UP = u'\ue00e'
+ PAGE_DOWN = u'\ue00f'
+ END = u'\ue010'
+ HOME = u'\ue011'
+ LEFT = u'\ue012'
+ ARROW_LEFT = u'\ue012' # alias
+ UP = u'\ue013'
+ ARROW_UP = u'\ue013' # alias
+ RIGHT = u'\ue014'
+ ARROW_RIGHT = u'\ue014' # alias
+ DOWN = u'\ue015'
+ ARROW_DOWN = u'\ue015' # alias
+ INSERT = u'\ue016'
+ DELETE = u'\ue017'
+ SEMICOLON = u'\ue018'
+ EQUALS = u'\ue019'
+
+ NUMPAD0 = u'\ue01a' # numbe pad keys
+ NUMPAD1 = u'\ue01b'
+ NUMPAD2 = u'\ue01c'
+ NUMPAD3 = u'\ue01d'
+ NUMPAD4 = u'\ue01e'
+ NUMPAD5 = u'\ue01f'
+ NUMPAD6 = u'\ue020'
+ NUMPAD7 = u'\ue021'
+ NUMPAD8 = u'\ue022'
+ NUMPAD9 = u'\ue023'
+ MULTIPLY = u'\ue024'
+ ADD = u'\ue025'
+ SEPARATOR = u'\ue026'
+ SUBTRACT = u'\ue027'
+ DECIMAL = u'\ue028'
+ DIVIDE = u'\ue029'
+
+ F1 = u'\ue031' # function keys
+ F2 = u'\ue032'
+ F3 = u'\ue033'
+ F4 = u'\ue034'
+ F5 = u'\ue035'
+ F6 = u'\ue036'
+ F7 = u'\ue037'
+ F8 = u'\ue038'
+ F9 = u'\ue039'
+ F10 = u'\ue03a'
+ F11 = u'\ue03b'
+ F12 = u'\ue03c'
+
+ META = u'\ue03d'
+ COMMAND = u'\ue03d'
diff --git a/testing/marionette/client/marionette_driver/localization.py b/testing/marionette/client/marionette_driver/localization.py
new file mode 100644
index 000000000..7b0d91b44
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/localization.py
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+class L10n(object):
+ """An API which allows Marionette to handle localized content.
+
+ The `localization`_ of UI elements in Gecko based applications is done via
+ entities and properties. For static values entities are used, which are located
+ in .dtd files. Whereby for dynamically updated content the values come from
+ .property files. Both types of elements can be identifed via a unique id,
+ and the translated content retrieved.
+
+ For example::
+
+ from marionette_driver.localization import L10n
+ l10n = L10n(marionette)
+
+ l10n.localize_entity(["chrome://global/locale/about.dtd"], "about.version")
+ l10n.localize_property(["chrome://global/locale/findbar.properties"], "FastFind"))
+
+ .. _localization: https://mzl.la/2eUMjyF
+ """
+
+ def __init__(self, marionette):
+ self._marionette = marionette
+
+ def localize_entity(self, dtd_urls, entity_id):
+ """Retrieve the localized string for the specified entity id.
+
+ :param dtd_urls: List of .dtd URLs which will be used to search for the entity.
+ :param entity_id: ID of the entity to retrieve the localized string for.
+
+ :returns: The localized string for the requested entity.
+ :raises: :exc:`NoSuchElementException`
+ """
+ body = {"urls": dtd_urls, "id": entity_id}
+ return self._marionette._send_message("localization:l10n:localizeEntity",
+ body, key="value")
+
+ def localize_property(self, properties_urls, property_id):
+ """Retrieve the localized string for the specified property id.
+
+ :param properties_urls: List of .properties URLs which will be used to
+ search for the property.
+ :param property_id: ID of the property to retrieve the localized string for.
+
+ :returns: The localized string for the requested property.
+ :raises: :exc:`NoSuchElementException`
+ """
+ body = {"urls": properties_urls, "id": property_id}
+ return self._marionette._send_message("localization:l10n:localizeProperty",
+ body, key="value")
diff --git a/testing/marionette/client/marionette_driver/marionette.py b/testing/marionette/client/marionette_driver/marionette.py
new file mode 100644
index 000000000..7450d1fbb
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -0,0 +1,2153 @@
+# 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 base64
+import datetime
+import json
+import os
+import socket
+import sys
+import time
+import traceback
+import warnings
+
+from contextlib import contextmanager
+
+import errors
+import transport
+
+from .decorators import do_process_check
+from .geckoinstance import GeckoInstance
+from .keys import Keys
+from .timeout import Timeouts
+
+WEBELEMENT_KEY = "ELEMENT"
+W3C_WEBELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
+
+
+class HTMLElement(object):
+ """Represents a DOM Element."""
+
+ def __init__(self, marionette, id):
+ self.marionette = marionette
+ assert(id is not None)
+ self.id = id
+
+ def __str__(self):
+ return self.id
+
+ def __eq__(self, other_element):
+ return self.id == other_element.id
+
+ def find_element(self, method, target):
+ """Returns an ``HTMLElement`` instance that matches the specified
+ method and target, relative to the current element.
+
+ For more details on this function, see the `find_element` method
+ in the Marionette class.
+ """
+ return self.marionette.find_element(method, target, self.id)
+
+ def find_elements(self, method, target):
+ """Returns a list of all ``HTMLElement`` instances that match the
+ specified method and target in the current context.
+
+ For more details on this function, see the find_elements method
+ in the Marionette class.
+ """
+ return self.marionette.find_elements(method, target, self.id)
+
+ def get_attribute(self, name):
+ """Returns the requested attribute, or None if no attribute
+ is set.
+ """
+ body = {"id": self.id, "name": name}
+ return self.marionette._send_message("getElementAttribute", body, key="value")
+
+ def get_property(self, name):
+ """Returns the requested property, or None if the property is
+ not set.
+ """
+ try:
+ body = {"id": self.id, "name": name}
+ return self.marionette._send_message("getElementProperty", body, key="value")
+ except errors.UnknownCommandException:
+ # Keep backward compatibility for code which uses get_attribute() to
+ # also retrieve element properties.
+ # Remove when Firefox 55 is stable.
+ return self.get_attribute(name)
+
+ def click(self):
+ self.marionette._send_message("clickElement", {"id": self.id})
+
+ def tap(self, x=None, y=None):
+ """Simulates a set of tap events on the element.
+
+ :param x: X coordinate of tap event. If not given, default to
+ the centre of the element.
+ :param y: Y coordinate of tap event. If not given, default to
+ the centre of the element.
+ """
+ body = {"id": self.id, "x": x, "y": y}
+ self.marionette._send_message("singleTap", body)
+
+ @property
+ def text(self):
+ """Returns the visible text of the element, and its child elements."""
+ body = {"id": self.id}
+ return self.marionette._send_message("getElementText", body, key="value")
+
+ def send_keys(self, *string):
+ """Sends the string via synthesized keypresses to the element."""
+ keys = Marionette.convert_keys(*string)
+ body = {"id": self.id, "value": keys}
+ self.marionette._send_message("sendKeysToElement", body)
+
+ def clear(self):
+ """Clears the input of the element."""
+ self.marionette._send_message("clearElement", {"id": self.id})
+
+ def is_selected(self):
+ """Returns True if the element is selected."""
+ body = {"id": self.id}
+ return self.marionette._send_message("isElementSelected", body, key="value")
+
+ def is_enabled(self):
+ """This command will return False if all the following criteria
+ are met otherwise return True:
+
+ * A form control is disabled.
+ * A ``HTMLElement`` has a disabled boolean attribute.
+ """
+ body = {"id": self.id}
+ return self.marionette._send_message("isElementEnabled", body, key="value")
+
+ def is_displayed(self):
+ """Returns True if the element is displayed, False otherwise."""
+ body = {"id": self.id}
+ return self.marionette._send_message("isElementDisplayed", body, key="value")
+
+ @property
+ def size(self):
+ """A dictionary with the size of the element."""
+ warnings.warn("The size property has been deprecated and will be removed in a future version. \
+ Please use HTMLElement#rect", DeprecationWarning)
+ rect = self.rect
+ return {"width": rect["width"], "height": rect["height"]}
+
+ @property
+ def tag_name(self):
+ """The tag name of the element."""
+ body = {"id": self.id}
+ return self.marionette._send_message("getElementTagName", body, key="value")
+
+ @property
+ def location(self):
+ """Get an element's location on the page.
+
+ The returned point will contain the x and y coordinates of the
+ top left-hand corner of the given element. The point (0,0)
+ refers to the upper-left corner of the document.
+
+ :returns: a dictionary containing x and y as entries
+ """
+ warnings.warn("The location property has been deprecated and will be removed in a future version. \
+ Please use HTMLElement#rect", DeprecationWarning)
+ rect = self.rect
+ return {"x": rect["x"], "y": rect["y"]}
+
+ @property
+ def rect(self):
+ """Gets the element's bounding rectangle.
+
+ This will return a dictionary with the following:
+
+ * x and y represent the top left coordinates of the ``HTMLElement``
+ relative to top left corner of the document.
+ * height and the width will contain the height and the width
+ of the DOMRect of the ``HTMLElement``.
+ """
+ body = {"id": self.id}
+ return self.marionette._send_message(
+ "getElementRect", body, key="value" if self.marionette.protocol == 1 else None)
+
+ def value_of_css_property(self, property_name):
+ """Gets the value of the specified CSS property name.
+
+ :param property_name: Property name to get the value of.
+ """
+ body = {"id": self.id, "propertyName": property_name}
+ return self.marionette._send_message(
+ "getElementValueOfCssProperty", body, key="value")
+
+
+class MouseButton(object):
+ """Enum-like class for mouse button constants."""
+ LEFT = 0
+ MIDDLE = 1
+ RIGHT = 2
+
+
+class Actions(object):
+ '''
+ An Action object represents a set of actions that are executed in a particular order.
+
+ All action methods (press, etc.) return the Actions object itself, to make
+ it easy to create a chain of events.
+
+ Example usage:
+
+ ::
+
+ # get html file
+ testAction = marionette.absolute_url("testFool.html")
+ # navigate to the file
+ marionette.navigate(testAction)
+ # find element1 and element2
+ element1 = marionette.find_element(By.ID, "element1")
+ element2 = marionette.find_element(By.ID, "element2")
+ # create action object
+ action = Actions(marionette)
+ # add actions (press, wait, move, release) into the object
+ action.press(element1).wait(5). move(element2).release()
+ # fire all the added events
+ action.perform()
+ '''
+
+ def __init__(self, marionette):
+ self.action_chain = []
+ self.marionette = marionette
+ self.current_id = None
+
+ def press(self, element, x=None, y=None):
+ '''
+ Sends a 'touchstart' event to this element.
+
+ If no coordinates are given, it will be targeted at the center of the
+ element. If given, it will be targeted at the (x,y) coordinates
+ relative to the top-left corner of the element.
+
+ :param element: The element to press on.
+ :param x: Optional, x-coordinate to tap, relative to the top-left
+ corner of the element.
+ :param y: Optional, y-coordinate to tap, relative to the top-left
+ corner of the element.
+ '''
+ element = element.id
+ self.action_chain.append(['press', element, x, y])
+ return self
+
+ def release(self):
+ '''
+ Sends a 'touchend' event to this element.
+
+ May only be called if press() has already be called on this element.
+
+ If press and release are chained without a move action between them,
+ then it will be processed as a 'tap' event, and will dispatch the
+ expected mouse events ('mousemove' (if necessary), 'mousedown',
+ 'mouseup', 'mouseclick') after the touch events. If there is a wait
+ period between press and release that will trigger a contextmenu,
+ then the 'contextmenu' menu event will be fired instead of the
+ touch/mouse events.
+ '''
+ self.action_chain.append(['release'])
+ return self
+
+ def move(self, element):
+ '''
+ Sends a 'touchmove' event at the center of the target element.
+
+ :param element: Element to move towards.
+
+ May only be called if press() has already be called.
+ '''
+ element = element.id
+ self.action_chain.append(['move', element])
+ return self
+
+ def move_by_offset(self, x, y):
+ '''
+ Sends 'touchmove' event to the given x, y coordinates relative to the
+ top-left of the currently touched element.
+
+ May only be called if press() has already be called.
+
+ :param x: Specifies x-coordinate of move event, relative to the
+ top-left corner of the element.
+ :param y: Specifies y-coordinate of move event, relative to the
+ top-left corner of the element.
+ '''
+ self.action_chain.append(['moveByOffset', x, y])
+ return self
+
+ def wait(self, time=None):
+ '''
+ Waits for specified time period.
+
+ :param time: Time in seconds to wait. If time is None then this has no effect
+ for a single action chain. If used inside a multi-action chain,
+ then time being None indicates that we should wait for all other
+ currently executing actions that are part of the chain to complete.
+ '''
+ self.action_chain.append(['wait', time])
+ return self
+
+ def cancel(self):
+ '''
+ Sends 'touchcancel' event to the target of the original 'touchstart' event.
+
+ May only be called if press() has already be called.
+ '''
+ self.action_chain.append(['cancel'])
+ return self
+
+ def tap(self, element, x=None, y=None):
+ '''
+ Performs a quick tap on the target element.
+
+ :param element: The element to tap.
+ :param x: Optional, x-coordinate of tap, relative to the top-left
+ corner of the element. If not specified, default to center of
+ element.
+ :param y: Optional, y-coordinate of tap, relative to the top-left
+ corner of the element. If not specified, default to center of
+ element.
+
+ This is equivalent to calling:
+
+ ::
+
+ action.press(element, x, y).release()
+ '''
+ element = element.id
+ self.action_chain.append(['press', element, x, y])
+ self.action_chain.append(['release'])
+ return self
+
+ def double_tap(self, element, x=None, y=None):
+ '''
+ Performs a double tap on the target element.
+
+ :param element: The element to double tap.
+ :param x: Optional, x-coordinate of double tap, relative to the
+ top-left corner of the element.
+ :param y: Optional, y-coordinate of double tap, relative to the
+ top-left corner of the element.
+ '''
+ element = element.id
+ self.action_chain.append(['press', element, x, y])
+ self.action_chain.append(['release'])
+ self.action_chain.append(['press', element, x, y])
+ self.action_chain.append(['release'])
+ return self
+
+ def click(self, element, button=MouseButton.LEFT, count=1):
+ '''
+ Performs a click with additional parameters to allow for double clicking,
+ right click, middle click, etc.
+
+ :param element: The element to click.
+ :param button: The mouse button to click (indexed from 0, left to right).
+ :param count: Optional, the count of clicks to synthesize (for double
+ click events).
+ '''
+ el = element.id
+ self.action_chain.append(['click', el, button, count])
+ return self
+
+ def context_click(self, element):
+ '''
+ Performs a context click on the specified element.
+
+ :param element: The element to context click.
+ '''
+ return self.click(element, button=MouseButton.RIGHT)
+
+ def middle_click(self, element):
+ '''
+ Performs a middle click on the specified element.
+
+ :param element: The element to middle click.
+ '''
+ return self.click(element, button=MouseButton.MIDDLE)
+
+ def double_click(self, element):
+ '''
+ Performs a double click on the specified element.
+
+ :param element: The element to double click.
+ '''
+ return self.click(element, count=2)
+
+ def flick(self, element, x1, y1, x2, y2, duration=200):
+ '''
+ Performs a flick gesture on the target element.
+
+ :param element: The element to perform the flick gesture on.
+ :param x1: Starting x-coordinate of flick, relative to the top left
+ corner of the element.
+ :param y1: Starting y-coordinate of flick, relative to the top left
+ corner of the element.
+ :param x2: Ending x-coordinate of flick, relative to the top left
+ corner of the element.
+ :param y2: Ending y-coordinate of flick, relative to the top left
+ corner of the element.
+ :param duration: Time needed for the flick gesture for complete (in
+ milliseconds).
+ '''
+ element = element.id
+ elapsed = 0
+ time_increment = 10
+ if time_increment >= duration:
+ time_increment = duration
+ move_x = time_increment*1.0/duration * (x2 - x1)
+ move_y = time_increment*1.0/duration * (y2 - y1)
+ self.action_chain.append(['press', element, x1, y1])
+ while elapsed < duration:
+ elapsed += time_increment
+ self.action_chain.append(['moveByOffset', move_x, move_y])
+ self.action_chain.append(['wait', time_increment/1000])
+ self.action_chain.append(['release'])
+ return self
+
+ def long_press(self, element, time_in_seconds, x=None, y=None):
+ '''
+ Performs a long press gesture on the target element.
+
+ :param element: The element to press.
+ :param time_in_seconds: Time in seconds to wait before releasing the press.
+ :param x: Optional, x-coordinate to tap, relative to the top-left
+ corner of the element.
+ :param y: Optional, y-coordinate to tap, relative to the top-left
+ corner of the element.
+
+ This is equivalent to calling:
+
+ ::
+
+ action.press(element, x, y).wait(time_in_seconds).release()
+
+ '''
+ element = element.id
+ self.action_chain.append(['press', element, x, y])
+ self.action_chain.append(['wait', time_in_seconds])
+ self.action_chain.append(['release'])
+ return self
+
+ def key_down(self, key_code):
+ """
+ Perform a "keyDown" action for the given key code. Modifier keys are
+ respected by the server for the course of an action chain.
+
+ :param key_code: The key to press as a result of this action.
+ """
+ self.action_chain.append(['keyDown', key_code])
+ return self
+
+ def key_up(self, key_code):
+ """
+ Perform a "keyUp" action for the given key code. Modifier keys are
+ respected by the server for the course of an action chain.
+ :param key_up: The key to release as a result of this action.
+ """
+ self.action_chain.append(['keyUp', key_code])
+ return self
+
+ def perform(self):
+ """Sends the action chain built so far to the server side for
+ execution and clears the current chain of actions."""
+ body = {"chain": self.action_chain, "nextId": self.current_id}
+ self.current_id = self.marionette._send_message("actionChain", body, key="value")
+ self.action_chain = []
+ return self
+
+
+class MultiActions(object):
+ '''
+ A MultiActions object represents a sequence of actions that may be
+ performed at the same time. Its intent is to allow the simulation
+ of multi-touch gestures.
+ Usage example:
+
+ ::
+
+ # create multiaction object
+ multitouch = MultiActions(marionette)
+ # create several action objects
+ action_1 = Actions(marionette)
+ action_2 = Actions(marionette)
+ # add actions to each action object/finger
+ action_1.press(element1).move_to(element2).release()
+ action_2.press(element3).wait().release(element3)
+ # fire all the added events
+ multitouch.add(action_1).add(action_2).perform()
+ '''
+
+ def __init__(self, marionette):
+ self.multi_actions = []
+ self.max_length = 0
+ self.marionette = marionette
+
+ def add(self, action):
+ '''
+ Adds a set of actions to perform.
+
+ :param action: An Actions object.
+ '''
+ self.multi_actions.append(action.action_chain)
+ if len(action.action_chain) > self.max_length:
+ self.max_length = len(action.action_chain)
+ return self
+
+ def perform(self):
+ """Perform all the actions added to this object."""
+ body = {"value": self.multi_actions, "max_length": self.max_length}
+ self.marionette._send_message("multiAction", body)
+
+
+class Alert(object):
+ """A class for interacting with alerts.
+
+ ::
+
+ Alert(marionette).accept()
+ Alert(merionette).dismiss()
+ """
+
+ def __init__(self, marionette):
+ self.marionette = marionette
+
+ def accept(self):
+ """Accept a currently displayed modal dialog."""
+ self.marionette._send_message("acceptDialog")
+
+ def dismiss(self):
+ """Dismiss a currently displayed modal dialog."""
+ self.marionette._send_message("dismissDialog")
+
+ @property
+ def text(self):
+ """Return the currently displayed text in a tab modal."""
+ return self.marionette._send_message("getTextFromDialog", key="value")
+
+ def send_keys(self, *string):
+ """Send keys to the currently displayed text input area in an open
+ tab modal dialog."""
+ body = {"value": Marionette.convert_keys(*string)}
+ self.marionette._send_message("sendKeysToDialog", body)
+
+
+class Marionette(object):
+ """Represents a Marionette connection to a browser or device."""
+
+ CONTEXT_CHROME = "chrome" # non-browser content: windows, dialogs, etc.
+ CONTEXT_CONTENT = "content" # browser content: iframes, divs, etc.
+ DEFAULT_STARTUP_TIMEOUT = 120
+ DEFAULT_SHUTDOWN_TIMEOUT = 65 # Firefox will kill hanging threads after 60s
+
+ # Bug 1336953 - Until we can remove the socket timeout parameter it has to be
+ # set a default value which is larger than the longest timeout as defined by the
+ # WebDriver spec. In that case its 300s for page load. Also add another minute
+ # so that slow builds have enough time to send the timeout error to the client.
+ DEFAULT_SOCKET_TIMEOUT = 360
+
+ def __init__(self, host="localhost", port=2828, app=None, bin=None,
+ baseurl=None, socket_timeout=DEFAULT_SOCKET_TIMEOUT,
+ startup_timeout=None, **instance_args):
+ """Construct a holder for the Marionette connection.
+
+ Remember to call ``start_session`` in order to initiate the
+ connection and start a Marionette session.
+
+ :param host: Host where the Marionette server listens.
+ Defaults to localhost.
+ :param port: Port where the Marionette server listens.
+ Defaults to port 2828.
+ :param baseurl: Where to look for files served from Marionette's
+ www directory.
+ :param socket_timeout: Timeout for Marionette socket operations.
+ :param startup_timeout: Seconds to wait for a connection with
+ binary.
+ :param bin: Path to browser binary. If any truthy value is given
+ this will attempt to start a Gecko instance with the specified
+ `app`.
+ :param app: Type of ``instance_class`` to use for managing app
+ instance. See ``marionette_driver.geckoinstance``.
+ :param instance_args: Arguments to pass to ``instance_class``.
+
+ """
+ self.host = host
+ self.port = self.local_port = int(port)
+ self.bin = bin
+ self.client = None
+ self.instance = None
+ self.session = None
+ self.session_id = None
+ self.process_id = None
+ self.profile = None
+ self.window = None
+ self.chrome_window = None
+ self.baseurl = baseurl
+ self._test_name = None
+ self.socket_timeout = socket_timeout
+ self.crashed = 0
+
+ self.startup_timeout = int(startup_timeout or self.DEFAULT_STARTUP_TIMEOUT)
+ if self.bin:
+ if not Marionette.is_port_available(self.port, host=self.host):
+ ex_msg = "{0}:{1} is unavailable.".format(self.host, self.port)
+ raise errors.MarionetteException(message=ex_msg)
+
+ self.instance = GeckoInstance.create(
+ app, host=self.host, port=self.port, bin=self.bin, **instance_args)
+ self.instance.start()
+ self.raise_for_port(timeout=self.startup_timeout)
+
+ self.timeout = Timeouts(self)
+
+ @property
+ def profile_path(self):
+ if self.instance and self.instance.profile:
+ return self.instance.profile.profile
+
+ def cleanup(self):
+ if self.session:
+ try:
+ self.delete_session()
+ except (errors.MarionetteException, IOError):
+ # These exceptions get thrown if the Marionette server
+ # hit an exception/died or the connection died. We can
+ # do no further server-side cleanup in this case.
+ pass
+ if self.instance:
+ self.instance.close()
+
+ def __del__(self):
+ self.cleanup()
+
+ @staticmethod
+ def is_port_available(port, host=''):
+ port = int(port)
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ s.bind((host, port))
+ return True
+ except socket.error:
+ return False
+ finally:
+ s.close()
+
+ def wait_for_port(self, timeout=None):
+ """Wait until Marionette server has been created the communication socket.
+
+ :param timeout: Timeout in seconds for the server to be ready.
+
+ """
+ if timeout is None:
+ timeout = self.DEFAULT_STARTUP_TIMEOUT
+
+ runner = None
+ if self.instance is not None:
+ runner = self.instance.runner
+
+ poll_interval = 0.1
+ starttime = datetime.datetime.now()
+
+ while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
+ # If the instance we want to connect to is not running return immediately
+ if runner is not None and not runner.is_running():
+ return False
+
+ sock = None
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(0.5)
+ sock.connect((self.host, self.port))
+ data = sock.recv(16)
+ if ":" in data:
+ return True
+ except socket.error:
+ pass
+ finally:
+ if sock is not None:
+ sock.close()
+
+ time.sleep(poll_interval)
+
+ return False
+
+ @do_process_check
+ def raise_for_port(self, timeout=None):
+ """Raise socket.timeout if no connection can be established.
+
+ :param timeout: Timeout in seconds for the server to be ready.
+
+ """
+ if not self.wait_for_port(timeout):
+ raise socket.timeout("Timed out waiting for connection on {0}:{1}!".format(
+ self.host, self.port))
+
+ @do_process_check
+ def _send_message(self, name, params=None, key=None):
+ """Send a blocking message to the server.
+
+ Marionette provides an asynchronous, non-blocking interface and
+ this attempts to paper over this by providing a synchronous API
+ to the user.
+
+ :param name: Requested command key.
+ :param params: Optional dictionary of key/value arguments.
+ :param key: Optional key to extract from response.
+
+ :returns: Full response from the server, or if `key` is given,
+ the value of said key in the response.
+ """
+
+ if not self.session_id and name != "newSession":
+ raise errors.MarionetteException("Please start a session")
+
+ try:
+ if self.protocol < 3:
+ data = {"name": name}
+ if params:
+ data["parameters"] = params
+ self.client.send(data)
+ msg = self.client.receive()
+
+ else:
+ msg = self.client.request(name, params)
+
+ except IOError:
+ self.delete_session(send_request=False)
+ raise
+
+ res, err = msg.result, msg.error
+ if err:
+ self._handle_error(err)
+
+ if key is not None:
+ return self._unwrap_response(res.get(key))
+ else:
+ return self._unwrap_response(res)
+
+ def _unwrap_response(self, value):
+ if isinstance(value, dict) and (WEBELEMENT_KEY in value or
+ W3C_WEBELEMENT_KEY in value):
+ if value.get(WEBELEMENT_KEY):
+ return HTMLElement(self, value.get(WEBELEMENT_KEY))
+ else:
+ return HTMLElement(self, value.get(W3C_WEBELEMENT_KEY))
+ elif isinstance(value, list):
+ return list(self._unwrap_response(item) for item in value)
+ else:
+ return value
+
+ def _handle_error(self, obj):
+ if self.protocol == 1:
+ if "error" not in obj or not isinstance(obj["error"], dict):
+ raise errors.MarionetteException(
+ "Malformed packet, expected key 'error' to be a dict: {}".format(obj))
+ error = obj["error"].get("status")
+ message = obj["error"].get("message")
+ stacktrace = obj["error"].get("stacktrace")
+
+ else:
+ error = obj["error"]
+ message = obj["message"]
+ stacktrace = obj["stacktrace"]
+
+ raise errors.lookup(error)(message, stacktrace=stacktrace)
+
+ def check_for_crash(self):
+ """Check if the process crashed.
+
+ :returns: True, if a crash happened since the method has been called the last time.
+ """
+ crash_count = 0
+
+ if self.instance:
+ name = self.test_name or 'marionette.py'
+ crash_count = self.instance.runner.check_for_crashes(test_name=name)
+ self.crashed = self.crashed + crash_count
+
+ return crash_count > 0
+
+ def _handle_socket_failure(self):
+ """Handle socket failures for the currently connected application.
+
+ If the application crashed then clean-up internal states, or in case of a content
+ crash also kill the process. If there are other reasons for a socket failure,
+ wait for the process to shutdown itself, or force kill it.
+
+ Please note that the method expects an exception to be handled on the current stack
+ frame, and is only called via the `@do_process_check` decorator.
+
+ """
+ exc, val, tb = sys.exc_info()
+
+ # If the application hasn't been launched by Marionette no further action can be done.
+ # In such cases we simply re-throw the exception.
+ if not self.instance:
+ raise exc, val, tb
+
+ else:
+ # Somehow the socket disconnected. Give the application some time to shutdown
+ # itself before killing the process.
+ returncode = self.instance.runner.wait(timeout=self.DEFAULT_SHUTDOWN_TIMEOUT)
+
+ if returncode is None:
+ message = ('Process killed because the connection to Marionette server is '
+ 'lost. Check gecko.log for errors')
+ # This will force-close the application without sending any other message.
+ self.cleanup()
+ else:
+ # If Firefox quit itself check if there was a crash
+ crash_count = self.check_for_crash()
+
+ if crash_count > 0:
+ if returncode == 0:
+ message = 'Content process crashed'
+ else:
+ message = 'Process crashed (Exit code: {returncode})'
+ else:
+ message = 'Process has been unexpectedly closed (Exit code: {returncode})'
+
+ self.delete_session(send_request=False, reset_session_id=True)
+
+ message += ' (Reason: {reason})'
+
+ raise IOError, message.format(returncode=returncode, reason=val), tb
+
+ @staticmethod
+ def convert_keys(*string):
+ typing = []
+ for val in string:
+ if isinstance(val, Keys):
+ typing.append(val)
+ elif isinstance(val, int):
+ val = str(val)
+ for i in range(len(val)):
+ typing.append(val[i])
+ else:
+ for i in range(len(val)):
+ typing.append(val[i])
+ return typing
+
+ def get_permission(self, perm):
+ script = """
+ let value = {
+ 'url': document.nodePrincipal.URI.spec,
+ 'appId': document.nodePrincipal.appId,
+ 'isInIsolatedMozBrowserElement': document.nodePrincipal.isInIsolatedMozBrowserElement,
+ 'type': arguments[0]
+ };
+ return value;"""
+ with self.using_context("content"):
+ value = self.execute_script(script, script_args=(perm,), sandbox="system")
+
+ with self.using_context("chrome"):
+ permission = self.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let perm = arguments[0];
+ let secMan = Services.scriptSecurityManager;
+ let attrs = {appId: perm.appId,
+ inIsolatedMozBrowser: perm.isInIsolatedMozBrowserElement};
+ let principal = secMan.createCodebasePrincipal(
+ Services.io.newURI(perm.url, null, null),
+ attrs);
+ let testPerm = Services.perms.testPermissionFromPrincipal(
+ principal, perm.type);
+ return testPerm;
+ """, script_args=(value,))
+ return permission
+
+ def push_permission(self, perm, allow):
+ script = """
+ let allow = arguments[0];
+ if (typeof(allow) == "boolean") {
+ if (allow) {
+ allow = Components.interfaces.nsIPermissionManager.ALLOW_ACTION;
+ }
+ else {
+ allow = Components.interfaces.nsIPermissionManager.DENY_ACTION;
+ }
+ }
+ let perm_type = arguments[1];
+
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ window.wrappedJSObject.permChanged = false;
+ window.wrappedJSObject.permObserver = function(subject, topic, data) {
+ if (topic == "perm-changed") {
+ let permission = subject.QueryInterface(Components.interfaces.nsIPermission);
+ if (perm_type == permission.type) {
+ Services.obs.removeObserver(window.wrappedJSObject.permObserver,
+ "perm-changed");
+ window.wrappedJSObject.permChanged = true;
+ }
+ }
+ };
+ Services.obs.addObserver(window.wrappedJSObject.permObserver,
+ "perm-changed", false);
+
+ let value = {
+ 'url': document.nodePrincipal.URI.spec,
+ 'appId': document.nodePrincipal.appId,
+ 'isInIsolatedMozBrowserElement': document.nodePrincipal.isInIsolatedMozBrowserElement,
+ 'type': perm_type,
+ 'action': allow
+ };
+ return value;
+ """
+ with self.using_context("content"):
+ perm = self.execute_script(script, script_args=(allow, perm,), sandbox="system")
+
+ current_perm = self.get_permission(perm["type"])
+ if current_perm == perm["action"]:
+ with self.using_context("content"):
+ self.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Services.obs.removeObserver(window.wrappedJSObject.permObserver,
+ "perm-changed");
+ """, sandbox="system")
+ return
+
+ with self.using_context("chrome"):
+ self.execute_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let perm = arguments[0];
+ let secMan = Services.scriptSecurityManager;
+ let attrs = {appId: perm.appId,
+ inIsolatedMozBrowser: perm.isInIsolatedMozBrowserElement};
+ let principal = secMan.createCodebasePrincipal(Services.io.newURI(perm.url,
+ null, null),
+ attrs);
+ Services.perms.addFromPrincipal(principal, perm.type, perm.action);
+ return true;
+ """, script_args=(perm,))
+
+ with self.using_context("content"):
+ self.execute_async_script("""
+ let wait = function() {
+ if (window.wrappedJSObject.permChanged) {
+ marionetteScriptFinished();
+ } else {
+ window.setTimeout(wait, 100);
+ }
+ }();
+ """, sandbox="system")
+
+ @contextmanager
+ def using_permissions(self, perms):
+ '''
+ Sets permissions for code being executed in a `with` block,
+ and restores them on exit.
+
+ :param perms: A dict containing one or more perms and their
+ values to be set.
+
+ Usage example::
+
+ with marionette.using_permissions({'systemXHR': True}):
+ ... do stuff ...
+ '''
+ original_perms = {}
+ for perm in perms:
+ original_perms[perm] = self.get_permission(perm)
+ self.push_permission(perm, perms[perm])
+
+ try:
+ yield
+ finally:
+ for perm in original_perms:
+ self.push_permission(perm, original_perms[perm])
+
+ def clear_pref(self, pref):
+ """Clear the user-defined value from the specified preference.
+
+ :param pref: Name of the preference.
+ """
+ with self.using_context(self.CONTEXT_CHROME):
+ self.execute_script("""
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+ Preferences.reset(arguments[0]);
+ """, script_args=(pref,))
+
+ def get_pref(self, pref, default_branch=False, value_type="nsISupportsString"):
+ """Get the value of the specified preference.
+
+ :param pref: Name of the preference.
+ :param default_branch: Optional, if `True` the preference value will be read
+ from the default branch. Otherwise the user-defined
+ value if set is returned. Defaults to `False`.
+ :param value_type: Optional, XPCOM interface of the pref's complex value.
+ Defaults to `nsISupportsString`. Other possible values are:
+ `nsILocalFile`, and `nsIPrefLocalizedString`.
+
+ Usage example::
+ marionette.get_pref("browser.tabs.warnOnClose")
+
+ """
+ with self.using_context(self.CONTEXT_CHROME):
+ pref_value = self.execute_script("""
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ let pref = arguments[0];
+ let defaultBranch = arguments[1];
+ let valueType = arguments[2];
+
+ prefs = new Preferences({defaultBranch: defaultBranch});
+ return prefs.get(pref, null, Components.interfaces[valueType]);
+ """, script_args=(pref, default_branch, value_type))
+ return pref_value
+
+ def set_pref(self, pref, value, default_branch=False):
+ """Set the value of the specified preference.
+
+ :param pref: Name of the preference.
+ :param value: The value to set the preference to. If the value is None,
+ reset the preference to its default value. If no default
+ value exists, the preference will cease to exist.
+ :param default_branch: Optional, if `True` the preference value will
+ be written to the default branch, and will remain until
+ the application gets restarted. Otherwise a user-defined
+ value is set. Defaults to `False`.
+
+ Usage example::
+ marionette.set_pref("browser.tabs.warnOnClose", True)
+
+ """
+ with self.using_context(self.CONTEXT_CHROME):
+ if value is None:
+ self.clear_pref(pref)
+ return
+
+ self.execute_script("""
+ Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+ let pref = arguments[0];
+ let value = arguments[1];
+ let defaultBranch = arguments[2];
+
+ prefs = new Preferences({defaultBranch: defaultBranch});
+ prefs.set(pref, value);
+ """, script_args=(pref, value, default_branch))
+
+ def set_prefs(self, prefs, default_branch=False):
+ """Set the value of a list of preferences.
+
+ :param prefs: A dict containing one or more preferences and their values
+ to be set. See `set_pref` for further details.
+ :param default_branch: Optional, if `True` the preference value will
+ be written to the default branch, and will remain until
+ the application gets restarted. Otherwise a user-defined
+ value is set. Defaults to `False`.
+
+ Usage example::
+
+ marionette.set_prefs({"browser.tabs.warnOnClose": True})
+
+ """
+ for pref, value in prefs.items():
+ self.set_pref(pref, value, default_branch=default_branch)
+
+ @contextmanager
+ def using_prefs(self, prefs, default_branch=False):
+ """Set preferences for code executed in a `with` block, and restores them on exit.
+
+ :param prefs: A dict containing one or more preferences and their values
+ to be set. See `set_prefs` for further details.
+ :param default_branch: Optional, if `True` the preference value will
+ be written to the default branch, and will remain until
+ the application gets restarted. Otherwise a user-defined
+ value is set. Defaults to `False`.
+
+ Usage example::
+
+ with marionette.using_prefs({"browser.tabs.warnOnClose": True}):
+ # ... do stuff ...
+
+ """
+ original_prefs = {p: self.get_pref(p) for p in prefs}
+ self.set_prefs(prefs, default_branch=default_branch)
+
+ try:
+ yield
+ finally:
+ self.set_prefs(original_prefs, default_branch=default_branch)
+
+ @do_process_check
+ def enforce_gecko_prefs(self, prefs):
+ """Checks if the running instance has the given prefs. If not,
+ it will kill the currently running instance, and spawn a new
+ instance with the requested preferences.
+
+ : param prefs: A dictionary whose keys are preference names.
+ """
+ if not self.instance:
+ raise errors.MarionetteException("enforce_gecko_prefs() can only be called "
+ "on Gecko instances launched by Marionette")
+ pref_exists = True
+ with self.using_context(self.CONTEXT_CHROME):
+ for pref, value in prefs.iteritems():
+ if type(value) is not str:
+ value = json.dumps(value)
+ pref_exists = self.execute_script("""
+ let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ let pref = '{0}';
+ let value = '{1}';
+ let type = prefInterface.getPrefType(pref);
+ switch(type) {{
+ case prefInterface.PREF_STRING:
+ return value == prefInterface.getCharPref(pref).toString();
+ case prefInterface.PREF_BOOL:
+ return value == prefInterface.getBoolPref(pref).toString();
+ case prefInterface.PREF_INT:
+ return value == prefInterface.getIntPref(pref).toString();
+ case prefInterface.PREF_INVALID:
+ return false;
+ }}
+ """.format(pref, value))
+ if not pref_exists:
+ break
+
+ if not pref_exists:
+ context = self._send_message("getContext", key="value")
+ self.delete_session()
+ self.instance.restart(prefs)
+ self.raise_for_port()
+ self.start_session()
+
+ # Restore the context as used before the restart
+ self.set_context(context)
+
+ def _request_in_app_shutdown(self, shutdown_flags=None):
+ """Terminate the currently running instance from inside the application.
+
+ :param shutdown_flags: If specified use additional flags for the shutdown
+ of the application. Possible values here correspond
+ to constants in nsIAppStartup: http://mzl.la/1X0JZsC.
+ """
+ flags = set([])
+ if shutdown_flags:
+ flags.add(shutdown_flags)
+
+ # Trigger a 'quit-application-requested' observer notification so that
+ # components can safely shutdown before quitting the application.
+ with self.using_context("chrome"):
+ canceled = self.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);
+ return cancelQuit.data;
+ """)
+ if canceled:
+ raise errors.MarionetteException("Something canceled the quit application request")
+
+ self._send_message("quitApplication", {"flags": list(flags)})
+
+ @do_process_check
+ def quit(self, in_app=False, callback=None):
+ """Terminate the currently running instance.
+
+ This command will delete the active marionette session. It also allows
+ manipulation of eg. the profile data while the application is not running.
+ To start the application again, start_session() has to be called.
+
+ :param in_app: If True, marionette will cause a quit from within the
+ browser. Otherwise the browser will be quit immediately
+ by killing the process.
+ :param callback: If provided and `in_app` is True, the callback will
+ be used to trigger the shutdown.
+ """
+ if not self.instance:
+ raise errors.MarionetteException("quit() can only be called "
+ "on Gecko instances launched by Marionette")
+
+ if in_app:
+ if callable(callback):
+ self._send_message("acceptConnections", {"value": False})
+ callback()
+ else:
+ self._request_in_app_shutdown()
+
+ # Ensure to explicitely mark the session as deleted
+ self.delete_session(send_request=False, reset_session_id=True)
+
+ # Give the application some time to shutdown
+ self.instance.runner.wait(timeout=self.DEFAULT_SHUTDOWN_TIMEOUT)
+ else:
+ self.delete_session(reset_session_id=True)
+ self.instance.close()
+
+ @do_process_check
+ def restart(self, clean=False, in_app=False, callback=None):
+ """
+ This will terminate the currently running instance, and spawn a new instance
+ with the same profile and then reuse the session id when creating a session again.
+
+ :param clean: If False the same profile will be used after the restart. Note
+ that the in app initiated restart always maintains the same
+ profile.
+ :param in_app: If True, marionette will cause a restart from within the
+ browser. Otherwise the browser will be restarted immediately
+ by killing the process.
+ :param callback: If provided and `in_app` is True, the callback will be
+ used to trigger the restart.
+ """
+ if not self.instance:
+ raise errors.MarionetteException("restart() can only be called "
+ "on Gecko instances launched by Marionette")
+
+ context = self._send_message("getContext", key="value")
+ session_id = self.session_id
+
+ if in_app:
+ if clean:
+ raise ValueError("An in_app restart cannot be triggered with the clean flag set")
+
+ if callable(callback):
+ self._send_message("acceptConnections", {"value": False})
+ callback()
+ else:
+ self._request_in_app_shutdown("eRestart")
+
+ # Ensure to explicitely mark the session as deleted
+ self.delete_session(send_request=False, reset_session_id=True)
+
+ try:
+ self.raise_for_port()
+ except socket.timeout:
+ if self.instance.runner.returncode is not None:
+ exc, val, tb = sys.exc_info()
+ self.cleanup()
+ raise exc, "Requested restart of the application was aborted", tb
+
+ else:
+ self.delete_session()
+ self.instance.restart(clean=clean)
+ self.raise_for_port()
+
+ self.start_session(session_id=session_id)
+
+ # Restore the context as used before the restart
+ self.set_context(context)
+
+ if in_app and self.process_id:
+ # In some cases Firefox restarts itself by spawning into a new process group.
+ # As long as mozprocess cannot track that behavior (bug 1284864) we assist by
+ # informing about the new process id.
+ self.instance.runner.process_handler.check_for_detached(self.process_id)
+
+ def absolute_url(self, relative_url):
+ '''
+ Returns an absolute url for files served from Marionette's www directory.
+
+ :param relative_url: The url of a static file, relative to Marionette's www directory.
+ '''
+ return "{0}{1}".format(self.baseurl, relative_url)
+
+ @do_process_check
+ def start_session(self, capabilities=None, session_id=None, timeout=60):
+ """Create a new Marionette session.
+
+ This method must be called before performing any other action.
+
+ :param capabilities: An optional dict of desired or required capabilities.
+ :param timeout: Timeout in seconds for the server to be ready.
+ :param session_id: unique identifier for the session. If no session id is
+ passed in then one will be generated by the marionette server.
+
+ :returns: A dict of the capabilities offered.
+
+ """
+ self.crashed = 0
+
+ if self.instance:
+ returncode = self.instance.runner.returncode
+ if returncode is not None:
+ # We're managing a binary which has terminated, so restart it.
+ self.instance.restart()
+
+ self.client = transport.TcpTransport(
+ self.host,
+ self.port,
+ self.socket_timeout)
+
+ # Call wait_for_port() before attempting to connect in
+ # the event gecko hasn't started yet.
+ timeout = timeout or self.startup_timeout
+ self.wait_for_port(timeout=timeout)
+ self.protocol, _ = self.client.connect()
+
+ body = {"capabilities": capabilities, "sessionId": session_id}
+ resp = self._send_message("newSession", body)
+
+ self.session_id = resp["sessionId"]
+ self.session = resp["value"] if self.protocol == 1 else resp["capabilities"]
+ # fallback to processId can be removed in Firefox 55
+ self.process_id = self.session.get("moz:processID", self.session.get("processId"))
+ self.profile = self.session.get("moz:profile")
+
+ return self.session
+
+ @property
+ def test_name(self):
+ return self._test_name
+
+ @test_name.setter
+ def test_name(self, test_name):
+ self._send_message("setTestName", {"value": test_name})
+ self._test_name = test_name
+
+ def delete_session(self, send_request=True, reset_session_id=False):
+ """Close the current session and disconnect from the server.
+
+ :param send_request: Optional, if `True` a request to close the session on
+ the server side will be send. Use `False` in case of eg. in_app restart()
+ or quit(), which trigger a deletion themselves. Defaults to `True`.
+ :param reset_session_id: Optional, if `True` the current session id will
+ be reset, which will require an explicit call to `start_session()` before
+ the test can continue. Defaults to `False`.
+ """
+ try:
+ if send_request:
+ self._send_message("deleteSession")
+ finally:
+ if reset_session_id:
+ self.session_id = None
+ self.session = None
+ self.process_id = None
+ self.profile = None
+ self.window = None
+
+ if self.client is not None:
+ self.client.close()
+
+ @property
+ def session_capabilities(self):
+ """A JSON dictionary representing the capabilities of the
+ current session.
+
+ """
+ return self.session
+
+ def set_script_timeout(self, timeout):
+ """Sets the maximum number of ms that an asynchronous script is
+ allowed to run.
+
+ If a script does not return in the specified amount of time,
+ a ScriptTimeoutException is raised.
+
+ :param timeout: The maximum number of milliseconds an asynchronous
+ script can run without causing an ScriptTimeoutException to
+ be raised
+
+ .. note:: `set_script_timeout` is deprecated, please use
+ `timeout.script` setter.
+
+ """
+ warnings.warn(
+ "set_script_timeout is deprecated, please use timeout.script setter",
+ DeprecationWarning)
+ self.timeout.script = timeout / 1000
+
+ def set_search_timeout(self, timeout):
+ """Sets a timeout for the find methods.
+
+ When searching for an element using
+ either :class:`Marionette.find_element` or
+ :class:`Marionette.find_elements`, the method will continue
+ trying to locate the element for up to timeout ms. This can be
+ useful if, for example, the element you're looking for might
+ not exist immediately, because it belongs to a page which is
+ currently being loaded.
+
+ :param timeout: Timeout in milliseconds.
+
+ .. note:: `set_search_timeout` is deprecated, please use
+ `timeout.implicit` setter.
+
+ """
+ warnings.warn(
+ "set_search_timeout is deprecated, please use timeout.implicit setter",
+ DeprecationWarning)
+ self.timeout.implicit = timeout / 1000
+
+ def set_page_load_timeout(self, timeout):
+ """Sets a timeout for loading pages.
+
+ A page load timeout specifies the amount of time the Marionette
+ instance should wait for a page load operation to complete. A
+ ``TimeoutException`` is returned if this limit is exceeded.
+
+ :param timeout: Timeout in milliseconds.
+
+ .. note:: `set_page_load_timeout` is deprecated, please use
+ `timeout.page_load` setter.
+
+ """
+ warnings.warn(
+ "set_page_load_timeout is deprecated, please use timeout.page_load setter",
+ DeprecationWarning)
+ self.timeout.page_load = timeout / 1000
+
+ @property
+ def current_window_handle(self):
+ """Get the current window's handle.
+
+ Returns an opaque server-assigned identifier to this window
+ that uniquely identifies it within this Marionette instance.
+ This can be used to switch to this window at a later point.
+
+ :returns: unique window handle
+ :rtype: string
+ """
+ self.window = self._send_message("getWindowHandle", key="value")
+ return self.window
+
+ @property
+ def current_chrome_window_handle(self):
+ """Get the current chrome window's handle. Corresponds to
+ a chrome window that may itself contain tabs identified by
+ window_handles.
+
+ Returns an opaque server-assigned identifier to this window
+ that uniquely identifies it within this Marionette instance.
+ This can be used to switch to this window at a later point.
+
+ :returns: unique window handle
+ :rtype: string
+ """
+ self.chrome_window = self._send_message(
+ "getCurrentChromeWindowHandle", key="value")
+ return self.chrome_window
+
+ def get_window_position(self):
+ """Get the current window's position.
+
+ :returns: a dictionary with x and y
+ """
+ return self._send_message(
+ "getWindowPosition", key="value" if self.protocol == 1 else None)
+
+ def set_window_position(self, x, y):
+ """Set the position of the current window
+
+ :param x: x coordinate for the top left of the window
+ :param y: y coordinate for the top left of the window
+ """
+ self._send_message("setWindowPosition", {"x": x, "y": y})
+
+ @property
+ def title(self):
+ """Current title of the active window."""
+ return self._send_message("getTitle", key="value")
+
+ @property
+ def window_handles(self):
+ """Get list of windows in the current context.
+
+ If called in the content context it will return a list of
+ references to all available browser windows. Called in the
+ chrome context, it will list all available windows, not just
+ browser windows (e.g. not just navigator.browser).
+
+ Each window handle is assigned by the server, and the list of
+ strings returned does not have a guaranteed ordering.
+
+ :returns: Unordered list of unique window handles as strings
+ """
+ return self._send_message(
+ "getWindowHandles", key="value" if self.protocol == 1 else None)
+
+ @property
+ def chrome_window_handles(self):
+ """Get a list of currently open chrome windows.
+
+ Each window handle is assigned by the server, and the list of
+ strings returned does not have a guaranteed ordering.
+
+ :returns: Unordered list of unique chrome window handles as strings
+ """
+ return self._send_message(
+ "getChromeWindowHandles", key="value" if self.protocol == 1 else None)
+
+ @property
+ def page_source(self):
+ """A string representation of the DOM."""
+ return self._send_message("getPageSource", key="value")
+
+ def close(self):
+ """Close the current window, ending the session if it's the last
+ window currently open.
+
+ :returns: Unordered list of remaining unique window handles as strings
+ """
+ return self._send_message("close")
+
+ def close_chrome_window(self):
+ """Close the currently selected chrome window, ending the session
+ if it's the last window open.
+
+ :returns: Unordered list of remaining unique chrome window handles as strings
+ """
+ return self._send_message("closeChromeWindow")
+
+ def set_context(self, context):
+ """Sets the context that Marionette commands are running in.
+
+ :param context: Context, may be one of the class properties
+ `CONTEXT_CHROME` or `CONTEXT_CONTENT`.
+
+ Usage example::
+
+ marionette.set_context(marionette.CONTEXT_CHROME)
+ """
+ if context not in [self.CONTEXT_CHROME, self.CONTEXT_CONTENT]:
+ raise ValueError("Unknown context: {}".format(context))
+ self._send_message("setContext", {"value": context})
+
+ @contextmanager
+ def using_context(self, context):
+ """Sets the context that Marionette commands are running in using
+ a `with` statement. The state of the context on the server is
+ saved before entering the block, and restored upon exiting it.
+
+ :param context: Context, may be one of the class properties
+ `CONTEXT_CHROME` or `CONTEXT_CONTENT`.
+
+ Usage example::
+
+ with marionette.using_context(marionette.CONTEXT_CHROME):
+ # chrome scope
+ ... do stuff ...
+ """
+ scope = self._send_message("getContext", key="value")
+ self.set_context(context)
+ try:
+ yield
+ finally:
+ self.set_context(scope)
+
+ def switch_to_alert(self):
+ """Returns an Alert object for interacting with a currently
+ displayed alert.
+
+ ::
+
+ alert = self.marionette.switch_to_alert()
+ text = alert.text
+ alert.accept()
+ """
+ return Alert(self)
+
+ def switch_to_window(self, window_id, focus=True):
+ """Switch to the specified window; subsequent commands will be
+ directed at the new window.
+
+ :param window_id: The id or name of the window to switch to.
+
+ :param focus: A boolean value which determins whether to focus
+ the window that we just switched to.
+ """
+ body = {"focus": focus, "name": window_id}
+ self._send_message("switchToWindow", body)
+ self.window = window_id
+
+ def get_active_frame(self):
+ """Returns an HTMLElement representing the frame Marionette is
+ currently acting on."""
+ return self._send_message("getActiveFrame", key="value")
+
+ def switch_to_default_content(self):
+ """Switch the current context to page's default content."""
+ return self.switch_to_frame()
+
+ def switch_to_parent_frame(self):
+ """
+ Switch to the Parent Frame
+ """
+ self._send_message("switchToParentFrame")
+
+ def switch_to_frame(self, frame=None, focus=True):
+ """Switch the current context to the specified frame. Subsequent
+ commands will operate in the context of the specified frame,
+ if applicable.
+
+ :param frame: A reference to the frame to switch to. This can
+ be an ``HTMLElement``, an integer index, string name, or an
+ ID attribute. If you call ``switch_to_frame`` without an
+ argument, it will switch to the top-level frame.
+
+ :param focus: A boolean value which determins whether to focus
+ the frame that we just switched to.
+ """
+ body = {"focus": focus}
+ if isinstance(frame, HTMLElement):
+ body["element"] = frame.id
+ elif frame is not None:
+ body["id"] = frame
+ self._send_message("switchToFrame", body)
+
+ def switch_to_shadow_root(self, host=None):
+ """Switch the current context to the specified host's Shadow DOM.
+ Subsequent commands will operate in the context of the specified Shadow
+ DOM, if applicable.
+
+ :param host: A reference to the host element containing Shadow DOM.
+ This can be an ``HTMLElement``. If you call
+ ``switch_to_shadow_root`` without an argument, it will switch to the
+ parent Shadow DOM or the top-level frame.
+ """
+ body = {}
+ if isinstance(host, HTMLElement):
+ body["id"] = host.id
+ return self._send_message("switchToShadowRoot", body)
+
+ def get_url(self):
+ """Get a string representing the current URL.
+
+ On Desktop this returns a string representation of the URL of
+ the current top level browsing context. This is equivalent to
+ document.location.href.
+
+ When in the context of the chrome, this returns the canonical
+ URL of the current resource.
+
+ :returns: string representation of URL
+ """
+ return self._send_message("getCurrentUrl", key="value")
+
+ def get_window_type(self):
+ """Gets the windowtype attribute of the window Marionette is
+ currently acting on.
+
+ This command only makes sense in a chrome context. You might use this
+ method to distinguish a browser window from an editor window.
+ """
+ return self._send_message("getWindowType", key="value")
+
+ def navigate(self, url):
+ """Navigate to given `url`.
+
+ Navigates the current top-level browsing context's content
+ frame to the given URL and waits for the document to load or
+ the session's page timeout duration to elapse before returning.
+
+ The command will return with a failure if there is an error
+ loading the document or the URL is blocked. This can occur if
+ it fails to reach the host, the URL is malformed, the page is
+ restricted (about:* pages), or if there is a certificate issue
+ to name some examples.
+
+ The document is considered successfully loaded when the
+ `DOMContentLoaded` event on the frame element associated with the
+ `window` triggers and `document.readState` is "complete".
+
+ In chrome context it will change the current `window`'s location
+ to the supplied URL and wait until `document.readState` equals
+ "complete" or the page timeout duration has elapsed.
+
+ :param url: The URL to navigate to.
+ """
+ self._send_message("get", {"url": url})
+
+ def go_back(self):
+ """Causes the browser to perform a back navigation."""
+ self._send_message("goBack")
+
+ def go_forward(self):
+ """Causes the browser to perform a forward navigation."""
+ self._send_message("goForward")
+
+ def refresh(self):
+ """Causes the browser to perform to refresh the current page."""
+ self._send_message("refresh")
+
+ def _to_json(self, args):
+ if isinstance(args, list) or isinstance(args, tuple):
+ wrapped = []
+ for arg in args:
+ wrapped.append(self._to_json(arg))
+ elif isinstance(args, dict):
+ wrapped = {}
+ for arg in args:
+ wrapped[arg] = self._to_json(args[arg])
+ elif type(args) == HTMLElement:
+ wrapped = {W3C_WEBELEMENT_KEY: args.id,
+ WEBELEMENT_KEY: args.id}
+ elif (isinstance(args, bool) or isinstance(args, basestring) or
+ isinstance(args, int) or isinstance(args, float) or args is None):
+ wrapped = args
+ return wrapped
+
+ def _from_json(self, value):
+ if isinstance(value, list):
+ unwrapped = []
+ for item in value:
+ unwrapped.append(self._from_json(item))
+ elif isinstance(value, dict):
+ unwrapped = {}
+ for key in value:
+ if key == W3C_WEBELEMENT_KEY:
+ unwrapped = HTMLElement(self, value[key])
+ break
+ elif key == WEBELEMENT_KEY:
+ unwrapped = HTMLElement(self, value[key])
+ break
+ else:
+ unwrapped[key] = self._from_json(value[key])
+ else:
+ unwrapped = value
+ return unwrapped
+
+ def execute_js_script(self, script, script_args=(), async=True,
+ new_sandbox=True, script_timeout=None,
+ inactivity_timeout=None, filename=None,
+ sandbox='default'):
+ args = self._to_json(script_args)
+ body = {"script": script,
+ "args": args,
+ "async": async,
+ "newSandbox": new_sandbox,
+ "scriptTimeout": script_timeout,
+ "inactivityTimeout": inactivity_timeout,
+ "filename": filename,
+ "line": None}
+ rv = self._send_message("executeJSScript", body, key="value")
+ return self._from_json(rv)
+
+ def execute_script(self, script, script_args=(), new_sandbox=True,
+ sandbox="default", script_timeout=None):
+ """Executes a synchronous JavaScript script, and returns the
+ result (or None if the script does return a value).
+
+ The script is executed in the context set by the most recent
+ set_context() call, or to the CONTEXT_CONTENT context if set_context()
+ has not been called.
+
+ :param script: A string containing the JavaScript to execute.
+ :param script_args: An interable of arguments to pass to the script.
+ :param sandbox: A tag referring to the sandbox you wish to use;
+ if you specify a new tag, a new sandbox will be created.
+ If you use the special tag `system`, the sandbox will
+ be created using the system principal which has elevated
+ privileges.
+ :param new_sandbox: If False, preserve global variables from
+ the last execute_*script call. This is True by default, in which
+ case no globals are preserved.
+
+ Simple usage example:
+
+ ::
+
+ result = marionette.execute_script("return 1;")
+ assert result == 1
+
+ You can use the `script_args` parameter to pass arguments to the
+ script:
+
+ ::
+
+ result = marionette.execute_script("return arguments[0] + arguments[1];",
+ script_args=(2, 3,))
+ assert result == 5
+ some_element = marionette.find_element(By.ID, "someElement")
+ sid = marionette.execute_script("return arguments[0].id;", script_args=(some_element,))
+ assert some_element.get_attribute("id") == sid
+
+ Scripts wishing to access non-standard properties of the window
+ object must use window.wrappedJSObject:
+
+ ::
+
+ result = marionette.execute_script('''
+ window.wrappedJSObject.test1 = "foo";
+ window.wrappedJSObject.test2 = "bar";
+ return window.wrappedJSObject.test1 + window.wrappedJSObject.test2;
+ ''')
+ assert result == "foobar"
+
+ Global variables set by individual scripts do not persist between
+ script calls by default. If you wish to persist data between
+ script calls, you can set new_sandbox to False on your next call,
+ and add any new variables to a new 'global' object like this:
+
+ ::
+
+ marionette.execute_script("global.test1 = 'foo';")
+ result = self.marionette.execute_script("return global.test1;", new_sandbox=False)
+ assert result == "foo"
+
+ """
+ args = self._to_json(script_args)
+ stack = traceback.extract_stack()
+ frame = stack[-2:-1][0] # grab the second-to-last frame
+ body = {"script": script,
+ "args": args,
+ "newSandbox": new_sandbox,
+ "sandbox": sandbox,
+ "scriptTimeout": script_timeout,
+ "line": int(frame[1]),
+ "filename": os.path.basename(frame[0])}
+ rv = self._send_message("executeScript", body, key="value")
+ return self._from_json(rv)
+
+ def execute_async_script(self, script, script_args=(), new_sandbox=True,
+ sandbox="default", script_timeout=None,
+ debug_script=False):
+ """Executes an asynchronous JavaScript script, and returns the
+ result (or None if the script does return a value).
+
+ The script is executed in the context set by the most recent
+ set_context() call, or to the CONTEXT_CONTENT context if
+ set_context() has not been called.
+
+ :param script: A string containing the JavaScript to execute.
+ :param script_args: An interable of arguments to pass to the script.
+ :param sandbox: A tag referring to the sandbox you wish to use; if
+ you specify a new tag, a new sandbox will be created. If you
+ use the special tag `system`, the sandbox will be created
+ using the system principal which has elevated privileges.
+ :param new_sandbox: If False, preserve global variables from
+ the last execute_*script call. This is True by default,
+ in which case no globals are preserved.
+ :param debug_script: Capture javascript exceptions when in
+ `CONTEXT_CHROME` context.
+
+ Usage example:
+
+ ::
+
+ marionette.timeout.script = 10
+ result = self.marionette.execute_async_script('''
+ // this script waits 5 seconds, and then returns the number 1
+ setTimeout(function() {
+ marionetteScriptFinished(1);
+ }, 5000);
+ ''')
+ assert result == 1
+ """
+ args = self._to_json(script_args)
+ stack = traceback.extract_stack()
+ frame = stack[-2:-1][0] # grab the second-to-last frame
+ body = {"script": script,
+ "args": args,
+ "newSandbox": new_sandbox,
+ "sandbox": sandbox,
+ "scriptTimeout": script_timeout,
+ "line": int(frame[1]),
+ "filename": os.path.basename(frame[0]),
+ "debug_script": debug_script}
+ rv = self._send_message("executeAsyncScript", body, key="value")
+ return self._from_json(rv)
+
+ def find_element(self, method, target, id=None):
+ """Returns an HTMLElement instances that matches the specified
+ method and target in the current context.
+
+ An HTMLElement instance may be used to call other methods on the
+ element, such as click(). If no element is immediately found, the
+ attempt to locate an element will be repeated for up to the amount of
+ time set by ``timeout.implicit``. If multiple elements match the given
+ criteria, only the first is returned. If no element matches, a
+ NoSuchElementException will be raised.
+
+ :param method: The method to use to locate the element; one of:
+ "id", "name", "class name", "tag name", "css selector",
+ "link text", "partial link text", "xpath", "anon" and "anon
+ attribute". Note that the "name", "link text" and "partial
+ link test" methods are not supported in the chrome DOM.
+ :param target: The target of the search. For example, if method =
+ "tag", target might equal "div". If method = "id", target would
+ be an element id.
+ :param id: If specified, search for elements only inside the element
+ with the specified id.
+ """
+ body = {"value": target, "using": method}
+ if id:
+ body["element"] = id
+ return self._send_message("findElement", body, key="value")
+
+ def find_elements(self, method, target, id=None):
+ """Returns a list of all HTMLElement instances that match the
+ specified method and target in the current context.
+
+ An HTMLElement instance may be used to call other methods on the
+ element, such as click(). If no element is immediately found,
+ the attempt to locate an element will be repeated for up to the
+ amount of time set by ``timeout.implicit``.
+
+ :param method: The method to use to locate the elements; one
+ of: "id", "name", "class name", "tag name", "css selector",
+ "link text", "partial link text", "xpath", "anon" and "anon
+ attribute". Note that the "name", "link text" and "partial link
+ test" methods are not supported in the chrome DOM.
+ :param target: The target of the search. For example, if method =
+ "tag", target might equal "div". If method = "id", target would be
+ an element id.
+ :param id: If specified, search for elements only inside the element
+ with the specified id.
+ """
+ body = {"value": target, "using": method}
+ if id:
+ body["element"] = id
+ return self._send_message(
+ "findElements", body, key="value" if self.protocol == 1 else None)
+
+ def get_active_element(self):
+ el_or_ref = self._send_message("getActiveElement", key="value")
+ if self.protocol < 3:
+ return HTMLElement(self, el_or_ref)
+ return el_or_ref
+
+ def log(self, msg, level="INFO"):
+ """Stores a timestamped log message in the Marionette server
+ for later retrieval.
+
+ :param msg: String with message to log.
+ :param level: String with log level (e.g. "INFO" or "DEBUG").
+ Defaults to "INFO".
+ """
+ body = {"value": msg, "level": level}
+ self._send_message("log", body)
+
+ def get_logs(self):
+ """Returns the list of logged messages.
+
+ Each log message is an array with three string elements: the level,
+ the message, and a date.
+
+ Usage example::
+
+ marionette.log("I AM INFO")
+ marionette.log("I AM ERROR", "ERROR")
+ logs = marionette.get_logs()
+ assert logs[0][1] == "I AM INFO"
+ assert logs[1][1] == "I AM ERROR"
+ """
+ return self._send_message("getLogs",
+ key="value" if self.protocol == 1 else None)
+
+ def import_script(self, js_file):
+ """Imports a script into the scope of the execute_script and
+ execute_async_script calls.
+
+ This is particularly useful if you wish to import your own
+ libraries.
+
+ :param js_file: Filename of JavaScript file to import.
+
+ For example, Say you have a script, importfunc.js, that contains:
+
+ ::
+
+ let testFunc = function() { return "i'm a test function!";};
+
+ Assuming this file is in the same directory as the test, you
+ could do something like:
+
+ ::
+
+ js = os.path.abspath(os.path.join(__file__, os.path.pardir, "importfunc.js"))
+ marionette.import_script(js)
+ assert "i'm a test function!" == self.marionette.execute_script("return testFunc();")
+ """
+ js = ""
+ with open(js_file, "r") as f:
+ js = f.read()
+ body = {"script": js}
+ self._send_message("importScript", body)
+
+ def clear_imported_scripts(self):
+ """Clears all imported scripts in this context, ie: calling
+ clear_imported_scripts in chrome context will clear only scripts
+ you imported in chrome, and will leave the scripts you imported
+ in content context.
+ """
+ self._send_message("clearImportedScripts")
+
+ def add_cookie(self, cookie):
+ """Adds a cookie to your current session.
+
+ :param cookie: A dictionary object, with required keys - "name"
+ and "value"; optional keys - "path", "domain", "secure",
+ "expiry".
+
+ Usage example:
+
+ ::
+
+ driver.add_cookie({"name": "foo", "value": "bar"})
+ driver.add_cookie({"name": "foo", "value": "bar", "path": "/"})
+ driver.add_cookie({"name": "foo", "value": "bar", "path": "/",
+ "secure": True})
+ """
+ body = {"cookie": cookie}
+ self._send_message("addCookie", body)
+
+ def delete_all_cookies(self):
+ """Delete all cookies in the scope of the current session.
+
+ Usage example:
+
+ ::
+
+ driver.delete_all_cookies()
+ """
+ self._send_message("deleteAllCookies")
+
+ def delete_cookie(self, name):
+ """Delete a cookie by its name.
+
+ :param name: Name of cookie to delete.
+
+ Usage example:
+
+ ::
+
+ driver.delete_cookie("foo")
+ """
+ self._send_message("deleteCookie", {"name": name})
+
+ def get_cookie(self, name):
+ """Get a single cookie by name. Returns the cookie if found,
+ None if not.
+
+ :param name: Name of cookie to get.
+ """
+ cookies = self.get_cookies()
+ for cookie in cookies:
+ if cookie["name"] == name:
+ return cookie
+ return None
+
+ def get_cookies(self):
+ """Get all the cookies for the current domain.
+
+ This is the equivalent of calling `document.cookie` and
+ parsing the result.
+
+ :returns: A list of cookies for the current domain.
+ """
+ return self._send_message("getCookies", key="value" if self.protocol == 1 else None)
+
+ def screenshot(self, element=None, highlights=None, format="base64",
+ full=True, scroll=True):
+ """Takes a screenshot of a web element or the current frame.
+
+ The screen capture is returned as a lossless PNG image encoded
+ as a base 64 string by default. If the `element` argument is defined the
+ capture area will be limited to the bounding box of that
+ element. Otherwise, the capture area will be the bounding box
+ of the current frame.
+
+ :param element: The element to take a screenshot of. If None, will
+ take a screenshot of the current frame.
+
+ :param highlights: A list of HTMLElement objects to draw a red
+ box around in the returned screenshot.
+
+ :param format: if "base64" (the default), returns the screenshot
+ as a base64-string. If "binary", the data is decoded and
+ returned as raw binary. If "hash", the data is hashed using
+ the SHA-256 algorithm and the result is returned as a hex digest.
+
+ :param full: If True (the default), the capture area will be the
+ complete frame. Else only the viewport is captured. Only applies
+ when `element` is None.
+
+ :param scroll: When `element` is provided, scroll to it before
+ taking the screenshot (default). Otherwise, avoid scrolling
+ `element` into view.
+ """
+
+ if element:
+ element = element.id
+ lights = None
+ if highlights:
+ lights = [highlight.id for highlight in highlights]
+
+ body = {"id": element,
+ "highlights": lights,
+ "full": full,
+ "hash": False,
+ "scroll": scroll}
+ if format == "hash":
+ body["hash"] = True
+ data = self._send_message("takeScreenshot", body, key="value")
+
+ if format == "base64" or format == "hash":
+ return data
+ elif format == "binary":
+ return base64.b64decode(data.encode("ascii"))
+ else:
+ raise ValueError("format parameter must be either 'base64'"
+ " or 'binary', not {0}".format(repr(format)))
+
+ @property
+ def orientation(self):
+ """Get the current browser orientation.
+
+ Will return one of the valid primary orientation values
+ portrait-primary, landscape-primary, portrait-secondary, or
+ landscape-secondary.
+ """
+ return self._send_message("getScreenOrientation", key="value")
+
+ def set_orientation(self, orientation):
+ """Set the current browser orientation.
+
+ The supplied orientation should be given as one of the valid
+ orientation values. If the orientation is unknown, an error
+ will be raised.
+
+ Valid orientations are "portrait" and "landscape", which fall
+ back to "portrait-primary" and "landscape-primary"
+ respectively, and "portrait-secondary" as well as
+ "landscape-secondary".
+
+ :param orientation: The orientation to lock the screen in.
+ """
+ body = {"orientation": orientation}
+ self._send_message("setScreenOrientation", body)
+
+ @property
+ def window_size(self):
+ """Get the current browser window size.
+
+ Will return the current browser window size in pixels. Refers to
+ window outerWidth and outerHeight values, which include scroll bars,
+ title bars, etc.
+
+ :returns: dictionary representation of current window width and height
+ """
+ return self._send_message("getWindowSize",
+ key="value" if self.protocol == 1 else None)
+
+ def set_window_size(self, width, height):
+ """Resize the browser window currently in focus.
+
+ The supplied width and height values refer to the window outerWidth
+ and outerHeight values, which include scroll bars, title bars, etc.
+
+ An error will be returned if the requested window size would result
+ in the window being in the maximised state.
+
+ :param width: The width to resize the window to.
+ :param height: The height to resize the window to.
+
+ """
+ body = {"width": width, "height": height}
+ return self._send_message("setWindowSize", body)
+
+ def maximize_window(self):
+ """ Resize the browser window currently receiving commands. The action
+ should be equivalent to the user pressing the the maximize button
+ """
+ return self._send_message("maximizeWindow")
diff --git a/testing/marionette/client/marionette_driver/selection.py b/testing/marionette/client/marionette_driver/selection.py
new file mode 100644
index 000000000..30e66deaa
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/selection.py
@@ -0,0 +1,227 @@
+# -*- coding: utf-8 -*-
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+class SelectionManager(object):
+ '''Interface for manipulating the selection and carets of the element.
+
+ We call the blinking cursor (nsCaret) as cursor, and call AccessibleCaret as
+ caret for short.
+
+ Simple usage example:
+
+ ::
+
+ element = marionette.find_element(By.ID, 'input')
+ sel = SelectionManager(element)
+ sel.move_caret_to_front()
+
+ '''
+
+ def __init__(self, element):
+ self.element = element
+
+ def _input_or_textarea(self):
+ '''Return True if element is either <input> or <textarea>.'''
+ return self.element.tag_name in ('input', 'textarea')
+
+ def js_selection_cmd(self):
+ '''Return a command snippet to get selection object.
+
+ If the element is <input> or <textarea>, return the selection object
+ associated with it. Otherwise, return the current selection object.
+
+ Note: "element" must be provided as the first argument to
+ execute_script().
+
+ '''
+ if self._input_or_textarea():
+ # We must unwrap sel so that DOMRect could be returned to Python
+ # side.
+ return '''var sel = arguments[0].editor.selection;'''
+ else:
+ return '''var sel = window.getSelection();'''
+
+ def move_cursor_by_offset(self, offset, backward=False):
+ '''Move cursor in the element by character offset.
+
+ :param offset: Move the cursor to the direction by offset characters.
+ :param backward: Optional, True to move backward; Default to False to
+ move forward.
+
+ '''
+ cmd = self.js_selection_cmd() + '''
+ for (let i = 0; i < {0}; ++i) {{
+ sel.modify("move", "{1}", "character");
+ }}
+ '''.format(offset, 'backward' if backward else 'forward')
+
+ self.element.marionette.execute_script(
+ cmd, script_args=[self.element], sandbox='system')
+
+ def move_cursor_to_front(self):
+ '''Move cursor in the element to the front of the content.'''
+ if self._input_or_textarea():
+ cmd = '''arguments[0].setSelectionRange(0, 0);'''
+ else:
+ cmd = '''var sel = window.getSelection();
+ sel.collapse(arguments[0].firstChild, 0);'''
+
+ self.element.marionette.execute_script(cmd, script_args=[self.element])
+
+ def move_cursor_to_end(self):
+ '''Move cursor in the element to the end of the content.'''
+ if self._input_or_textarea():
+ cmd = '''var len = arguments[0].value.length;
+ arguments[0].setSelectionRange(len, len);'''
+ else:
+ cmd = '''var sel = window.getSelection();
+ sel.collapse(arguments[0].lastChild, arguments[0].lastChild.length);'''
+
+ self.element.marionette.execute_script(cmd, script_args=[self.element])
+
+ def selection_rect_list(self, idx):
+ '''Return the selection's DOMRectList object for the range at given idx.
+
+ If the element is either <input> or <textarea>, return the DOMRectList of
+ the range at given idx of the selection within the element. Otherwise,
+ return the DOMRectList of the of the range at given idx of current selection.
+
+ '''
+ cmd = self.js_selection_cmd() +\
+ '''return sel.getRangeAt({}).getClientRects();'''.format(idx)
+ return self.element.marionette.execute_script(cmd,
+ script_args=[self.element],
+ sandbox='system')
+
+ def range_count(self):
+ '''Get selection's range count'''
+ cmd = self.js_selection_cmd() +\
+ '''return sel.rangeCount;'''
+ return self.element.marionette.execute_script(cmd,
+ script_args=[self.element],
+ sandbox='system')
+
+ def _selection_location_helper(self, location_type):
+ '''Return the start and end location of the selection in the element.
+
+ Return a tuple containing two pairs of (x, y) coordinates of the start
+ and end locations in the element. The coordinates are relative to the
+ top left-hand corner of the element. Both ltr and rtl directions are
+ considered.
+
+ '''
+ range_count = self.range_count()
+ first_rect_list = self.selection_rect_list(0)
+ last_rect_list = self.selection_rect_list(range_count - 1)
+ last_list_length = last_rect_list['length']
+ first_rect, last_rect = first_rect_list['0'], last_rect_list[str(last_list_length - 1)]
+ origin_x, origin_y = self.element.rect['x'], self.element.rect['y']
+
+ if self.element.get_property('dir') == 'rtl': # such as Arabic
+ start_pos, end_pos = 'right', 'left'
+ else:
+ start_pos, end_pos = 'left', 'right'
+
+ # Calculate y offset according to different needs.
+ if location_type == 'center':
+ start_y_offset = first_rect['height'] / 2.0
+ end_y_offset = last_rect['height'] / 2.0
+ elif location_type == 'caret':
+ # Selection carets' tip are below the bottom of the two ends of the
+ # selection. Add 5px to y should be sufficient to locate them.
+ caret_tip_y_offset = 5
+ start_y_offset = first_rect['height'] + caret_tip_y_offset
+ end_y_offset = last_rect['height'] + caret_tip_y_offset
+ else:
+ start_y_offset = end_y_offset = 0
+
+ caret1_x = first_rect[start_pos] - origin_x
+ caret1_y = first_rect['top'] + start_y_offset - origin_y
+ caret2_x = last_rect[end_pos] - origin_x
+ caret2_y = last_rect['top'] + end_y_offset - origin_y
+
+ return ((caret1_x, caret1_y), (caret2_x, caret2_y))
+
+ def selection_location(self):
+ '''Return the start and end location of the selection in the element.
+
+ Return a tuple containing two pairs of (x, y) coordinates of the start
+ and end of the selection. The coordinates are relative to the top
+ left-hand corner of the element. Both ltr and rtl direction are
+ considered.
+
+ '''
+ return self._selection_location_helper('center')
+
+ def carets_location(self):
+ '''Return a pair of the two carets' location.
+
+ Return a tuple containing two pairs of (x, y) coordinates of the two
+ carets' tip. The coordinates are relative to the top left-hand corner of
+ the element. Both ltr and rtl direction are considered.
+
+ '''
+ return self._selection_location_helper('caret')
+
+ def cursor_location(self):
+ '''Return the blanking cursor's center location within the element.
+
+ Return (x, y) coordinates of the cursor's center relative to the top
+ left-hand corner of the element.
+
+ '''
+ return self._selection_location_helper('center')[0]
+
+ def first_caret_location(self):
+ '''Return the first caret's location.
+
+ Return (x, y) coordinates of the first caret's tip relative to the top
+ left-hand corner of the element.
+
+ '''
+ return self.carets_location()[0]
+
+ def second_caret_location(self):
+ '''Return the second caret's location.
+
+ Return (x, y) coordinates of the second caret's tip relative to the top
+ left-hand corner of the element.
+
+ '''
+ return self.carets_location()[1]
+
+ def select_all(self):
+ '''Select all the content in the element.'''
+ if self._input_or_textarea():
+ cmd = '''var len = arguments[0].value.length;
+ arguments[0].focus();
+ arguments[0].setSelectionRange(0, len);'''
+ else:
+ cmd = '''var range = document.createRange();
+ range.setStart(arguments[0].firstChild, 0);
+ range.setEnd(arguments[0].lastChild, arguments[0].lastChild.length);
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);'''
+
+ self.element.marionette.execute_script(cmd, script_args=[self.element])
+
+ @property
+ def content(self):
+ '''Return all the content of the element.'''
+ if self._input_or_textarea():
+ return self.element.get_property('value')
+ else:
+ return self.element.text
+
+ @property
+ def selected_content(self):
+ '''Return the selected portion of the content in the element.'''
+ cmd = self.js_selection_cmd() +\
+ '''return sel.toString();'''
+ return self.element.marionette.execute_script(cmd,
+ script_args=[self.element],
+ sandbox='system')
diff --git a/testing/marionette/client/marionette_driver/timeout.py b/testing/marionette/client/marionette_driver/timeout.py
new file mode 100644
index 000000000..e2fa94f4f
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/timeout.py
@@ -0,0 +1,98 @@
+# 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 errors
+
+
+DEFAULT_SCRIPT_TIMEOUT = 30
+DEFAULT_PAGE_LOAD_TIMEOUT = 300
+DEFAULT_IMPLICIT_WAIT_TIMEOUT = 0
+
+
+class Timeouts(object):
+ """Manage timeout settings in the Marionette session.
+
+ Usage::
+
+ marionette = Marionette(...)
+ marionette.start_session()
+ marionette.timeout.page_load = 10
+ marionette.timeout.page_load
+ # => 10
+
+ """
+
+ def __init__(self, marionette):
+ self._marionette = marionette
+
+ def _set(self, name, sec):
+ ms = sec * 1000
+ try:
+ self._marionette._send_message("setTimeouts", {name: ms})
+ except errors.UnknownCommandException:
+ # remove when 55 is stable
+ self._marionette._send_message("timeouts", {"type": name, "ms": ms})
+
+ def _get(self, name):
+ ms = self._marionette._send_message("getTimeouts", key=name)
+ return ms / 1000
+
+ @property
+ def script(self):
+ """Get the session's script timeout. This specifies the time
+ to wait for injected scripts to finished before interrupting
+ them. It is by default 30 seconds.
+
+ """
+ return self._get("script")
+
+ @script.setter
+ def script(self, sec):
+ """Set the session's script timeout. This specifies the time
+ to wait for injected scripts to finish before interrupting them.
+
+ """
+ self._set("script", sec)
+
+ @property
+ def page_load(self):
+ """Get the session's page load timeout. This specifies the time
+ to wait for the page loading to complete. It is by default 5
+ minutes (or 300 seconds).
+
+ """
+ return self._get("page load")
+
+ @page_load.setter
+ def page_load(self, sec):
+ """Set the session's page load timeout. This specifies the time
+ to wait for the page loading to complete.
+
+ """
+ self._set("page load", sec)
+
+ @property
+ def implicit(self):
+ """Get the session's implicit wait timeout. This specifies the
+ time to wait for the implicit element location strategy when
+ retrieving elements. It is by default disabled (0 seconds).
+
+ """
+ return self._get("implicit")
+
+ @implicit.setter
+ def implicit(self, sec):
+ """Set the session's implicit wait timeout. This specifies the
+ time to wait for the implicit element location strategy when
+ retrieving elements.
+
+ """
+ self._set("implicit", sec)
+
+ def reset(self):
+ """Resets timeouts to their default values."""
+ self.script = DEFAULT_SCRIPT_TIMEOUT
+ self.page_load = DEFAULT_PAGE_LOAD_TIMEOUT
+ self.implicit = DEFAULT_IMPLICIT_WAIT_TIMEOUT
diff --git a/testing/marionette/client/marionette_driver/transport.py b/testing/marionette/client/marionette_driver/transport.py
new file mode 100644
index 000000000..82828fdef
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/transport.py
@@ -0,0 +1,300 @@
+# 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 json
+import socket
+import time
+
+
+class SocketTimeout(object):
+ def __init__(self, socket, timeout):
+ self.sock = socket
+ self.timeout = timeout
+ self.old_timeout = None
+
+ def __enter__(self):
+ self.old_timeout = self.sock.gettimeout()
+ self.sock.settimeout(self.timeout)
+
+ def __exit__(self, *args, **kwargs):
+ self.sock.settimeout(self.old_timeout)
+
+
+class Message(object):
+ def __init__(self, msgid):
+ self.id = msgid
+
+ def __eq__(self, other):
+ return self.id == other.id
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class Command(Message):
+ TYPE = 0
+
+ def __init__(self, msgid, name, params):
+ Message.__init__(self, msgid)
+ self.name = name
+ self.params = params
+
+ def __str__(self):
+ return "<Command id={0}, name={1}, params={2}>".format(self.id, self.name, self.params)
+
+ def to_msg(self):
+ msg = [Command.TYPE, self.id, self.name, self.params]
+ return json.dumps(msg)
+
+ @staticmethod
+ def from_msg(payload):
+ data = json.loads(payload)
+ assert data[0] == Command.TYPE
+ cmd = Command(data[1], data[2], data[3])
+ return cmd
+
+
+class Response(Message):
+ TYPE = 1
+
+ def __init__(self, msgid, error, result):
+ Message.__init__(self, msgid)
+ self.error = error
+ self.result = result
+
+ def __str__(self):
+ return "<Response id={0}, error={1}, result={2}>".format(self.id, self.error, self.result)
+
+ def to_msg(self):
+ msg = [Response.TYPE, self.id, self.error, self.result]
+ return json.dumps(msg)
+
+ @staticmethod
+ def from_msg(payload):
+ data = json.loads(payload)
+ assert data[0] == Response.TYPE
+ return Response(data[1], data[2], data[3])
+
+
+class Proto2Command(Command):
+ """Compatibility shim that marshals messages from a protocol level
+ 2 and below remote into ``Command`` objects.
+ """
+
+ def __init__(self, name, params):
+ Command.__init__(self, None, name, params)
+
+
+class Proto2Response(Response):
+ """Compatibility shim that marshals messages from a protocol level
+ 2 and below remote into ``Response`` objects.
+ """
+
+ def __init__(self, error, result):
+ Response.__init__(self, None, error, result)
+
+ @staticmethod
+ def from_data(data):
+ err, res = None, None
+ if "error" in data:
+ err = data
+ else:
+ res = data
+ return Proto2Response(err, res)
+
+
+class TcpTransport(object):
+ """Socket client that communciates with Marionette via TCP.
+
+ It speaks the protocol of the remote debugger in Gecko, in which
+ messages are always preceded by the message length and a colon, e.g.:
+
+ 7:MESSAGE
+
+ On top of this protocol it uses a Marionette message format, that
+ depending on the protocol level offered by the remote server, varies.
+ Supported protocol levels are 1 and above.
+ """
+ max_packet_length = 4096
+
+ def __init__(self, addr, port, socket_timeout=60.0):
+ """If `socket_timeout` is `0` or `0.0`, non-blocking socket mode
+ will be used. Setting it to `1` or `None` disables timeouts on
+ socket operations altogether.
+ """
+ self.addr = addr
+ self.port = port
+ self._socket_timeout = socket_timeout
+
+ self.protocol = 1
+ self.application_type = None
+ self.last_id = 0
+ self.expected_response = None
+ self.sock = None
+
+ @property
+ def socket_timeout(self):
+ return self._socket_timeout
+
+ @socket_timeout.setter
+ def socket_timeout(self, value):
+ if self.sock:
+ self.sock.settimeout(value)
+ self._socket_timeout = value
+
+ def _unmarshal(self, packet):
+ msg = None
+
+ # protocol 3 and above
+ if self.protocol >= 3:
+ typ = int(packet[1])
+ if typ == Command.TYPE:
+ msg = Command.from_msg(packet)
+ elif typ == Response.TYPE:
+ msg = Response.from_msg(packet)
+
+ # protocol 2 and below
+ else:
+ data = json.loads(packet)
+
+ msg = Proto2Response.from_data(data)
+
+ return msg
+
+ def receive(self, unmarshal=True):
+ """Wait for the next complete response from the remote.
+
+ :param unmarshal: Default is to deserialise the packet and
+ return a ``Message`` type. Setting this to false will return
+ the raw packet.
+ """
+ now = time.time()
+ data = ""
+ bytes_to_recv = 10
+
+ while self.socket_timeout is None or (time.time() - now < self.socket_timeout):
+ try:
+ chunk = self.sock.recv(bytes_to_recv)
+ data += chunk
+ except socket.timeout:
+ pass
+ else:
+ if not chunk:
+ raise socket.error("No data received over socket")
+
+ sep = data.find(":")
+ if sep > -1:
+ length = data[0:sep]
+ remaining = data[sep + 1:]
+
+ if len(remaining) == int(length):
+ if unmarshal:
+ msg = self._unmarshal(remaining)
+ self.last_id = msg.id
+
+ if self.protocol >= 3:
+ self.last_id = msg.id
+
+ # keep reading incoming responses until
+ # we receive the user's expected response
+ if isinstance(msg, Response) and msg != self.expected_response:
+ return self.receive(unmarshal)
+
+ return msg
+
+ else:
+ return remaining
+
+ bytes_to_recv = int(length) - len(remaining)
+
+ raise socket.timeout("Connection timed out after {}s".format(self.socket_timeout))
+
+ def connect(self):
+ """Connect to the server and process the hello message we expect
+ to receive in response.
+
+ Returns a tuple of the protocol level and the application type.
+ """
+ try:
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.sock.settimeout(self.socket_timeout)
+
+ self.sock.connect((self.addr, self.port))
+ except:
+ # Unset self.sock so that the next attempt to send will cause
+ # another connection attempt.
+ self.sock = None
+ raise
+
+ with SocketTimeout(self.sock, 2.0):
+ # first packet is always a JSON Object
+ # which we can use to tell which protocol level we are at
+ raw = self.receive(unmarshal=False)
+ hello = json.loads(raw)
+ self.protocol = hello.get("marionetteProtocol", 1)
+ self.application_type = hello.get("applicationType")
+
+ return (self.protocol, self.application_type)
+
+ def send(self, obj):
+ """Send message to the remote server. Allowed input is a
+ ``Message`` instance or a JSON serialisable object.
+ """
+ if not self.sock:
+ self.connect()
+
+ if isinstance(obj, Message):
+ data = obj.to_msg()
+ if isinstance(obj, Command):
+ self.expected_response = obj
+ else:
+ data = json.dumps(obj)
+ payload = "{0}:{1}".format(len(data), data)
+
+ totalsent = 0
+ while totalsent < len(payload):
+ sent = self.sock.send(payload[totalsent:])
+ if sent == 0:
+ raise IOError("Socket error after sending {0} of {1} bytes"
+ .format(totalsent, len(payload)))
+ else:
+ totalsent += sent
+
+ def respond(self, obj):
+ """Send a response to a command. This can be an arbitrary JSON
+ serialisable object or an ``Exception``.
+ """
+ res, err = None, None
+ if isinstance(obj, Exception):
+ err = obj
+ else:
+ res = obj
+ msg = Response(self.last_id, err, res)
+ self.send(msg)
+ return self.receive()
+
+ def request(self, name, params):
+ """Sends a message to the remote server and waits for a response
+ to come back.
+ """
+ self.last_id = self.last_id + 1
+ cmd = Command(self.last_id, name, params)
+ self.send(cmd)
+ return self.receive()
+
+ def close(self):
+ """Close the socket."""
+ if self.sock:
+ try:
+ self.sock.shutdown(socket.SHUT_RDWR)
+ except IOError as exc:
+ # Errno 57 is "socket not connected", which we don't care about here.
+ if exc.errno != 57:
+ raise
+
+ self.sock.close()
+ self.sock = None
+
+ def __del__(self):
+ self.close()
diff --git a/testing/marionette/client/marionette_driver/wait.py b/testing/marionette/client/marionette_driver/wait.py
new file mode 100644
index 000000000..c89465ce4
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/wait.py
@@ -0,0 +1,167 @@
+# 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 collections
+import errors
+import sys
+import time
+
+DEFAULT_TIMEOUT = 5
+DEFAULT_INTERVAL = 0.1
+
+
+class Wait(object):
+
+ """An explicit conditional utility class for waiting until a condition
+ evaluates to true or not null.
+
+ This will repeatedly evaluate a condition in anticipation for a
+ truthy return value, or its timeout to expire, or its waiting
+ predicate to become true.
+
+ A `Wait` instance defines the maximum amount of time to wait for a
+ condition, as well as the frequency with which to check the
+ condition. Furthermore, the user may configure the wait to ignore
+ specific types of exceptions whilst waiting, such as
+ `errors.NoSuchElementException` when searching for an element on
+ the page.
+
+ """
+
+ def __init__(self, marionette, timeout=DEFAULT_TIMEOUT,
+ interval=DEFAULT_INTERVAL, ignored_exceptions=None,
+ clock=None):
+ """Configure the Wait instance to have a custom timeout, interval, and
+ list of ignored exceptions. Optionally a different time
+ implementation than the one provided by the standard library
+ (time) can also be provided.
+
+ Sample usage::
+
+ # Wait 30 seconds for window to open, checking for its presence once
+ # every 5 seconds.
+ wait = Wait(marionette, timeout=30, interval=5,
+ ignored_exceptions=errors.NoSuchWindowException)
+ window = wait.until(lambda m: m.switch_to_window(42))
+
+ :param marionette: The input value to be provided to
+ conditions, usually a Marionette instance.
+
+ :param timeout: How long to wait for the evaluated condition
+ to become true. The default timeout is
+ `wait.DEFAULT_TIMEOUT`.
+
+ :param interval: How often the condition should be evaluated.
+ In reality the interval may be greater as the cost of
+ evaluating the condition function. If that is not the case the
+ interval for the next condition function call is shortend to keep
+ the original interval sequence as best as possible.
+ The default polling interval is `wait.DEFAULT_INTERVAL`.
+
+ :param ignored_exceptions: Ignore specific types of exceptions
+ whilst waiting for the condition. Any exceptions not
+ whitelisted will be allowed to propagate, terminating the
+ wait.
+
+ :param clock: Allows overriding the use of the runtime's
+ default time library. See `wait.SystemClock` for
+ implementation details.
+
+ """
+
+ self.marionette = marionette
+ self.timeout = timeout
+ self.clock = clock or SystemClock()
+ self.end = self.clock.now + self.timeout
+ self.interval = interval
+
+ exceptions = []
+ if ignored_exceptions is not None:
+ if isinstance(ignored_exceptions, collections.Iterable):
+ exceptions.extend(iter(ignored_exceptions))
+ else:
+ exceptions.append(ignored_exceptions)
+ self.exceptions = tuple(set(exceptions))
+
+ def until(self, condition, is_true=None, message=""):
+ """Repeatedly runs condition until its return value evaluates to true,
+ or its timeout expires or the predicate evaluates to true.
+
+ This will poll at the given interval until the given timeout
+ is reached, or the predicate or conditions returns true. A
+ condition that returns null or does not evaluate to true will
+ fully elapse its timeout before raising an
+ `errors.TimeoutException`.
+
+ If an exception is raised in the condition function and it's
+ not ignored, this function will raise immediately. If the
+ exception is ignored, it will continue polling for the
+ condition until it returns successfully or a
+ `TimeoutException` is raised.
+
+ :param condition: A callable function whose return value will
+ be returned by this function if it evaluates to true.
+
+ :param is_true: An optional predicate that will terminate and
+ return when it evaluates to False. It should be a
+ function that will be passed clock and an end time. The
+ default predicate will terminate a wait when the clock
+ elapses the timeout.
+
+ :param message: An optional message to include in the
+ exception's message if this function times out.
+
+ """
+
+ rv = None
+ last_exc = None
+ until = is_true or until_pred
+ start = self.clock.now
+
+ while not until(self.clock, self.end):
+ try:
+ next = self.clock.now + self.interval
+ rv = condition(self.marionette)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except self.exceptions:
+ last_exc = sys.exc_info()
+
+ # Re-adjust the interval depending on how long the callback
+ # took to evaluate the condition
+ interval_new = max(next - self.clock.now, 0)
+
+ if not rv:
+ self.clock.sleep(interval_new)
+ continue
+
+ if rv is not None:
+ return rv
+
+ self.clock.sleep(interval_new)
+
+ if message:
+ message = " with message: {}".format(message)
+
+ raise errors.TimeoutException(
+ "Timed out after {0} seconds{1}".format(round((self.clock.now - start), 1),
+ message if message else ""),
+ cause=last_exc)
+
+
+def until_pred(clock, end):
+ return clock.now >= end
+
+
+class SystemClock(object):
+
+ def __init__(self):
+ self._time = time
+
+ def sleep(self, duration):
+ self._time.sleep(duration)
+
+ @property
+ def now(self):
+ return self._time.time()