Bug 1088666 - Re-organize marionette client's documentation into basic, advanced and api reference sections, r=AutomatedTester, DONTBUILD because NPOTB

--HG--
extra : rebase_source : 4dc97864c6944d1b102ab7ab98287b480fe51ad7
This commit is contained in:
Andrew Halberstadt 2014-11-24 09:49:42 -05:00
parent 0e7807d54c
commit e2f5e08b80
10 changed files with 615 additions and 214 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,185 @@
.. py:currentmodule:: marionette
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 desktop Firefox and FirefoxOS (support for
Firefox for Android is planned, but not yet fully implemented).
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/marionette-js-client
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_client
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::
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 a Firefox OS device will
have the ability to rotate the window, while a server running from Firefox
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 or the
system app in Firefox OS. Content scope is where things like webpages or normal
Firefox OS apps 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.set_script_timeout` and handling
`ScriptTimeoutException`.

View File

@ -95,8 +95,20 @@ pygments_style = 'sphinx'
# 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.

View File

@ -1,220 +1,16 @@
.. Marionette Python Client documentation master file, created by
sphinx-quickstart on Tue Aug 6 13:54:46 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
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 desktop Firefox and FirefoxOS (support for
Firefox for Android is planned, but not yet fully implemented).
.. _Marionette: https://developer.mozilla.org/en-US/docs/Marionette
Getting Started
---------------
Getting the Client
^^^^^^^^^^^^^^^^^^
We officially support a python client. The latest supported version of
the client is available on pypi_, so you can download it via pip.
This client should be used within a virtual environment to ensure that
your environment is pristine:
.. parsed-literal::
virtualenv venv
source venv/bin/activate
pip install marionette_client
.. _pypi: https://pypi.python.org/pypi/marionette_client/
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::
first_link = client.find_element("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()
Using the Client for Testing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Please visit, our `Marionette Tests`_ section for information regarding testing.
.. _Marionette Tests: https://developer.mozilla.org/en/Marionette/Tests
.. automodule:: marionette
Marionette Objects
------------------
.. autoclass:: Marionette
Session Management
^^^^^^^^^^^^^^^^^^
.. automethod:: Marionette.start_session
.. automethod:: Marionette.delete_session
.. autoattribute:: Marionette.session_capabilities
.. automethod:: Marionette.get_cookie
.. automethod:: Marionette.get_cookies
.. automethod:: Marionette.add_cookie
.. automethod:: Marionette.delete_all_cookies
Context Management
^^^^^^^^^^^^^^^^^^
.. autoattribute:: Marionette.current_window_handle
.. autoattribute:: Marionette.window_handles
.. automethod:: Marionette.set_context
.. automethod:: Marionette.switch_to_frame
.. automethod:: Marionette.switch_to_window
.. automethod:: Marionette.get_active_frame
.. automethod:: Marionette.close
Navigation Methods
^^^^^^^^^^^^^^^^^^
.. autoattribute:: Marionette.title
.. automethod:: Marionette.navigate
.. automethod:: Marionette.get_url
.. automethod:: Marionette.go_back
.. automethod:: Marionette.go_forward
.. automethod:: Marionette.refresh
.. automethod:: Marionette.absolute_url
.. automethod:: Marionette.get_window_type
DOM Element Methods
^^^^^^^^^^^^^^^^^^^
.. automethod:: Marionette.set_search_timeout
.. automethod:: Marionette.find_element
.. automethod:: Marionette.find_elements
Script Execution
^^^^^^^^^^^^^^^^
.. automethod:: Marionette.execute_script
.. automethod:: Marionette.execute_async_script
.. automethod:: Marionette.set_script_timeout
Debugging
^^^^^^^^^
.. autoattribute:: Marionette.page_source
.. automethod:: Marionette.log
.. automethod:: Marionette.get_logs
.. automethod:: Marionette.screenshot
Querying and Modifying Document Content
---------------------------------------
.. autoclass:: HTMLElement
:members:
.. autoclass:: DateTimeValue
:members:
Action Objects
--------------
Action Sequences
^^^^^^^^^^^^^^^^
.. autoclass:: Actions
:members:
Multi-action Sequences
^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: MultiActions
:members:
Explicit Waiting and Expected Conditions
----------------------------------------
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".
Because Marionette returns control to the user
when the document is completely loaded,
any subsequent interaction with elements
are subject to manual synchronisation.
The reason for this is that Marionette
does not keep a direct representation of the DOM,
but instead exposes a way for the user to
query the browser's DOM state.
The `Wait` helper class provided by Marionette
avoids some of the caveats of ``time.sleep(n)``,
which sets the condition to an exact time period to wait.
It will return immediately
once the provided condition evaluates to true.
In addition to writing your own custom conditions
you can combine `Wait`
with a number of ready-made expected conditions
that are listed below.
Waits
^^^^^
.. autoclass:: marionette.wait.Wait
:members:
:special-members:
.. autoattribute marionette.wait.DEFAULT_TIMEOUT
.. autoattribute marionette.wait.DEFAULT_INTERVAL
Expected Conditions
^^^^^^^^^^^^^^^^^^^
.. automodule:: marionette.expected
:members:
.. include:: basics.rst
Indices and tables
==================
------------------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. toctree::
:hidden:
Getting Started <basics>
Interactive Tutorial <interactive>
advanced/landing
reference

View File

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

View File

@ -0,0 +1,43 @@
=============
API Reference
=============
.. py:currentmodule:: marionette
Marionette
----------
.. autoclass:: Marionette
:members:
HTMLElement
-----------
.. autoclass:: HTMLElement
:members:
DateTimeValue
-------------
.. autoclass:: DateTimeValue
:members:
Actions
-------
.. autoclass:: Actions
:members:
MultiActions
------------
.. autoclass:: MultiActions
:members:
Wait
----
.. autoclass:: Wait
:members:
:special-members:
.. autoattribute marionette.wait.DEFAULT_TIMEOUT
.. autoattribute marionette.wait.DEFAULT_INTERVAL
Built-in Conditions
^^^^^^^^^^^^^^^^^^^
.. automodule:: marionette.expected
:members: