mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-16 04:07:31 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
3
testing/marionette/client/.flake8
Normal file
3
testing/marionette/client/.flake8
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[flake8]
|
||||
max-line-length = 99
|
||||
exclude = __init__.py,disti/*,build/*,
|
||||
2
testing/marionette/client/MANIFEST.in
Normal file
2
testing/marionette/client/MANIFEST.in
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
exclude MANIFEST.in
|
||||
include requirements.txt
|
||||
153
testing/marionette/client/docs/Makefile
Normal file
153
testing/marionette/client/docs/Makefile
Normal 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."
|
||||
46
testing/marionette/client/docs/advanced/actions.rst
Normal file
46
testing/marionette/client/docs/advanced/actions.rst
Normal 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()
|
||||
54
testing/marionette/client/docs/advanced/debug.rst
Normal file
54
testing/marionette/client/docs/advanced/debug.rst
Normal 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)
|
||||
126
testing/marionette/client/docs/advanced/findelement.rst
Normal file
126
testing/marionette/client/docs/advanced/findelement.rst
Normal 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
|
||||
13
testing/marionette/client/docs/advanced/landing.rst
Normal file
13
testing/marionette/client/docs/advanced/landing.rst
Normal 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
|
||||
71
testing/marionette/client/docs/advanced/stale.rst
Normal file
71
testing/marionette/client/docs/advanced/stale.rst
Normal 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`.
|
||||
185
testing/marionette/client/docs/basics.rst
Normal file
185
testing/marionette/client/docs/basics.rst
Normal 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`.
|
||||
259
testing/marionette/client/docs/conf.py
Normal file
259
testing/marionette/client/docs/conf.py
Normal 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'
|
||||
16
testing/marionette/client/docs/index.rst
Normal file
16
testing/marionette/client/docs/index.rst
Normal 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
|
||||
55
testing/marionette/client/docs/interactive.rst
Normal file
55
testing/marionette/client/docs/interactive.rst
Normal 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()
|
||||
|
||||
190
testing/marionette/client/docs/make.bat
Normal file
190
testing/marionette/client/docs/make.bat
Normal 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
|
||||
51
testing/marionette/client/docs/reference.rst
Normal file
51
testing/marionette/client/docs/reference.rst
Normal 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:
|
||||
26
testing/marionette/client/marionette_driver/__init__.py
Normal file
26
testing/marionette/client/marionette_driver/__init__.py
Normal 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
|
||||
70
testing/marionette/client/marionette_driver/addons.py
Normal file
70
testing/marionette/client/marionette_driver/addons.py
Normal 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)
|
||||
27
testing/marionette/client/marionette_driver/by.py
Normal file
27
testing/marionette/client/marionette_driver/by.py
Normal 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"
|
||||
|
|
@ -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'))
|
||||
69
testing/marionette/client/marionette_driver/decorators.py
Normal file
69
testing/marionette/client/marionette_driver/decorators.py
Normal 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
|
||||
179
testing/marionette/client/marionette_driver/errors.py
Normal file
179
testing/marionette/client/marionette_driver/errors.py
Normal 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)
|
||||
311
testing/marionette/client/marionette_driver/expected.py
Normal file
311
testing/marionette/client/marionette_driver/expected.py
Normal 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
|
||||
467
testing/marionette/client/marionette_driver/geckoinstance.py
Normal file
467
testing/marionette/client/marionette_driver/geckoinstance.py
Normal 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',
|
||||
}
|
||||
93
testing/marionette/client/marionette_driver/gestures.py
Normal file
93
testing/marionette/client/marionette_driver/gestures.py
Normal 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()
|
||||
84
testing/marionette/client/marionette_driver/keys.py
Normal file
84
testing/marionette/client/marionette_driver/keys.py
Normal 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'
|
||||
54
testing/marionette/client/marionette_driver/localization.py
Normal file
54
testing/marionette/client/marionette_driver/localization.py
Normal 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")
|
||||
2153
testing/marionette/client/marionette_driver/marionette.py
Normal file
2153
testing/marionette/client/marionette_driver/marionette.py
Normal file
File diff suppressed because it is too large
Load diff
227
testing/marionette/client/marionette_driver/selection.py
Normal file
227
testing/marionette/client/marionette_driver/selection.py
Normal 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')
|
||||
98
testing/marionette/client/marionette_driver/timeout.py
Normal file
98
testing/marionette/client/marionette_driver/timeout.py
Normal 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
|
||||
300
testing/marionette/client/marionette_driver/transport.py
Normal file
300
testing/marionette/client/marionette_driver/transport.py
Normal 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()
|
||||
167
testing/marionette/client/marionette_driver/wait.py
Normal file
167
testing/marionette/client/marionette_driver/wait.py
Normal 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()
|
||||
2
testing/marionette/client/requirements.txt
Normal file
2
testing/marionette/client/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
mozrunner >= 6.13
|
||||
mozversion >= 1.1
|
||||
49
testing/marionette/client/setup.py
Normal file
49
testing/marionette/client/setup.py
Normal 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(),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue