From e2f5e08b800c0dfd3edbc83b376fa4d2f9ca95b6 Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Mon, 24 Nov 2014 09:49:42 -0500 Subject: [PATCH] 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 --- .../client/docs/advanced/actions.rst | 46 ++++ .../marionette/client/docs/advanced/debug.rst | 54 +++++ .../client/docs/advanced/findelement.rst | 126 ++++++++++ .../client/docs/advanced/landing.rst | 13 + .../marionette/client/docs/advanced/stale.rst | 71 ++++++ testing/marionette/client/docs/basics.rst | 185 +++++++++++++++ testing/marionette/client/docs/conf.py | 12 + testing/marionette/client/docs/index.rst | 224 +----------------- .../marionette/client/docs/interactive.rst | 55 +++++ testing/marionette/client/docs/reference.rst | 43 ++++ 10 files changed, 615 insertions(+), 214 deletions(-) create mode 100644 testing/marionette/client/docs/advanced/actions.rst create mode 100644 testing/marionette/client/docs/advanced/debug.rst create mode 100644 testing/marionette/client/docs/advanced/findelement.rst create mode 100644 testing/marionette/client/docs/advanced/landing.rst create mode 100644 testing/marionette/client/docs/advanced/stale.rst create mode 100644 testing/marionette/client/docs/basics.rst create mode 100644 testing/marionette/client/docs/interactive.rst create mode 100644 testing/marionette/client/docs/reference.rst diff --git a/testing/marionette/client/docs/advanced/actions.rst b/testing/marionette/client/docs/advanced/actions.rst new file mode 100644 index 000000000000..294855a6f7ef --- /dev/null +++ b/testing/marionette/client/docs/advanced/actions.rst @@ -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() diff --git a/testing/marionette/client/docs/advanced/debug.rst b/testing/marionette/client/docs/advanced/debug.rst new file mode 100644 index 000000000000..e72d2549bd02 --- /dev/null +++ b/testing/marionette/client/docs/advanced/debug.rst @@ -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) diff --git a/testing/marionette/client/docs/advanced/findelement.rst b/testing/marionette/client/docs/advanced/findelement.rst new file mode 100644 index 000000000000..abcbc8e89a81 --- /dev/null +++ b/testing/marionette/client/docs/advanced/findelement.rst @@ -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:: + +
+ +
+ + +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 diff --git a/testing/marionette/client/docs/advanced/landing.rst b/testing/marionette/client/docs/advanced/landing.rst new file mode 100644 index 000000000000..0a44de63d7f4 --- /dev/null +++ b/testing/marionette/client/docs/advanced/landing.rst @@ -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 diff --git a/testing/marionette/client/docs/advanced/stale.rst b/testing/marionette/client/docs/advanced/stale.rst new file mode 100644 index 000000000000..0af5768653d6 --- /dev/null +++ b/testing/marionette/client/docs/advanced/stale.rst @@ -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:: + + + + + + +
+
+ + + +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`. diff --git a/testing/marionette/client/docs/basics.rst b/testing/marionette/client/docs/basics.rst new file mode 100644 index 000000000000..0f117b1f1e55 --- /dev/null +++ b/testing/marionette/client/docs/basics.rst @@ -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 `! + +.. _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() `: + +.. 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