import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,3 @@
[flake8]
max-line-length = 99
exclude = __init__.py,disti/*,build/*,

View file

@ -0,0 +1,2 @@
exclude MANIFEST.in
include requirements.txt

View file

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MarionettePythonClient.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MarionettePythonClient.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/MarionettePythonClient"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MarionettePythonClient"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View file

@ -0,0 +1,46 @@
Actions
=======
.. py:currentmodule:: marionette
Action Sequences
----------------
:class:`Actions` are designed as a way to simulate user input as closely as possible
on a touch device like a smart phone. A common operation is to tap the screen
and drag your finger to another part of the screen and lift it off.
This can be simulated using an Action::
from marionette import Actions
start_element = marionette.find_element('id', 'start')
end_element = marionette.find_element('id', 'end')
action = Actions(marionette)
action.press(start_element).wait(1).move(end_element).release()
action.perform()
This will simulate pressing an element, waiting for one second, moving the
finger over to another element and then lifting the finger off the screen. The
wait is optional in this case, but can be useful for simulating delays typical
to a users behaviour.
Multi-Action Sequences
----------------------
Sometimes it may be necessary to simulate multiple actions at the same time.
For example a user may be dragging one finger while tapping another. This is
where :class:`MultiActions` come in. MultiActions are simply a way of combining
two or more actions together and performing them all at the same time::
action1 = Actions(marionette)
action1.press(start_element).move(end_element).release()
action2 = Actions(marionette)
action2.press(another_element).wait(1).release()
multi = MultiActions(marionette)
multi.add(action1)
multi.add(action2)
multi.perform()

View file

@ -0,0 +1,54 @@
Debugging
=========
.. py:currentmodule:: marionette
Sometimes when working with Marionette you'll run into unexpected behaviour and
need to do some debugging. This page outlines some of the Marionette methods
that can be useful to you.
Please note that the best tools for debugging are the `ones that ship with
Gecko`_. This page doesn't describe how to use those with Marionette. Also see
a related topic about `using the debugger with Marionette`_ on MDN.
.. _ones that ship with Gecko: https://developer.mozilla.org/en-US/docs/Tools
.. _using the debugger with Marionette: https://developer.mozilla.org/en-US/docs/Marionette/Debugging
Storing Logs on the Server
~~~~~~~~~~~~~~~~~~~~~~~~~~
By calling `~Marionette.log` it is possible to store a message on the server.
Logs can later be retrieved using `~Marionette.get_logs`. For example::
try:
marionette.log("Sending a click event") # logged at INFO level
elem.click()
except:
marionette.log("Something went wrong!", "ERROR")
print(marionette.get_logs())
Disclaimer: Example for illustrative purposes only, don't actually hide
tracebacks like that!
Seeing What's on the Page
~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes it's difficult to tell what is actually on the page that is being
manipulated. Either because it happens too fast, the window isn't big enough or
you are manipulating a remote server! There are two methods that can help you
out. The first is `~Marionette.screenshot`::
marionette.screenshot() # takes screenshot of entire frame
elem = marionette.find_element(By.ID, 'some-div')
marionette.screenshot(elem) # takes a screenshot of only the given element
Sometimes you just want to see the DOM layout. You can do this with the
`~Marionette.page_source` property. Note that the page source depends on the
context you are in::
print(marionette.page_source)
marionette.set_context('chrome')
print(marionette.page_source)

View file

@ -0,0 +1,126 @@
Finding Elements
================
.. py:currentmodule:: marionette
One of the most common and yet often most difficult tasks in Marionette is
finding a DOM element on a webpage or in the chrome UI. Marionette provides
several different search strategies to use when finding elements. All search
strategies work with both :func:`~Marionette.find_element` and
:func:`~Marionette.find_elements`, though some strategies are not implemented
in chrome scope.
In the event that more than one element is matched by the query,
:func:`~Marionette.find_element` will only return the first element found. In
the event that no elements are matched by the query,
:func:`~Marionette.find_element` will raise `NoSuchElementException` while
:func:`~Marionette.find_elements` will return an empty list.
Search Strategies
-----------------
Search strategies are defined in the :class:`By` class::
from marionette import By
print(By.ID)
The strategies are:
* `id` - The easiest way to find an element is to refer to its id directly::
container = client.find_element(By.ID, 'container')
* `class name` - To find elements belonging to a certain class, use `class name`::
buttons = client.find_elements(By.CLASS_NAME, 'button')
* `css selector` - It's also possible to find elements using a `css selector`_::
container_buttons = client.find_elements(By.CSS_SELECTOR, '#container .buttons')
* `name` - Find elements by their name attribute (not implemented in chrome
scope)::
form = client.find_element(By.NAME, 'signup')
* `tag name` - To find all the elements with a given tag, use `tag name`::
paragraphs = client.find_elements(By.TAG_NAME, 'p')
* `link text` - A convenience strategy for finding link elements by their
innerHTML (not implemented in chrome scope)::
link = client.find_element(By.LINK_TEXT, 'Click me!')
* `partial link text` - Same as `link text` except substrings of the innerHTML
are matched (not implemented in chrome scope)::
link = client.find_element(By.PARTIAL_LINK_TEXT, 'Clic')
* `xpath` - Find elements using an xpath_ query::
elem = client.find_element(By.XPATH, './/*[@id="foobar"')
.. _css selector: https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors
.. _xpath: https://developer.mozilla.org/en-US/docs/Web/XPath
Chaining Searches
-----------------
In addition to the methods on the Marionette object, HTMLElement objects also
provide :func:`~HTMLElement.find_element` and :func:`~HTMLElement.find_elements`
methods. The difference is that only child nodes of the element will be searched.
Consider the following html snippet::
<div id="content">
<span id="main"></span>
</div>
<div id="footer"></div>
Doing the following will work::
client.find_element(By.ID, 'container').find_element(By.ID, 'main')
But this will raise a `NoSuchElementException`::
client.find_element(By.ID, 'container').find_element(By.ID, 'footer')
Finding Anonymous Nodes
-----------------------
When working in chrome scope, for example manipulating the Firefox user
interface, you may run into something called an anonymous node.
Firefox uses a markup language called XUL_ for its interface. XUL is similar
to HTML in that it has a DOM and tags that render controls on the display. One
ability of XUL is to create re-useable widgets that are made up out of several
smaller XUL elements. These widgets can be bound to the DOM using something
called the `XML binding language (XBL)`_.
The end result is that the DOM sees the widget as a single entity. It doesn't
know anything about how that widget is made up. All of the smaller XUL elements
that make up the widget are called `anonymous content`_. It is not possible to
query such elements using traditional DOM methods like `getElementById`.
Marionette provides two special strategies used for finding anonymous content.
Unlike normal elements, anonymous nodes can only be seen by their parent. So
it's necessary to first find the parent element and then search for the
anonymous children from there.
* `anon` - Finds all anonymous children of the element, there is no search term
so `None` must be passed in::
anon_children = client.find_element('id', 'parent').find_elements('anon', None)
* `anon attribute` - Find an anonymous child based on an attribute. An
unofficial convention is for anonymous nodes to have an
`anonid` attribute::
anon_child = client.find_element('id', 'parent').find_element('anon attribute', {'anonid': 'container'})
.. _XUL: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL
.. _XML binding language (XBL): https://developer.mozilla.org/en-US/docs/XBL
.. _anonymous content: https://developer.mozilla.org/en-US/docs/XBL/XBL_1.0_Reference/Anonymous_Content

View file

@ -0,0 +1,13 @@
Advanced Topics
===============
Here are a collection of articles explaining some of the more complicated
aspects of Marionette.
.. toctree::
:maxdepth: 1
findelement
stale
actions
debug

View file

@ -0,0 +1,71 @@
Dealing with Stale Elements
===========================
.. py:currentmodule:: marionette
Marionette does not keep a live representation of the DOM saved. All it can do
is send commands to the Marionette server which queries the DOM on the client's
behalf. References to elements are also not passed from server to client. A
unique id is generated for each element that gets referenced and a mapping of
id to element object is stored on the server. When commands such as
:func:`~HTMLElement.click()` are run, the client sends the element's id along
with the command. The server looks up the proper DOM element in its reference
table and executes the command on it.
In practice this means that the DOM can change state and Marionette will never
know until it sends another query. For example, look at the following HTML::
<head>
<script type=text/javascript>
function addDiv() {
var div = document.createElement("div");
document.getElementById("container").appendChild(div);
}
</script>
</head>
<body>
<div id="container">
</div>
<input id="button" type=button onclick="addDiv();">
</body>
Care needs to be taken as the DOM is being modified after the page has loaded.
The following code has a race condition::
button = client.find_element('id', 'button')
button.click()
assert len(client.find_elements('css selector', '#container div')) > 0
Explicit Waiting and Expected Conditions
----------------------------------------
To avoid the above scenario, manual synchronisation is needed. Waits are used
to pause program execution until a given condition is true. This is a useful
technique to employ when documents load new content or change after
``Document.readyState``'s value changes to "complete".
The :class:`Wait` helper class provided by Marionette avoids some of the
caveats of ``time.sleep(n)``. It will return immediately once the provided
condition evaluates to true.
To avoid the race condition in the above example, one could do::
button = client.find_element('id', 'button')
button.click()
def find_divs():
return client.find_elements('css selector', '#container div')
divs = Wait(client).until(find_divs)
assert len(divs) > 0
This avoids the race condition. Because finding elements is a common condition
to wait for, it is built in to Marionette. Instead of the above, you could
write::
button = client.find_element('id', 'button')
button.click()
assert len(Wait(client).until(expected.elements_present('css selector', '#container div'))) > 0
For a full list of built-in conditions, see :mod:`~marionette.expected`.

View file

@ -0,0 +1,185 @@
.. py:currentmodule:: marionette_driver.marionette
Marionette Python Client
========================
The Marionette Python client library allows you to remotely control a
Gecko-based browser or device which is running a Marionette_
server. This includes Firefox Desktop and Firefox for Android.
The Marionette server is built directly into Gecko and can be started by
passing in a command line option to Gecko, or by using a Marionette-enabled
build. The server listens for connections from various clients. Clients can
then control Gecko by sending commands to the server.
This is the official Python client for Marionette. There also exists a
`NodeJS client`_ maintained by the Firefox OS automation team.
.. _Marionette: https://developer.mozilla.org/en-US/docs/Marionette
.. _NodeJS client: https://github.com/mozilla-b2g/gaia/tree/master/tests/jsmarionette
Getting the Client
------------------
The Python client is officially supported. To install it, first make sure you
have `pip installed`_ then run:
.. parsed-literal::
pip install marionette_driver
It's highly recommended to use virtualenv_ when installing Marionette to avoid
package conflicts and other general nastiness.
You should now be ready to start using Marionette. The best way to learn is to
play around with it. Start a `Marionette-enabled instance of Firefox`_, fire up
a python shell and follow along with the
:doc:`interactive tutorial <interactive>`!
.. _pip installed: https://pip.pypa.io/en/latest/installing.html
.. _virtualenv: http://virtualenv.readthedocs.org/en/latest/
.. _Marionette-enabled instance of Firefox: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Builds
Using the Client for Testing
----------------------------
Please visit the `Marionette Tests`_ section on MDN for information regarding
testing with Marionette.
.. _Marionette Tests: https://developer.mozilla.org/en/Marionette/Tests
Session Management
------------------
A session is a single instance of a Marionette client connected to a Marionette
server. Before you can start executing commands, you need to start a session
with :func:`start_session() <Marionette.start_session>`:
.. parsed-literal::
from marionette_driver.marionette import Marionette
client = Marionette('localhost', port=2828)
client.start_session()
This returns a session id and an object listing the capabilities of the
Marionette server. For example, a server running on Firefox Desktop will
have some features which a server running from Firefox Android won't.
It's also possible to access the capabilities using the
:attr:`~Marionette.session_capabilities` attribute. After finishing with a
session, you can delete it with :func:`~Marionette.delete_session()`. Note that
this will also happen automatically when the Marionette object is garbage
collected.
Context Management
------------------
Commands can only be executed in a single window, frame and scope at a time. In
order to run commands elsewhere, it's necessary to explicitly switch to the
appropriate context.
Use :func:`~Marionette.switch_to_window` to execute commands in the context of a
new window:
.. parsed-literal::
original_window = client.current_window_handle
for handle in client.window_handles:
if handle != original_window:
client.switch_to_window(handle)
print("Switched to window with '{}' loaded.".format(client.get_url()))
client.switch_to_window(original_window)
Similarly, use :func:`~Marionette.switch_to_frame` to execute commands in the
context of a new frame (e.g an <iframe> element):
.. parsed-literal::
iframe = client.find_element(By.TAG_NAME, 'iframe')
client.switch_to_frame(iframe)
assert iframe == client.get_active_frame()
Finally Marionette can switch between `chrome` and `content` scope. Chrome is a
privileged scope where you can access things like the Firefox UI itself.
Content scope is where things like webpages live. You can switch between
`chrome` and `content` using the :func:`~Marionette.set_context` and :func:`~Marionette.using_context` functions:
.. parsed-literal::
client.set_context(client.CONTEXT_CONTENT)
# content scope
with client.using_context(client.CONTEXT_CHROME):
#chrome scope
... do stuff ...
# content scope restored
Navigation
----------
Use :func:`~Marionette.navigate` to open a new website. It's also possible to
move through the back/forward cache using :func:`~Marionette.go_forward` and
:func:`~Marionette.go_back` respectively. To retrieve the currently
open website, use :func:`~Marionette.get_url`:
.. parsed-literal::
url = 'http://mozilla.org'
client.navigate(url)
client.go_back()
client.go_forward()
assert client.get_url() == url
DOM Elements
------------
In order to inspect or manipulate actual DOM elements, they must first be found
using the :func:`~Marionette.find_element` or :func:`~Marionette.find_elements`
methods:
.. parsed-literal::
from marionette import HTMLElement
element = client.find_element(By.ID, 'my-id')
assert type(element) == HTMLElement
elements = client.find_elements(By.TAG_NAME, 'a')
assert type(elements) == list
For a full list of valid search strategies, see :doc:`advanced/findelement`.
Now that an element has been found, it's possible to manipulate it:
.. parsed-literal::
element.click()
element.send_keys('hello!')
print(element.get_attribute('style'))
For the full list of possible commands, see the :class:`HTMLElement`
reference.
Be warned that a reference to an element object can become stale if it was
modified or removed from the document. See :doc:`advanced/stale` for tips
on working around this limitation.
Script Execution
----------------
Sometimes Marionette's provided APIs just aren't enough and it is necessary to
run arbitrary javascript. This is accomplished with the
:func:`~Marionette.execute_script` and :func:`~Marionette.execute_async_script`
functions. They accomplish what their names suggest, the former executes some
synchronous JavaScript, while the latter provides a callback mechanism for
running asynchronous JavaScript:
.. parsed-literal::
result = client.execute_script("return arguments[0] + arguments[1];",
script_args=[2, 3])
assert result == 5
The async method works the same way, except it won't return until a special
`marionetteScriptFinished()` function is called:
.. parsed-literal::
result = client.execute_async_script("""
setTimeout(function() {
marionetteScriptFinished("all done");
}, arguments[0]);
""", script_args=[1000])
assert result == "all done"
Beware that running asynchronous scripts can potentially hang the program
indefinitely if they are not written properly. It is generally a good idea to
set a script timeout using :func:`~Marionette.timeout.script` and handling
`ScriptTimeoutException`.

View file

@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
#
# Marionette Python Client documentation build configuration file, created by
# sphinx-quickstart on Tue Aug 6 13:54:46 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
here = os.path.dirname(os.path.abspath(__file__))
parent = os.path.dirname(here)
sys.path.insert(0, parent)
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Marionette Python Client'
copyright = u'2013, Mozilla Automation and Tools and individual contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
# version = '0'
# The full version, including alpha/beta/rc tags.
# release = '0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd:
try:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
except ImportError:
pass
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'MarionettePythonClientdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'MarionettePythonClient.tex', u'Marionette Python Client Documentation',
u'Mozilla Automation and Tools team', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'marionettepythonclient', u'Marionette Python Client Documentation',
[u'Mozilla Automation and Tools team'], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'MarionettePythonClient', 'Marionette Python Client Documentation',
'Mozilla Automation and Tools team', 'MarionettePythonClient',
'One line description of project.', 'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'

View file

@ -0,0 +1,16 @@
.. include:: basics.rst
Indices and tables
------------------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. toctree::
:hidden:
Getting Started <basics>
Interactive Tutorial <interactive>
advanced/landing
reference

View file

@ -0,0 +1,55 @@
Using the Client Interactively
==============================
Once you installed the client and have Marionette running, you can fire
up your favourite interactive python environment and start playing with
Marionette. Let's use a typical python shell:
.. parsed-literal::
python
First, import Marionette:
.. parsed-literal::
from marionette import Marionette
Now create the client for this session. Assuming you're using the default
port on a Marionette instance running locally:
.. parsed-literal::
client = Marionette(host='localhost', port=2828)
client.start_session()
This will return some id representing your session id. Now that you've
established a connection, let's start doing interesting things:
.. parsed-literal::
client.execute_script("alert('o hai there!');")
You should now see this alert pop up! How exciting! Okay, let's do
something practical. Close the dialog and try this:
.. parsed-literal::
client.navigate("http://www.mozilla.org")
Now you're at mozilla.org! You can even verify it using the following:
.. parsed-literal::
client.get_url()
You can even find an element and click on it. Let's say you want to get
the first link:
.. parsed-literal::
from marionette import By
first_link = client.find_element(By.TAG_NAME, "a")
first_link now holds a reference to the first link on the page. You can click it:
.. parsed-literal::
first_link.click()

View file

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\MarionettePythonClient.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\MarionettePythonClient.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View file

@ -0,0 +1,51 @@
=============
API Reference
=============
Marionette
----------
.. autoclass:: marionette_driver.marionette.Marionette
:members:
HTMLElement
-----------
.. autoclass:: marionette_driver.marionette.HTMLElement
:members:
DateTimeValue
-------------
.. autoclass:: marionette_driver.DateTimeValue
:members:
Actions
-------
.. autoclass:: marionette_driver.marionette.Actions
:members:
MultiActions
------------
.. autoclass:: marionette_driver.marionette.MultiActions
:members:
Wait
----
.. autoclass:: marionette_driver.Wait
:members:
:special-members:
.. autoattribute marionette_driver.wait.DEFAULT_TIMEOUT
.. autoattribute marionette_driver.wait.DEFAULT_INTERVAL
Built-in Conditions
^^^^^^^^^^^^^^^^^^^
.. automodule:: marionette_driver.expected
:members:
Addons
------
.. autoclass:: marionette_driver.addons.Addons
:members:
Localization
------------
.. autoclass:: marionette_driver.localization.L10n
:members:

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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'))

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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',
}

View file

@ -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()

View file

@ -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'

View file

@ -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")

File diff suppressed because it is too large Load diff

View file

@ -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')

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -0,0 +1,2 @@
mozrunner >= 6.13
mozversion >= 1.1

View file

@ -0,0 +1,49 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import re
from setuptools import setup, find_packages
THIS_DIR = os.path.dirname(os.path.realpath(__name__))
def read(*parts):
with open(os.path.join(THIS_DIR, *parts)) as f:
return f.read()
def get_version():
return re.findall("__version__ = '([\d\.]+)'",
read('marionette_driver', '__init__.py'), re.M)[0]
setup(name='marionette_driver',
version=get_version(),
description="Marionette Driver",
long_description='See http://marionette-client.readthedocs.org/en/latest/',
# Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX',
'Topic :: Software Development :: Quality Assurance',
'Topic :: Software Development :: Testing',
'Topic :: Utilities',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
],
keywords='mozilla',
author='Auto-tools',
author_email='tools-marionette@lists.mozilla.org',
url='https://wiki.mozilla.org/Auto-tools/Projects/Marionette',
license='MPL',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=read('requirements.txt').splitlines(),
)