tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

marionette.py (83082B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import base64
      6 import datetime
      7 import json
      8 import os
      9 import socket
     10 import sys
     11 import time
     12 import traceback
     13 from contextlib import contextmanager
     14 
     15 from . import errors, transport
     16 from .decorators import do_process_check
     17 from .geckoinstance import GeckoInstance
     18 from .keys import Keys
     19 from .timeout import Timeouts
     20 
     21 WEB_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
     22 WEB_FRAME_KEY = "frame-075b-4da1-b6ba-e579c2d3230a"
     23 WEB_SHADOW_ROOT_KEY = "shadow-6066-11e4-a52e-4f735466cecf"
     24 WEB_WINDOW_KEY = "window-fcc6-11e5-b4f8-330a88ab9d7f"
     25 
     26 
     27 class MouseButton:
     28    """Enum-like class for mouse button constants."""
     29 
     30    LEFT = 0
     31    MIDDLE = 1
     32    RIGHT = 2
     33 
     34 
     35 class ActionSequence:
     36    r"""API for creating and performing action sequences.
     37 
     38    Each action method adds one or more actions to a queue. When perform()
     39    is called, the queued actions fire in order.
     40 
     41    May be chained together as in::
     42 
     43         ActionSequence(self.marionette, "key", id) \
     44            .key_down("a") \
     45            .key_up("a") \
     46            .perform()
     47    """
     48 
     49    def __init__(self, marionette, action_type, input_id, pointer_params=None):
     50        self.marionette = marionette
     51        self._actions = []
     52        self._id = input_id
     53        self._pointer_params = pointer_params
     54        self._type = action_type
     55 
     56    @property
     57    def dict(self):
     58        d = {
     59            "type": self._type,
     60            "id": self._id,
     61            "actions": self._actions,
     62        }
     63        if self._pointer_params is not None:
     64            d["parameters"] = self._pointer_params
     65        return d
     66 
     67    def perform(self):
     68        """Perform all queued actions."""
     69        self.marionette.actions.perform([self.dict])
     70 
     71    def _key_action(self, subtype, value):
     72        self._actions.append({"type": subtype, "value": value})
     73 
     74    def _pointer_action(self, subtype, button):
     75        self._actions.append({"type": subtype, "button": button})
     76 
     77    def pause(self, duration):
     78        self._actions.append({"type": "pause", "duration": duration})
     79        return self
     80 
     81    def pointer_move(self, x, y, duration=None, origin=None):
     82        """Queue a pointerMove action.
     83 
     84        :param x: Destination x-axis coordinate of pointer in CSS pixels.
     85        :param y: Destination y-axis coordinate of pointer in CSS pixels.
     86        :param duration: Number of milliseconds over which to distribute the
     87                         move. If None, remote end defaults to 0.
     88        :param origin: Origin of coordinates, either "viewport", "pointer" or
     89                       an Element. If None, remote end defaults to "viewport".
     90        """
     91        action = {"type": "pointerMove", "x": x, "y": y}
     92        if duration is not None:
     93            action["duration"] = duration
     94        if origin is not None:
     95            if isinstance(origin, WebElement):
     96                action["origin"] = {origin.kind: origin.id}
     97            else:
     98                action["origin"] = origin
     99        self._actions.append(action)
    100        return self
    101 
    102    def pointer_up(self, button=MouseButton.LEFT):
    103        """Queue a pointerUp action for `button`.
    104 
    105        :param button: Pointer button to perform action with.
    106                       Default: 0, which represents main device button.
    107        """
    108        self._pointer_action("pointerUp", button)
    109        return self
    110 
    111    def pointer_down(self, button=MouseButton.LEFT):
    112        """Queue a pointerDown action for `button`.
    113 
    114        :param button: Pointer button to perform action with.
    115                       Default: 0, which represents main device button.
    116        """
    117        self._pointer_action("pointerDown", button)
    118        return self
    119 
    120    def click(self, element=None, button=MouseButton.LEFT):
    121        """Queue a click with the specified button.
    122 
    123        If an element is given, move the pointer to that element first,
    124        otherwise click current pointer coordinates.
    125 
    126        :param element: Optional element to click.
    127        :param button: Integer representing pointer button to perform action
    128                       with. Default: 0, which represents main device button.
    129        """
    130        if element:
    131            self.pointer_move(0, 0, origin=element)
    132        return self.pointer_down(button).pointer_up(button)
    133 
    134    def key_down(self, value):
    135        """Queue a keyDown action for `value`.
    136 
    137        :param value: Single character to perform key action with.
    138        """
    139        self._key_action("keyDown", value)
    140        return self
    141 
    142    def key_up(self, value):
    143        """Queue a keyUp action for `value`.
    144 
    145        :param value: Single character to perform key action with.
    146        """
    147        self._key_action("keyUp", value)
    148        return self
    149 
    150    def scroll(self, x, y, delta_x, delta_y, duration=None, origin=None):
    151        """Queue a scroll action.
    152 
    153        :param x: Destination x-axis coordinate of pointer in CSS pixels.
    154        :param y: Destination y-axis coordinate of pointer in CSS pixels.
    155        :param delta_x: Scroll delta for x-axis in CSS pixels.
    156        :param delta_y: Scroll delta for y-axis in CSS pixels.
    157        :param duration: Number of milliseconds over which to distribute the
    158                         scroll. If None, remote end defaults to 0.
    159        :param origin: Origin of coordinates, either "viewport", "pointer" or
    160                       an Element. If None, remote end defaults to "viewport".
    161        """
    162        action = {
    163            "type": "scroll",
    164            "x": x,
    165            "y": y,
    166            "deltaX": delta_x,
    167            "deltaY": delta_y,
    168        }
    169 
    170        if duration is not None:
    171            action["duration"] = duration
    172        if origin is not None:
    173            if isinstance(origin, WebElement):
    174                action["origin"] = {origin.kind: origin.id}
    175            else:
    176                action["origin"] = origin
    177        self._actions.append(action)
    178        return self
    179 
    180    def send_keys(self, keys):
    181        """Queue a keyDown and keyUp action for each character in `keys`.
    182 
    183        :param keys: String of keys to perform key actions with.
    184        """
    185        for c in keys:
    186            self.key_down(c)
    187            self.key_up(c)
    188        return self
    189 
    190 
    191 class Actions:
    192    def __init__(self, marionette):
    193        self.marionette = marionette
    194 
    195    def perform(self, actions=None):
    196        """Perform actions by tick from each action sequence in `actions`.
    197 
    198        :param actions: List of input source action sequences. A single action
    199                        sequence may be created with the help of
    200                        ``ActionSequence.dict``.
    201        """
    202        body = {"actions": [] if actions is None else actions}
    203        return self.marionette._send_message("WebDriver:PerformActions", body)
    204 
    205    def release(self):
    206        return self.marionette._send_message("WebDriver:ReleaseActions")
    207 
    208    def sequence(self, *args, **kwargs):
    209        """Return an empty ActionSequence of the designated type.
    210 
    211        See ActionSequence for parameter list.
    212        """
    213        return ActionSequence(self.marionette, *args, **kwargs)
    214 
    215 
    216 class WebElement:
    217    """Represents a DOM Element."""
    218 
    219    identifiers = (WEB_ELEMENT_KEY,)
    220 
    221    def __init__(self, marionette, id, kind=WEB_ELEMENT_KEY):
    222        self.marionette = marionette
    223        assert id is not None
    224        self.id = id
    225        self.kind = kind
    226 
    227    def __str__(self):
    228        return self.id
    229 
    230    def __eq__(self, other_element):
    231        return self.id == other_element.id
    232 
    233    def __hash__(self):
    234        # pylint --py3k: W1641
    235        return hash(self.id)
    236 
    237    def find_element(self, method, target):
    238        """Returns an ``WebElement`` instance that matches the specified
    239        method and target, relative to the current element.
    240 
    241        For more details on this function, see the
    242        :func:`~marionette_driver.marionette.Marionette.find_element` method
    243        in the Marionette class.
    244        """
    245        return self.marionette.find_element(method, target, self.id)
    246 
    247    def find_elements(self, method, target):
    248        """Returns a list of all ``WebElement`` instances that match the
    249        specified method and target in the current context.
    250 
    251        For more details on this function, see the
    252        :func:`~marionette_driver.marionette.Marionette.find_elements` method
    253        in the Marionette class.
    254        """
    255        return self.marionette.find_elements(method, target, self.id)
    256 
    257    def get_attribute(self, name):
    258        """Returns the requested attribute, or None if no attribute
    259        is set.
    260        """
    261        body = {"id": self.id, "name": name}
    262        return self.marionette._send_message(
    263            "WebDriver:GetElementAttribute", body, key="value"
    264        )
    265 
    266    def get_property(self, name):
    267        """Returns the requested property, or None if the property is
    268        not set.
    269        """
    270        try:
    271            body = {"id": self.id, "name": name}
    272            return self.marionette._send_message(
    273                "WebDriver:GetElementProperty", body, key="value"
    274            )
    275        except errors.UnknownCommandException:
    276            # Keep backward compatibility for code which uses get_attribute() to
    277            # also retrieve element properties.
    278            # Remove when Firefox 55 is stable.
    279            return self.get_attribute(name)
    280 
    281    def click(self):
    282        """Simulates a click on the element."""
    283        self.marionette._send_message("WebDriver:ElementClick", {"id": self.id})
    284 
    285    @property
    286    def text(self):
    287        """Returns the visible text of the element, and its child elements."""
    288        body = {"id": self.id}
    289        return self.marionette._send_message(
    290            "WebDriver:GetElementText", body, key="value"
    291        )
    292 
    293    def send_keys(self, *strings):
    294        """Sends the string via synthesized keypresses to the element.
    295        If an array is passed in like `marionette.send_keys(Keys.SHIFT, "a")` it
    296        will be joined into a string.
    297        If an integer is passed in like `marionette.send_keys(1234)` it will be
    298        coerced into a string.
    299        """
    300        keys = Marionette.convert_keys(*strings)
    301        self.marionette._send_message(
    302            "WebDriver:ElementSendKeys", {"id": self.id, "text": keys}
    303        )
    304 
    305    def clear(self):
    306        """Clears the input of the element."""
    307        self.marionette._send_message("WebDriver:ElementClear", {"id": self.id})
    308 
    309    def is_selected(self):
    310        """Returns True if the element is selected."""
    311        body = {"id": self.id}
    312        return self.marionette._send_message(
    313            "WebDriver:IsElementSelected", body, key="value"
    314        )
    315 
    316    def is_enabled(self):
    317        """This command will return False if all the following criteria
    318        are met otherwise return True:
    319 
    320        * A form control is disabled.
    321        * A ``WebElement`` has a disabled boolean attribute.
    322        """
    323        body = {"id": self.id}
    324        return self.marionette._send_message(
    325            "WebDriver:IsElementEnabled", body, key="value"
    326        )
    327 
    328    def is_displayed(self):
    329        """Returns True if the element is displayed, False otherwise."""
    330        body = {"id": self.id}
    331        return self.marionette._send_message(
    332            "WebDriver:IsElementDisplayed", body, key="value"
    333        )
    334 
    335    @property
    336    def tag_name(self):
    337        """The tag name of the element."""
    338        body = {"id": self.id}
    339        return self.marionette._send_message(
    340            "WebDriver:GetElementTagName", body, key="value"
    341        )
    342 
    343    @property
    344    def rect(self):
    345        """Gets the element's bounding rectangle.
    346 
    347        This will return a dictionary with the following:
    348 
    349          * x and y represent the top left coordinates of the ``WebElement``
    350            relative to top left corner of the document.
    351          * height and the width will contain the height and the width
    352            of the DOMRect of the ``WebElement``.
    353        """
    354        return self.marionette._send_message(
    355            "WebDriver:GetElementRect", {"id": self.id}
    356        )
    357 
    358    def value_of_css_property(self, property_name):
    359        """Gets the value of the specified CSS property name.
    360 
    361        :param property_name: Property name to get the value of.
    362        """
    363        body = {"id": self.id, "propertyName": property_name}
    364        return self.marionette._send_message(
    365            "WebDriver:GetElementCSSValue", body, key="value"
    366        )
    367 
    368    @property
    369    def shadow_root(self):
    370        """Gets the shadow root of the current element"""
    371        return self.marionette._send_message(
    372            "WebDriver:GetShadowRoot", {"id": self.id}, key="value"
    373        )
    374 
    375    @property
    376    def computed_label(self):
    377        """Gets the computed accessibility label of the current element"""
    378        return self.marionette._send_message(
    379            "WebDriver:GetComputedLabel", {"id": self.id}, key="value"
    380        )
    381 
    382    @property
    383    def computed_role(self):
    384        """Gets the computed accessibility role of the current element"""
    385        return self.marionette._send_message(
    386            "WebDriver:GetComputedRole", {"id": self.id}, key="value"
    387        )
    388 
    389    @classmethod
    390    def _from_json(cls, json, marionette):
    391        if isinstance(json, dict):
    392            if WEB_ELEMENT_KEY in json:
    393                return cls(marionette, json[WEB_ELEMENT_KEY], WEB_ELEMENT_KEY)
    394        raise ValueError("Unrecognised web element")
    395 
    396 
    397 class ShadowRoot:
    398    """A Class to handling Shadow Roots"""
    399 
    400    identifiers = (WEB_SHADOW_ROOT_KEY,)
    401 
    402    def __init__(self, marionette, id, kind=WEB_SHADOW_ROOT_KEY):
    403        self.marionette = marionette
    404        assert id is not None
    405        self.id = id
    406        self.kind = kind
    407 
    408    def __str__(self):
    409        return self.id
    410 
    411    def __eq__(self, other_element):
    412        return self.id == other_element.id
    413 
    414    def __hash__(self):
    415        # pylint --py3k: W1641
    416        return hash(self.id)
    417 
    418    def find_element(self, method, target):
    419        """Returns a ``WebElement`` instance that matches the specified
    420        method and target, relative to the current shadow root.
    421 
    422        For more details on this function, see the
    423        :func:`~marionette_driver.marionette.Marionette.find_element` method
    424        in the Marionette class.
    425        """
    426        body = {"shadowRoot": self.id, "value": target, "using": method}
    427        return self.marionette._send_message(
    428            "WebDriver:FindElementFromShadowRoot", body, key="value"
    429        )
    430 
    431    def find_elements(self, method, target):
    432        """Returns a list of all ``WebElement`` instances that match the
    433         specified method and target in the current shadow root.
    434 
    435        For more details on this function, see the
    436        :func:`~marionette_driver.marionette.Marionette.find_elements` method
    437        in the Marionette class.
    438        """
    439        body = {"shadowRoot": self.id, "value": target, "using": method}
    440        return self.marionette._send_message(
    441            "WebDriver:FindElementsFromShadowRoot", body
    442        )
    443 
    444    @classmethod
    445    def _from_json(cls, json, marionette):
    446        if isinstance(json, dict):
    447            if WEB_SHADOW_ROOT_KEY in json:
    448                return cls(marionette, json[WEB_SHADOW_ROOT_KEY])
    449        raise ValueError("Unrecognised shadow root")
    450 
    451 
    452 class WebFrame:
    453    """A Class to handle frame windows"""
    454 
    455    identifiers = (WEB_FRAME_KEY,)
    456 
    457    def __init__(self, marionette, id, kind=WEB_FRAME_KEY):
    458        self.marionette = marionette
    459        assert id is not None
    460        self.id = id
    461        self.kind = kind
    462 
    463    def __str__(self):
    464        return self.id
    465 
    466    def __eq__(self, other_element):
    467        return self.id == other_element.id
    468 
    469    def __hash__(self):
    470        # pylint --py3k: W1641
    471        return hash(self.id)
    472 
    473    @classmethod
    474    def _from_json(cls, json, marionette):
    475        if isinstance(json, dict):
    476            if WEB_FRAME_KEY in json:
    477                return cls(marionette, json[WEB_FRAME_KEY])
    478        raise ValueError("Unrecognised web frame")
    479 
    480 
    481 class WebWindow:
    482    """A Class to handle top-level windows"""
    483 
    484    identifiers = (WEB_WINDOW_KEY,)
    485 
    486    def __init__(self, marionette, id, kind=WEB_WINDOW_KEY):
    487        self.marionette = marionette
    488        assert id is not None
    489        self.id = id
    490        self.kind = kind
    491 
    492    def __str__(self):
    493        return self.id
    494 
    495    def __eq__(self, other_element):
    496        return self.id == other_element.id
    497 
    498    def __hash__(self):
    499        # pylint --py3k: W1641
    500        return hash(self.id)
    501 
    502    @classmethod
    503    def _from_json(cls, json, marionette):
    504        if isinstance(json, dict):
    505            if WEB_WINDOW_KEY in json:
    506                return cls(marionette, json[WEB_WINDOW_KEY])
    507        raise ValueError("Unrecognised web window")
    508 
    509 
    510 class Alert:
    511    """A class for interacting with alerts.
    512 
    513    ::
    514 
    515        Alert(marionette).accept()
    516        Alert(marionette).dismiss()
    517    """
    518 
    519    def __init__(self, marionette):
    520        self.marionette = marionette
    521 
    522    def accept(self):
    523        """Accept a currently displayed modal dialog."""
    524        self.marionette._send_message("WebDriver:AcceptAlert")
    525 
    526    def dismiss(self):
    527        """Dismiss a currently displayed modal dialog."""
    528        self.marionette._send_message("WebDriver:DismissAlert")
    529 
    530    @property
    531    def text(self):
    532        """Return the currently displayed text in a tab modal."""
    533        return self.marionette._send_message("WebDriver:GetAlertText", key="value")
    534 
    535    def send_keys(self, *string):
    536        """Send keys to the currently displayed text input area in an open
    537        tab modal dialog."""
    538        self.marionette._send_message(
    539            "WebDriver:SendAlertText", {"text": Marionette.convert_keys(*string)}
    540        )
    541 
    542 
    543 class Marionette:
    544    """Represents a Marionette connection to a browser or device."""
    545 
    546    CONTEXT_CHROME = "chrome"  # non-browser content: windows, dialogs, etc.
    547    CONTEXT_CONTENT = "content"  # browser content: iframes, divs, etc.
    548    DEFAULT_STARTUP_TIMEOUT = 120
    549    DEFAULT_SHUTDOWN_TIMEOUT = (
    550        70  # By default Firefox will kill hanging threads after 60s
    551    )
    552 
    553    # Bug 1336953 - Until we can remove the socket timeout parameter it has to be
    554    # set a default value which is larger than the longest timeout as defined by the
    555    # WebDriver spec. In that case its 300s for page load. Also add another minute
    556    # so that slow builds have enough time to send the timeout error to the client.
    557    DEFAULT_SOCKET_TIMEOUT = 360
    558 
    559    def __init__(
    560        self,
    561        host="127.0.0.1",
    562        port=2828,
    563        app=None,
    564        bin=None,
    565        baseurl=None,
    566        socket_timeout=None,
    567        startup_timeout=None,
    568        **instance_args,
    569    ):
    570        """Construct a holder for the Marionette connection.
    571 
    572        Remember to call ``start_session`` in order to initiate the
    573        connection and start a Marionette session.
    574 
    575        :param host: Host where the Marionette server listens.
    576            Defaults to 127.0.0.1.
    577        :param port: Port where the Marionette server listens.
    578            Defaults to port 2828.
    579        :param baseurl: Where to look for files served from Marionette's
    580            www directory.
    581        :param socket_timeout: Timeout for Marionette socket operations.
    582        :param startup_timeout: Seconds to wait for a connection with
    583            binary.
    584        :param bin: Path to browser binary.  If any truthy value is given
    585            this will attempt to start a Gecko instance with the specified
    586            `app`.
    587        :param app: Type of ``instance_class`` to use for managing app
    588            instance. See ``marionette_driver.geckoinstance``.
    589        :param instance_args: Arguments to pass to ``instance_class``.
    590        """
    591        self.host = "127.0.0.1"  # host
    592        if int(port) == 0:
    593            port = Marionette.check_port_available(port)
    594        self.port = self.local_port = int(port)
    595        self.bin = bin
    596        self.client = None
    597        self.instance = None
    598        self.requested_capabilities = None
    599        self.session = None
    600        self.session_id = None
    601        self.process_id = None
    602        self.profile = None
    603        self.window = None
    604        self.chrome_window = None
    605        self.baseurl = baseurl
    606        self._test_name = None
    607        self.crashed = 0
    608        self.is_shutting_down = False
    609        self.cleanup_ran = False
    610 
    611        if socket_timeout is None:
    612            self.socket_timeout = self.DEFAULT_SOCKET_TIMEOUT
    613        else:
    614            self.socket_timeout = float(socket_timeout)
    615 
    616        if startup_timeout is None:
    617            self.startup_timeout = self.DEFAULT_STARTUP_TIMEOUT
    618        else:
    619            self.startup_timeout = int(startup_timeout)
    620 
    621        self.shutdown_timeout = self.DEFAULT_SHUTDOWN_TIMEOUT
    622 
    623        if self.bin:
    624            self.instance = GeckoInstance.create(
    625                app, host=self.host, port=self.port, bin=self.bin, **instance_args
    626            )
    627            self.start_binary(self.startup_timeout)
    628 
    629        self.actions = Actions(self)
    630        self.timeout = Timeouts(self)
    631 
    632    @property
    633    def profile_path(self):
    634        if self.instance and self.instance.profile:
    635            return self.instance.profile.profile
    636 
    637    def start_binary(self, timeout):
    638        try:
    639            self.check_port_available(self.port, host=self.host)
    640        except OSError:
    641            _, value, tb = sys.exc_info()
    642            msg = f"Port {self.host}:{self.port} is unavailable ({value})"
    643            raise OSError(msg).with_traceback(tb)
    644 
    645        try:
    646            self.instance.start()
    647            self.raise_for_port(timeout=timeout)
    648        except socket.timeout:
    649            # Something went wrong with starting up Marionette server. Given
    650            # that the process will not quit itself, force a shutdown immediately.
    651            self.cleanup()
    652 
    653            msg = (
    654                "Process killed after {}s because no connection to Marionette "
    655                "server could be established. Check gecko.log for errors"
    656            )
    657            raise OSError(msg.format(timeout)).with_traceback(sys.exc_info()[2])
    658 
    659    def cleanup(self):
    660        if self.session is not None:
    661            try:
    662                self.delete_session()
    663            except (OSError, errors.MarionetteException):
    664                # These exceptions get thrown if the Marionette server
    665                # hit an exception/died or the connection died. We can
    666                # do no further server-side cleanup in this case.
    667                pass
    668 
    669        if self.instance:
    670            # stop application and, if applicable, stop emulator
    671            self.instance.close(clean=True)
    672            if self.instance.unresponsive_count >= 3:
    673                raise errors.UnresponsiveInstanceException(
    674                    "Application clean-up has failed >2 consecutive times."
    675                )
    676 
    677        self.cleanup_ran = True
    678 
    679    def __del__(self):
    680        if not self.cleanup_ran:
    681            self.cleanup()
    682 
    683    @staticmethod
    684    def check_port_available(port, host=""):
    685        """Check if "host:port" is available.
    686 
    687        Raise socket.error if port is not available.
    688        """
    689        port = int(port)
    690        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    691        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    692        try:
    693            s.bind((host, port))
    694            port = s.getsockname()[1]
    695        finally:
    696            s.close()
    697            return port
    698 
    699    def raise_for_port(self, timeout=None, check_process_status=True):
    700        """Raise socket.timeout if no connection can be established.
    701 
    702        :param timeout: Optional timeout in seconds for the server to be ready.
    703        :param check_process_status: Optional, if `True` the process will be
    704            continuously checked if it has exited, and the connection
    705            attempt will be aborted.
    706        """
    707        if timeout is None:
    708            timeout = self.startup_timeout
    709 
    710        runner = None
    711        if self.instance is not None:
    712            runner = self.instance.runner
    713 
    714        poll_interval = 0.1
    715        starttime = datetime.datetime.now()
    716        timeout_time = starttime + datetime.timedelta(seconds=timeout)
    717 
    718        client = transport.TcpTransport(self.host, self.port, 0.5)
    719 
    720        connected = False
    721        while datetime.datetime.now() < timeout_time:
    722            # If the instance we want to connect to is not running return immediately
    723            if check_process_status and runner is not None and not runner.is_running():
    724                break
    725 
    726            try:
    727                client.connect()
    728                return True
    729            except OSError:
    730                pass
    731            finally:
    732                client.close()
    733 
    734            time.sleep(poll_interval)
    735 
    736        if not connected:
    737            # There might have been a startup crash of the application
    738            if runner is not None and self.check_for_crash() > 0:
    739                raise OSError(f"Process crashed (Exit code: {runner.wait(0)})")
    740 
    741            raise socket.timeout(
    742                f"Timed out waiting for connection on {self.host}:{self.port}!"
    743            )
    744 
    745    @do_process_check
    746    def _send_message(self, name, params=None, key=None):
    747        """Send a blocking message to the server.
    748 
    749        Marionette provides an asynchronous, non-blocking interface and
    750        this attempts to paper over this by providing a synchronous API
    751        to the user.
    752 
    753        :param name: Requested command key.
    754        :param params: Optional dictionary of key/value arguments.
    755        :param key: Optional key to extract from response.
    756 
    757        :returns: Full response from the server, or if `key` is given,
    758            the value of said key in the response.
    759        """
    760        if not self.session_id and name != "WebDriver:NewSession":
    761            raise errors.InvalidSessionIdException("Please start a session")
    762 
    763        try:
    764            msg = self.client.request(name, params)
    765        except OSError:
    766            self.delete_session(send_request=False)
    767            raise
    768 
    769        res, err = msg.result, msg.error
    770        if err:
    771            self._handle_error(err)
    772 
    773        if key is not None:
    774            return self._from_json(res.get(key))
    775        else:
    776            return self._from_json(res)
    777 
    778    def _handle_error(self, obj):
    779        error = obj["error"]
    780        message = obj["message"]
    781        stacktrace = obj["stacktrace"]
    782 
    783        raise errors.lookup(error)(message, stacktrace=stacktrace)
    784 
    785    def check_for_crash(self):
    786        """Check if the process crashed.
    787 
    788        :returns: True, if a crash happened since the method has been called the last time.
    789        """
    790        crash_count = 0
    791 
    792        if self.instance:
    793            name = self.test_name or "marionette.py"
    794            crash_count = self.instance.runner.check_for_crashes(test_name=name)
    795            self.crashed = self.crashed + crash_count
    796 
    797        return crash_count > 0
    798 
    799    def _handle_socket_failure(self):
    800        """Handle socket failures for the currently connected application.
    801 
    802        If the application crashed then clean-up internal states, or in case of a content
    803        crash also kill the process. If there are other reasons for a socket failure,
    804        wait for the process to shutdown itself, or force kill it.
    805 
    806        Please note that the method expects an exception to be handled on the current stack
    807        frame, and is only called via the `@do_process_check` decorator.
    808 
    809        """
    810        exc_cls, exc, tb = sys.exc_info()
    811 
    812        # If the application hasn't been launched by Marionette no further action can be done.
    813        # In such cases we simply re-throw the exception.
    814        if not self.instance:
    815            raise exc.with_traceback(tb)
    816 
    817        else:
    818            # Somehow the socket disconnected. Give the application some time to shutdown
    819            # itself before killing the process.
    820            returncode = self.instance.runner.wait(timeout=self.shutdown_timeout)
    821 
    822            if returncode is None:
    823                message = (
    824                    "Process killed because the connection to Marionette server is "
    825                    "lost. Check gecko.log for errors"
    826                )
    827                # This will force-close the application without sending any other message.
    828                self.cleanup()
    829            else:
    830                # If Firefox quit itself check if there was a crash
    831                crash_count = self.check_for_crash()
    832 
    833                if crash_count > 0:
    834                    # SIGUSR1 indicates a forced shutdown due to a content process crash
    835                    if returncode == 245:
    836                        message = "Content process crashed"
    837                    else:
    838                        message = "Process crashed (Exit code: {returncode})"
    839                else:
    840                    message = (
    841                        "Process has been unexpectedly closed (Exit code: {returncode})"
    842                    )
    843 
    844                self.delete_session(send_request=False)
    845 
    846            message += " (Reason: {reason})"
    847 
    848            raise OSError(
    849                message.format(returncode=returncode, reason=exc)
    850            ).with_traceback(tb)
    851 
    852    @staticmethod
    853    def convert_keys(*string):
    854        typing = []
    855        for val in string:
    856            if isinstance(val, Keys):
    857                typing.append(val)
    858            elif isinstance(val, int):
    859                val = str(val)
    860                for i in range(len(val)):
    861                    typing.append(val[i])
    862            else:
    863                for i in range(len(val)):
    864                    typing.append(val[i])
    865        return "".join(typing)
    866 
    867    def clear_pref(self, pref):
    868        """Clear the user-defined value from the specified preference.
    869 
    870        :param pref: Name of the preference.
    871        """
    872        with self.using_context(self.CONTEXT_CHROME):
    873            self.execute_script(
    874                """
    875               const { Preferences } = ChromeUtils.importESModule(
    876                 "resource://gre/modules/Preferences.sys.mjs"
    877               );
    878               Preferences.reset(arguments[0]);
    879               """,
    880                script_args=(pref,),
    881            )
    882 
    883    def get_pref(self, pref, default_branch=False, value_type="unspecified"):
    884        """Get the value of the specified preference.
    885 
    886        :param pref: Name of the preference.
    887        :param default_branch: Optional, if `True` the preference value will be read
    888                               from the default branch. Otherwise the user-defined
    889                               value if set is returned. Defaults to `False`.
    890        :param value_type: Optional, XPCOM interface of the pref's complex value.
    891                           Possible values are: `nsIFile` and
    892                           `nsIPrefLocalizedString`.
    893 
    894        Usage example::
    895 
    896            marionette.get_pref("browser.tabs.warnOnClose")
    897 
    898        """
    899        with self.using_context(self.CONTEXT_CHROME):
    900            pref_value = self.execute_script(
    901                """
    902                const { Preferences } = ChromeUtils.importESModule(
    903                  "resource://gre/modules/Preferences.sys.mjs"
    904                );
    905 
    906                let pref = arguments[0];
    907                let defaultBranch = arguments[1];
    908                let valueType = arguments[2];
    909 
    910                prefs = new Preferences({defaultBranch: defaultBranch});
    911                return prefs.get(pref, null, Components.interfaces[valueType]);
    912                """,
    913                script_args=(pref, default_branch, value_type),
    914            )
    915            return pref_value
    916 
    917    def set_pref(self, pref, value, default_branch=False):
    918        """Set the value of the specified preference.
    919 
    920        :param pref: Name of the preference.
    921        :param value: The value to set the preference to. If the value is None,
    922                      reset the preference to its default value. If no default
    923                      value exists, the preference will cease to exist.
    924        :param default_branch: Optional, if `True` the preference value will
    925                       be written to the default branch, and will remain until
    926                       the application gets restarted. Otherwise a user-defined
    927                       value is set. Defaults to `False`.
    928 
    929        Usage example::
    930 
    931            marionette.set_pref("browser.tabs.warnOnClose", True)
    932 
    933        """
    934        with self.using_context(self.CONTEXT_CHROME):
    935            if value is None:
    936                self.clear_pref(pref)
    937                return
    938 
    939            self.execute_script(
    940                """
    941                const { Preferences } = ChromeUtils.importESModule(
    942                  "resource://gre/modules/Preferences.sys.mjs"
    943                );
    944 
    945                let pref = arguments[0];
    946                let value = arguments[1];
    947                let defaultBranch = arguments[2];
    948 
    949                prefs = new Preferences({defaultBranch: defaultBranch});
    950                prefs.set(pref, value);
    951                """,
    952                script_args=(pref, value, default_branch),
    953            )
    954 
    955    def set_prefs(self, prefs, default_branch=False):
    956        """Set the value of a list of preferences.
    957 
    958        :param prefs: A dict containing one or more preferences and their values
    959                      to be set. See :func:`set_pref` for further details.
    960        :param default_branch: Optional, if `True` the preference value will
    961                       be written to the default branch, and will remain until
    962                       the application gets restarted. Otherwise a user-defined
    963                       value is set. Defaults to `False`.
    964 
    965        Usage example::
    966 
    967            marionette.set_prefs({"browser.tabs.warnOnClose": True})
    968 
    969        """
    970        for pref, value in prefs.items():
    971            self.set_pref(pref, value, default_branch=default_branch)
    972 
    973    @contextmanager
    974    def using_prefs(self, prefs, default_branch=False):
    975        """Set preferences for code executed in a `with` block, and restores them on exit.
    976 
    977        :param prefs: A dict containing one or more preferences and their values
    978                      to be set. See :func:`set_prefs` for further details.
    979        :param default_branch: Optional, if `True` the preference value will
    980                       be written to the default branch, and will remain until
    981                       the application gets restarted. Otherwise a user-defined
    982                       value is set. Defaults to `False`.
    983 
    984        Usage example::
    985 
    986            with marionette.using_prefs({"browser.tabs.warnOnClose": True}):
    987                # ... do stuff ...
    988 
    989        """
    990        original_prefs = {p: self.get_pref(p) for p in prefs}
    991        self.set_prefs(prefs, default_branch=default_branch)
    992 
    993        try:
    994            yield
    995        finally:
    996            self.set_prefs(original_prefs, default_branch=default_branch)
    997 
    998    @do_process_check
    999    def enforce_gecko_prefs(self, prefs):
   1000        """Checks if the running instance has the given prefs. If not,
   1001        it will kill the currently running instance, and spawn a new
   1002        instance with the requested preferences.
   1003 
   1004        :param prefs: A dictionary whose keys are preference names.
   1005        """
   1006        if not self.instance:
   1007            raise errors.MarionetteException(
   1008                "enforce_gecko_prefs() can only be called "
   1009                "on Gecko instances launched by Marionette"
   1010            )
   1011        pref_exists = True
   1012        with self.using_context(self.CONTEXT_CHROME):
   1013            for pref, value in prefs.items():
   1014                if type(value) is not str:
   1015                    value = json.dumps(value)
   1016                pref_exists = self.execute_script(
   1017                    f"""
   1018                let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
   1019                                              .getService(Components.interfaces.nsIPrefBranch);
   1020                let pref = '{pref}';
   1021                let value = '{value}';
   1022                let type = prefInterface.getPrefType(pref);
   1023                switch(type) {{
   1024                    case prefInterface.PREF_STRING:
   1025                        return value == prefInterface.getCharPref(pref).toString();
   1026                    case prefInterface.PREF_BOOL:
   1027                        return value == prefInterface.getBoolPref(pref).toString();
   1028                    case prefInterface.PREF_INT:
   1029                        return value == prefInterface.getIntPref(pref).toString();
   1030                    case prefInterface.PREF_INVALID:
   1031                        return false;
   1032                }}
   1033                """
   1034                )
   1035                if not pref_exists:
   1036                    break
   1037 
   1038        if not pref_exists:
   1039            context = self._send_message("Marionette:GetContext", key="value")
   1040            self.delete_session()
   1041            self.instance.restart(prefs)
   1042            self.raise_for_port()
   1043            self.start_session(self.requested_capabilities)
   1044 
   1045            # Restore the context as used before the restart
   1046            self.set_context(context)
   1047 
   1048    def _request_in_app_shutdown(self, flags=None, safe_mode=False):
   1049        """Attempt to quit the currently running instance from inside the
   1050        application. If shutdown is prevented by some component the quit
   1051        will be forced.
   1052 
   1053        This method effectively calls `Services.startup.quit` in Gecko.
   1054        Possible flag values are listed at https://bit.ly/3IYcjYi.
   1055 
   1056        :param flags: Optional additional quit masks to include.
   1057 
   1058        :param safe_mode: Optional flag to indicate that the application has to
   1059            be restarted in safe mode.
   1060 
   1061        :returns: A dictionary containing details of the application shutdown.
   1062                  The `cause` property reflects the reason, and `forced` indicates
   1063                  that something prevented the shutdown and the application had
   1064                  to be forced to shutdown.
   1065 
   1066        :throws InvalidArgumentException: If there are multiple
   1067            `shutdown_flags` ending with `"Quit"`.
   1068        """
   1069        body = {}
   1070        if flags is not None:
   1071            body["flags"] = list(
   1072                flags,
   1073            )
   1074        if safe_mode:
   1075            body["safeMode"] = safe_mode
   1076 
   1077        return self._send_message("Marionette:Quit", body)
   1078 
   1079    @do_process_check
   1080    def quit(self, clean=False, in_app=True, callback=None):
   1081        """
   1082        By default this method will trigger a normal shutdown of the currently running instance.
   1083        But it can also be used to force terminate the process.
   1084 
   1085        This command will delete the active marionette session. It also allows
   1086        manipulation of eg. the profile data while the application is not running.
   1087        To start the application again, :func:`start_session` has to be called.
   1088 
   1089        :param clean: If True a new profile will be used after the next start of
   1090                      the application. Note that the in_app initiated quit always
   1091                      maintains the same profile.
   1092 
   1093        :param in_app: If True, marionette will cause a quit from within the
   1094                       application. Otherwise the application will be restarted
   1095                       immediately by killing the process.
   1096 
   1097        :param callback: If provided and `in_app` is True, the callback will
   1098                         be used to trigger the shutdown.
   1099 
   1100        :returns: A dictionary containing details of the application shutdown.
   1101                  The `cause` property reflects the reason, and `forced` indicates
   1102                  that something prevented the shutdown and the application had
   1103                  to be forced to shutdown.
   1104        """
   1105        if not self.instance:
   1106            raise errors.MarionetteException(
   1107                "quit() can only be called on Gecko instances launched by Marionette"
   1108            )
   1109 
   1110        quit_details = {"cause": "shutdown", "forced": False}
   1111 
   1112        if in_app:
   1113            if clean:
   1114                raise ValueError(
   1115                    "An in_app restart cannot be triggered with the clean flag set"
   1116                )
   1117 
   1118            if callback is not None and not callable(callback):
   1119                raise ValueError(f"Specified callback '{callback}' is not callable")
   1120 
   1121            # Block Marionette from accepting new connections
   1122            self._send_message("Marionette:AcceptConnections", {"value": False})
   1123 
   1124            try:
   1125                self.is_shutting_down = True
   1126                if callback is not None:
   1127                    callback()
   1128                    quit_details["in_app"] = True
   1129                else:
   1130                    quit_details = self._request_in_app_shutdown()
   1131 
   1132            except OSError:
   1133                # A possible IOError should be ignored at this point, given that
   1134                # quit() could have been called inside of `using_context`,
   1135                # which wants to reset the context but fails sending the message.
   1136                pass
   1137 
   1138            except Exception:
   1139                # For any other error assume the application is not going to shutdown.
   1140                # As such allow Marionette to accept new connections again.
   1141                self.is_shutting_down = False
   1142                self._send_message("Marionette:AcceptConnections", {"value": True})
   1143                raise
   1144 
   1145            try:
   1146                self.delete_session(send_request=False)
   1147 
   1148                # Try to wait for the process to end itself before force-closing it.
   1149                returncode = self.instance.runner.wait(timeout=self.shutdown_timeout)
   1150                if returncode is None:
   1151                    self.cleanup()
   1152 
   1153                    message = "Process still running {}s after quit request"
   1154                    raise OSError(message.format(self.shutdown_timeout))
   1155 
   1156            finally:
   1157                self.is_shutting_down = False
   1158 
   1159        else:
   1160            self.delete_session(send_request=False)
   1161            self.instance.close(clean=clean)
   1162 
   1163            quit_details.update({"in_app": False, "forced": True})
   1164 
   1165        if quit_details.get("cause") not in (None, "shutdown"):
   1166            raise errors.MarionetteException(
   1167                "Unexpected shutdown reason '{}' for quitting the process.".format(
   1168                    quit_details["cause"]
   1169                )
   1170            )
   1171 
   1172        return quit_details
   1173 
   1174    @do_process_check
   1175    def restart(
   1176        self, callback=None, clean=False, in_app=True, safe_mode=False, silent=False
   1177    ):
   1178        """
   1179        By default this method will restart the currently running instance by using the same
   1180        profile. But it can also be forced to terminate the currently running instance, and
   1181        to spawn a new instance with the same or different profile.
   1182 
   1183        :param callback: If provided and `in_app` is True, the callback will be
   1184                         used to trigger the restart.
   1185 
   1186        :param clean: If True a new profile will be used after the restart. Note
   1187                      that the in_app initiated restart always maintains the same
   1188                      profile.
   1189 
   1190        :param in_app: If True, marionette will cause a restart from within the
   1191                       application. Otherwise the application will be restarted
   1192                       immediately by killing the process.
   1193 
   1194        :param safe_mode: Optional flag to indicate that the application has to
   1195            be restarted in safe mode.
   1196 
   1197        :param silent: Optional flag to indicate that the application should
   1198            not open any window after a restart. Note that this flag is only
   1199            supported on MacOS and requires "in_app" to be True.
   1200 
   1201        :returns: A dictionary containing details of the application restart.
   1202                  The `cause` property reflects the reason, and `forced` indicates
   1203                  that something prevented the shutdown and the application had
   1204                  to be forced to shutdown.
   1205        """
   1206        if not self.instance:
   1207            raise errors.MarionetteException(
   1208                "restart() can only be called on Gecko instances launched by Marionette"
   1209            )
   1210 
   1211        context = self._send_message("Marionette:GetContext", key="value")
   1212        restart_details = {"cause": "restart", "forced": False}
   1213 
   1214        # Safe mode and the silent flag require an in_app restart.
   1215        if (safe_mode or silent) and not in_app:
   1216            raise ValueError("An in_app restart is required for safe or silent mode")
   1217 
   1218        if in_app:
   1219            if clean:
   1220                raise ValueError(
   1221                    "An in_app restart cannot be triggered with the clean flag set"
   1222                )
   1223 
   1224            if callback is not None and not callable(callback):
   1225                raise ValueError(f"Specified callback '{callback}' is not callable")
   1226 
   1227            # Block Marionette from accepting new connections
   1228            self._send_message("Marionette:AcceptConnections", {"value": False})
   1229 
   1230            try:
   1231                self.is_shutting_down = True
   1232                if callback is not None:
   1233                    callback()
   1234                    restart_details["in_app"] = True
   1235                else:
   1236                    flags = ["eRestart"]
   1237                    if silent:
   1238                        flags.append("eSilently")
   1239 
   1240                    try:
   1241                        restart_details = self._request_in_app_shutdown(
   1242                            flags=flags, safe_mode=safe_mode
   1243                        )
   1244                    except Exception as e:
   1245                        self._send_message(
   1246                            "Marionette:AcceptConnections", {"value": True}
   1247                        )
   1248                        raise e
   1249 
   1250            except OSError:
   1251                # A possible IOError should be ignored at this point, given that
   1252                # restart() could have been called inside of `using_context`,
   1253                # which wants to reset the context but fails sending the message.
   1254                pass
   1255 
   1256            timeout_restart = self.shutdown_timeout + self.startup_timeout
   1257            try:
   1258                # Wait for a new Marionette connection to appear while the
   1259                # process restarts itself.
   1260                self.raise_for_port(timeout=timeout_restart, check_process_status=False)
   1261            except socket.timeout:
   1262                exc_cls, _, tb = sys.exc_info()
   1263 
   1264                if self.instance.runner.returncode is None:
   1265                    self.is_shutting_down = False
   1266 
   1267                    # The process is still running, which means the shutdown
   1268                    # request was not correct or the application ignored it.
   1269                    # Allow Marionette to accept connections again.
   1270                    self._send_message("Marionette:AcceptConnections", {"value": True})
   1271 
   1272                    message = "Process still running {}s after restart request"
   1273                    raise exc_cls(message.format(timeout_restart)).with_traceback(tb)
   1274 
   1275                else:
   1276                    # The process shutdown but didn't start again.
   1277                    self.cleanup()
   1278                    msg = "Process unexpectedly quit without restarting (exit code: {})"
   1279                    raise exc_cls(
   1280                        msg.format(self.instance.runner.returncode)
   1281                    ).with_traceback(tb)
   1282 
   1283            self.is_shutting_down = False
   1284 
   1285            # Create a new session to retrieve the new process id of the application
   1286            self.delete_session(send_request=False)
   1287 
   1288        else:
   1289            self.delete_session()
   1290            self.instance.restart(clean=clean)
   1291 
   1292            self.raise_for_port(timeout=self.DEFAULT_STARTUP_TIMEOUT)
   1293 
   1294            restart_details.update({"in_app": False, "forced": True})
   1295 
   1296        self.start_session(self.requested_capabilities, process_forked=in_app)
   1297        # Restore the context as used before the restart
   1298        self.set_context(context)
   1299 
   1300        if restart_details.get("cause") not in (None, "restart"):
   1301            raise errors.MarionetteException(
   1302                "Unexpected shutdown reason '{}' for restarting the process".format(
   1303                    restart_details["cause"]
   1304                )
   1305            )
   1306 
   1307        return restart_details
   1308 
   1309    def absolute_url(self, relative_url):
   1310        """
   1311        Returns an absolute url for files served from Marionette's www directory.
   1312 
   1313        :param relative_url: The url of a static file, relative to Marionette's www directory.
   1314        """
   1315        return f"{self.baseurl}{relative_url}"
   1316 
   1317    @do_process_check
   1318    def start_session(self, capabilities=None, process_forked=False, timeout=None):
   1319        """Create a new WebDriver session.
   1320        This method must be called before performing any other action.
   1321 
   1322        :param capabilities: An optional dictionary of
   1323            Marionette-recognised capabilities.  It does not
   1324            accept a WebDriver conforming capabilities dictionary
   1325            (including alwaysMatch, firstMatch, desiredCapabilities,
   1326            or requriedCapabilities), and only recognises extension
   1327            capabilities that are specific to Marionette.
   1328        :param process_forked: If True, the existing process forked itself due
   1329        to an internal restart.
   1330        :param timeout: Optional timeout in seconds for the server to be ready.
   1331 
   1332        :returns: A dictionary of the capabilities offered.
   1333        """
   1334        if capabilities is None:
   1335            capabilities = {"strictFileInteractability": True}
   1336        self.requested_capabilities = capabilities
   1337 
   1338        if timeout is None:
   1339            timeout = self.startup_timeout
   1340 
   1341        self.crashed = 0
   1342 
   1343        if not process_forked:
   1344            # Only handle the binary if there was no process before which also
   1345            # might have forked itself due to a restart
   1346            if self.instance:
   1347                returncode = self.instance.runner.returncode
   1348                # We're managing a binary which has terminated. Start it again
   1349                # and implicitely wait for the Marionette server to be ready.
   1350                if returncode is not None:
   1351                    self.start_binary(timeout)
   1352 
   1353            else:
   1354                # In the case when Marionette doesn't manage the binary wait until
   1355                # its server component has been started.
   1356                self.raise_for_port(timeout=timeout)
   1357 
   1358        self.client = transport.TcpTransport(self.host, self.port, self.socket_timeout)
   1359        self.protocol, _ = self.client.connect()
   1360 
   1361        try:
   1362            resp = self._send_message("WebDriver:NewSession", capabilities)
   1363        except errors.UnknownException:
   1364            # Force closing the managed process when the session cannot be
   1365            # created due to global JavaScript errors.
   1366            exc_type, value, tb = sys.exc_info()
   1367            if self.instance and self.instance.runner.is_running():
   1368                self.instance.close()
   1369            raise exc_type(value.message).with_traceback(tb)
   1370 
   1371        self.session_id = resp["sessionId"]
   1372        self.session = resp["capabilities"]
   1373        self.cleanup_ran = False
   1374 
   1375        self.process_id = self.session.get("moz:processID")
   1376        if process_forked:
   1377            self.instance.update_process(self.process_id, self.shutdown_timeout)
   1378 
   1379        self.profile = self.session.get("moz:profile")
   1380 
   1381        timeout = self.session.get("moz:shutdownTimeout")
   1382        if timeout is not None:
   1383            # pylint --py3k W1619
   1384            self.shutdown_timeout = timeout / 1000 + 10
   1385 
   1386        return self.session
   1387 
   1388    @property
   1389    def test_name(self):
   1390        return self._test_name
   1391 
   1392    @test_name.setter
   1393    def test_name(self, test_name):
   1394        self._test_name = test_name
   1395 
   1396    def delete_session(self, send_request=True):
   1397        """Close the current session and disconnect from the server.
   1398 
   1399        :param send_request: Optional, if `True` a request to close the session on
   1400            the server side will be sent. Use `False` in case of eg. in_app restart()
   1401            or quit(), which trigger a deletion themselves. Defaults to `True`.
   1402        """
   1403        try:
   1404            if send_request:
   1405                try:
   1406                    self._send_message("WebDriver:DeleteSession")
   1407                except errors.InvalidSessionIdException:
   1408                    pass
   1409        finally:
   1410            self.process_id = None
   1411            self.profile = None
   1412            self.session = None
   1413            self.session_id = None
   1414            self.window = None
   1415 
   1416            if self.client is not None:
   1417                self.client.close()
   1418 
   1419    @property
   1420    def session_capabilities(self):
   1421        """A JSON dictionary representing the capabilities of the
   1422        current session.
   1423 
   1424        """
   1425        return self.session
   1426 
   1427    @property
   1428    def current_window_handle(self):
   1429        """Get the current window's handle.
   1430 
   1431        Returns an opaque server-assigned identifier to this window
   1432        that uniquely identifies it within this Marionette instance.
   1433        This can be used to switch to this window at a later point.
   1434 
   1435        :returns: unique window handle
   1436        :rtype: string
   1437        """
   1438        with self.using_context("content"):
   1439            self.window = self._send_message("WebDriver:GetWindowHandle", key="value")
   1440 
   1441        return self.window
   1442 
   1443    @property
   1444    def current_chrome_window_handle(self):
   1445        """Get the current chrome window's handle. Corresponds to
   1446        a chrome window that may itself contain tabs identified by
   1447        window_handles.
   1448 
   1449        Returns an opaque server-assigned identifier to this window
   1450        that uniquely identifies it within this Marionette instance.
   1451        This can be used to switch to this window at a later point.
   1452 
   1453        :returns: unique window handle
   1454        :rtype: string
   1455        """
   1456        with self.using_context("chrome"):
   1457            self.chrome_window = self._send_message(
   1458                "WebDriver:GetWindowHandle", key="value"
   1459            )
   1460 
   1461        return self.chrome_window
   1462 
   1463    def set_window_rect(self, x=None, y=None, height=None, width=None):
   1464        """Set the position and size of the current window.
   1465 
   1466        The supplied width and height values refer to the window outerWidth
   1467        and outerHeight values, which include scroll bars, title bars, etc.
   1468 
   1469        An error will be returned if the requested window size would result
   1470        in the window being in the maximised state.
   1471 
   1472        :param x: x coordinate for the top left of the window
   1473        :param y: y coordinate for the top left of the window
   1474        :param width: The width to resize the window to.
   1475        :param height: The height to resize the window to.
   1476        """
   1477        if (x is None and y is None) and (height is None and width is None):
   1478            raise errors.InvalidArgumentException(
   1479                "x and y or height and width need values"
   1480            )
   1481 
   1482        body = {"x": x, "y": y, "height": height, "width": width}
   1483        return self._send_message("WebDriver:SetWindowRect", body)
   1484 
   1485    @property
   1486    def window_rect(self):
   1487        return self._send_message("WebDriver:GetWindowRect")
   1488 
   1489    @property
   1490    def title(self):
   1491        """Current title of the active window."""
   1492        return self._send_message("WebDriver:GetTitle", key="value")
   1493 
   1494    @property
   1495    def window_handles(self):
   1496        """Get list of windows in the current context.
   1497 
   1498        If called in the content context it will return a list of
   1499        references to all available browser windows.
   1500 
   1501        Each window handle is assigned by the server, and the list of
   1502        strings returned does not have a guaranteed ordering.
   1503 
   1504        :returns: Unordered list of unique window handles as strings
   1505        """
   1506        with self.using_context("content"):
   1507            return self._send_message("WebDriver:GetWindowHandles")
   1508 
   1509    @property
   1510    def chrome_window_handles(self):
   1511        """Get a list of currently open chrome windows.
   1512 
   1513        Each window handle is assigned by the server, and the list of
   1514        strings returned does not have a guaranteed ordering.
   1515 
   1516        :returns: Unordered list of unique chrome window handles as strings
   1517        """
   1518        with self.using_context("chrome"):
   1519            return self._send_message("WebDriver:GetWindowHandles")
   1520 
   1521    @property
   1522    def page_source(self):
   1523        """A string representation of the DOM."""
   1524        return self._send_message("WebDriver:GetPageSource", key="value")
   1525 
   1526    def open(self, type=None, focus=False, private=False):
   1527        """Open a new window, or tab based on the specified context type.
   1528 
   1529        If no context type is given the application will choose the best
   1530        option based on tab and window support.
   1531 
   1532        :param type: Type of window to be opened. Can be one of "tab" or "window"
   1533        :param focus: If true, the opened window will be focused
   1534        :param private: If true, open a private window
   1535 
   1536        :returns: Dict with new window handle, and type of opened window
   1537        """
   1538        body = {"type": type, "focus": focus, "private": private}
   1539        return self._send_message("WebDriver:NewWindow", body)
   1540 
   1541    def close(self):
   1542        """Close the current window, ending the session if it's the last
   1543        window currently open.
   1544 
   1545        :returns: Unordered list of remaining unique window handles as strings
   1546        """
   1547        return self._send_message("WebDriver:CloseWindow")
   1548 
   1549    def close_chrome_window(self):
   1550        """Close the currently selected chrome window, ending the session
   1551        if it's the last window open.
   1552 
   1553        :returns: Unordered list of remaining unique chrome window handles as strings
   1554        """
   1555        return self._send_message("WebDriver:CloseChromeWindow")
   1556 
   1557    def register_chrome_handler(self, manifestPath, entries):
   1558        """Register a chrome protocol handler.
   1559 
   1560        :param manifestPath: Path to the chrome manifest file
   1561        :param entries:  Chrome entries to register.
   1562 
   1563        `entries` is an array of arrays, each containing a registry entry
   1564        (type, namespace, path, options) as it would appar in a chrome.manifest
   1565        file. Only the following entry types are currently accepted:
   1566 
   1567            - "content" A URL entry. Must be a 3-element array.
   1568            - "override" A URL override entry. Must be a 3-element array.
   1569            - "locale" A locale package entry. Must be a 4-element array.
   1570 
   1571        :returns: id of the registered chrome handler
   1572        """
   1573        return self._send_message(
   1574            "Marionette:RegisterChromeHandler",
   1575            {
   1576                "manifestPath": manifestPath,
   1577                "entries": entries,
   1578            },
   1579            key="value",
   1580        )
   1581 
   1582    def unregister_chrome_handler(self, id):
   1583        """Unregister a previous registered chrome protocol handler."""
   1584        self._send_message("Marionette:UnregisterChromeHandler", {"id": id})
   1585 
   1586    def set_context(self, context):
   1587        """Sets the context that Marionette commands are running in.
   1588 
   1589        :param context: Context, may be one of the class properties
   1590            `CONTEXT_CHROME` or `CONTEXT_CONTENT`.
   1591 
   1592        Usage example::
   1593 
   1594            marionette.set_context(marionette.CONTEXT_CHROME)
   1595        """
   1596        if context not in [self.CONTEXT_CHROME, self.CONTEXT_CONTENT]:
   1597            raise ValueError(f"Unknown context: {context}")
   1598 
   1599        self._send_message("Marionette:SetContext", {"value": context})
   1600 
   1601    @contextmanager
   1602    def using_context(self, context):
   1603        """Sets the context that Marionette commands are running in using
   1604        a `with` statement. The state of the context on the server is
   1605        saved before entering the block, and restored upon exiting it.
   1606 
   1607        :param context: Context, may be one of the class properties
   1608            `CONTEXT_CHROME` or `CONTEXT_CONTENT`.
   1609 
   1610        Usage example::
   1611 
   1612            with marionette.using_context(marionette.CONTEXT_CHROME):
   1613                # chrome scope
   1614                ... do stuff ...
   1615        """
   1616        scope = self._send_message("Marionette:GetContext", key="value")
   1617        self.set_context(context)
   1618        try:
   1619            yield
   1620        finally:
   1621            self.set_context(scope)
   1622 
   1623    def switch_to_alert(self):
   1624        """Returns an :class:`~marionette_driver.marionette.Alert` object for
   1625        interacting with a currently displayed alert.
   1626 
   1627        ::
   1628 
   1629            alert = self.marionette.switch_to_alert()
   1630            text = alert.text
   1631            alert.accept()
   1632        """
   1633        return Alert(self)
   1634 
   1635    def switch_to_window(self, handle, focus=True):
   1636        """Switch to the specified window; subsequent commands will be
   1637        directed at the new window.
   1638 
   1639        :param handle: The id of the window to switch to.
   1640 
   1641        :param focus: A boolean value which determins whether to focus
   1642            the window that we just switched to.
   1643        """
   1644        self._send_message(
   1645            "WebDriver:SwitchToWindow", {"handle": handle, "focus": focus}
   1646        )
   1647        self.window = handle
   1648 
   1649    def switch_to_default_content(self):
   1650        """Switch the current context to page's default content."""
   1651        return self.switch_to_frame()
   1652 
   1653    def switch_to_parent_frame(self):
   1654        """
   1655        Switch to the Parent Frame
   1656        """
   1657        self._send_message("WebDriver:SwitchToParentFrame")
   1658 
   1659    def switch_to_frame(self, frame=None):
   1660        """Switch the current context to the specified frame. Subsequent
   1661        commands will operate in the context of the specified frame,
   1662        if applicable.
   1663 
   1664        :param frame: A reference to the frame to switch to.  This can
   1665            be an :class:`~marionette_driver.marionette.WebElement`,
   1666            or an integer index. If you call ``switch_to_frame`` without an
   1667            argument, it will switch to the top-level frame.
   1668        """
   1669        body = {}
   1670        if isinstance(frame, WebElement):
   1671            body["element"] = frame.id
   1672        elif frame is not None:
   1673            body["id"] = frame
   1674 
   1675        self._send_message("WebDriver:SwitchToFrame", body)
   1676 
   1677    def get_url(self):
   1678        """Get a string representing the current URL.
   1679 
   1680        On Desktop this returns a string representation of the URL of
   1681        the current top level browsing context.  This is equivalent to
   1682        document.location.href.
   1683 
   1684        When in the context of the chrome, this returns the canonical
   1685        URL of the current resource.
   1686 
   1687        :returns: string representation of URL
   1688        """
   1689        return self._send_message("WebDriver:GetCurrentURL", key="value")
   1690 
   1691    def get_window_type(self):
   1692        """Gets the windowtype attribute of the window Marionette is
   1693        currently acting on.
   1694 
   1695        This command only makes sense in a chrome context. You might use this
   1696        method to distinguish a browser window from an editor window.
   1697        """
   1698        try:
   1699            return self._send_message("Marionette:GetWindowType", key="value")
   1700        except errors.UnknownCommandException:
   1701            return self._send_message("getWindowType", key="value")
   1702 
   1703    def navigate(self, url):
   1704        """Navigate to given `url`.
   1705 
   1706        Navigates the current top-level browsing context's content
   1707        frame to the given URL and waits for the document to load or
   1708        the session's page timeout duration to elapse before returning.
   1709 
   1710        The command will return with a failure if there is an error
   1711        loading the document or the URL is blocked.  This can occur if
   1712        it fails to reach the host, the URL is malformed, the page is
   1713        restricted (about:* pages), or if there is a certificate issue
   1714        to name some examples.
   1715 
   1716        The document is considered successfully loaded when the
   1717        `DOMContentLoaded` event on the frame element associated with the
   1718        `window` triggers and `document.readyState` is "complete".
   1719 
   1720        In chrome context it will change the current `window`'s location
   1721        to the supplied URL and wait until `document.readyState` equals
   1722        "complete" or the page timeout duration has elapsed.
   1723 
   1724        :param url: The URL to navigate to.
   1725        """
   1726        self._send_message("WebDriver:Navigate", {"url": url})
   1727 
   1728    def go_back(self):
   1729        """Causes the browser to perform a back navigation."""
   1730        self._send_message("WebDriver:Back")
   1731 
   1732    def go_forward(self):
   1733        """Causes the browser to perform a forward navigation."""
   1734        self._send_message("WebDriver:Forward")
   1735 
   1736    def refresh(self):
   1737        """Causes the browser to perform to refresh the current page."""
   1738        self._send_message("WebDriver:Refresh")
   1739 
   1740    def _to_json(self, args):
   1741        if isinstance(args, (list, tuple)):
   1742            wrapped = []
   1743            for arg in args:
   1744                wrapped.append(self._to_json(arg))
   1745        elif isinstance(args, dict):
   1746            wrapped = {}
   1747            for arg in args:
   1748                wrapped[arg] = self._to_json(args[arg])
   1749        elif type(args) is WebElement:
   1750            wrapped = {WEB_ELEMENT_KEY: args.id}
   1751        elif type(args) is ShadowRoot:
   1752            wrapped = {WEB_SHADOW_ROOT_KEY: args.id}
   1753        elif type(args) is WebFrame:
   1754            wrapped = {WEB_FRAME_KEY: args.id}
   1755        elif type(args) is WebWindow:
   1756            wrapped = {WEB_WINDOW_KEY: args.id}
   1757        elif isinstance(args, (bool, int, float, str)) or args is None:
   1758            wrapped = args
   1759        return wrapped
   1760 
   1761    def _from_json(self, value):
   1762        if isinstance(value, dict) and any(
   1763            k in value.keys() for k in WebElement.identifiers
   1764        ):
   1765            return WebElement._from_json(value, self)
   1766        elif isinstance(value, dict) and any(
   1767            k in value.keys() for k in ShadowRoot.identifiers
   1768        ):
   1769            return ShadowRoot._from_json(value, self)
   1770        elif isinstance(value, dict) and any(
   1771            k in value.keys() for k in WebFrame.identifiers
   1772        ):
   1773            return WebFrame._from_json(value, self)
   1774        elif isinstance(value, dict) and any(
   1775            k in value.keys() for k in WebWindow.identifiers
   1776        ):
   1777            return WebWindow._from_json(value, self)
   1778        elif isinstance(value, dict):
   1779            return {key: self._from_json(val) for key, val in value.items()}
   1780        elif isinstance(value, list):
   1781            return list(self._from_json(item) for item in value)
   1782        else:
   1783            return value
   1784 
   1785    def execute_script(
   1786        self,
   1787        script,
   1788        script_args=(),
   1789        new_sandbox=True,
   1790        sandbox="default",
   1791        script_timeout=None,
   1792    ):
   1793        """Executes a synchronous JavaScript script, and returns the
   1794        result (or None if the script does return a value).
   1795 
   1796        The script is executed in the context set by the most recent
   1797        :func:`set_context` call, or to the CONTEXT_CONTENT context if
   1798        :func:`set_context` has not been called.
   1799 
   1800        :param script: A string containing the JavaScript to execute.
   1801        :param script_args: An interable of arguments to pass to the script.
   1802        :param new_sandbox: If False, preserve global variables from
   1803            the last execute_*script call. This is True by default, in which
   1804            case no globals are preserved.
   1805        :param sandbox: A tag referring to the sandbox you wish to use;
   1806            if you specify a new tag, a new sandbox will be created.
   1807            If you use the special tag `system`, the sandbox will
   1808            be created using the system principal which has elevated
   1809            privileges.
   1810        :param script_timeout: Timeout in milliseconds, overriding
   1811            the session's default script timeout.
   1812 
   1813        Simple usage example:
   1814 
   1815        ::
   1816 
   1817            result = marionette.execute_script("return 1;")
   1818            assert result == 1
   1819 
   1820        You can use the `script_args` parameter to pass arguments to the
   1821        script:
   1822 
   1823        ::
   1824 
   1825            result = marionette.execute_script("return arguments[0] + arguments[1];",
   1826                                               script_args=(2, 3,))
   1827            assert result == 5
   1828            some_element = marionette.find_element(By.ID, "someElement")
   1829            sid = marionette.execute_script("return arguments[0].id;", script_args=(some_element,))
   1830            assert some_element.get_attribute("id") == sid
   1831 
   1832        Scripts wishing to access non-standard properties of the window
   1833        object must use window.wrappedJSObject:
   1834 
   1835        ::
   1836 
   1837            result = marionette.execute_script('''
   1838              window.wrappedJSObject.test1 = "foo";
   1839              window.wrappedJSObject.test2 = "bar";
   1840              return window.wrappedJSObject.test1 + window.wrappedJSObject.test2;
   1841              ''')
   1842            assert result == "foobar"
   1843 
   1844        Global variables set by individual scripts do not persist between
   1845        script calls by default.  If you wish to persist data between
   1846        script calls, you can set `new_sandbox` to False on your next call,
   1847        and add any new variables to a new 'global' object like this:
   1848 
   1849        ::
   1850 
   1851            marionette.execute_script("global.test1 = 'foo';")
   1852            result = self.marionette.execute_script("return global.test1;", new_sandbox=False)
   1853            assert result == "foo"
   1854 
   1855        """
   1856        original_timeout = None
   1857        if script_timeout is not None:
   1858            original_timeout = self.timeout.script
   1859            self.timeout.script = script_timeout / 1000.0
   1860 
   1861        try:
   1862            args = self._to_json(script_args)
   1863            stack = traceback.extract_stack()
   1864            frame = stack[-2:-1][0]  # grab the second-to-last frame
   1865            filename = (
   1866                frame[0] if sys.platform == "win32" else os.path.relpath(frame[0])
   1867            )
   1868            body = {
   1869                "script": script.strip(),
   1870                "args": args,
   1871                "newSandbox": new_sandbox,
   1872                "sandbox": sandbox,
   1873                "line": int(frame[1]),
   1874                "filename": filename,
   1875            }
   1876            rv = self._send_message("WebDriver:ExecuteScript", body, key="value")
   1877 
   1878        finally:
   1879            if script_timeout is not None:
   1880                self.timeout.script = original_timeout
   1881 
   1882        return rv
   1883 
   1884    def execute_async_script(
   1885        self,
   1886        script,
   1887        script_args=(),
   1888        new_sandbox=True,
   1889        sandbox="default",
   1890        script_timeout=None,
   1891    ):
   1892        """Executes an asynchronous JavaScript script, and returns the
   1893        result (or None if the script does return a value).
   1894 
   1895        The script is executed in the context set by the most recent
   1896        :func:`set_context` call, or to the CONTEXT_CONTENT context if
   1897        :func:`set_context` has not been called.
   1898 
   1899        :param script: A string containing the JavaScript to execute.
   1900        :param script_args: An interable of arguments to pass to the script.
   1901        :param new_sandbox: If False, preserve global variables from
   1902            the last execute_*script call. This is True by default,
   1903            in which case no globals are preserved.
   1904        :param sandbox: A tag referring to the sandbox you wish to use; if
   1905            you specify a new tag, a new sandbox will be created.  If you
   1906            use the special tag `system`, the sandbox will be created
   1907            using the system principal which has elevated privileges.
   1908        :param script_timeout: Timeout in milliseconds, overriding
   1909            the session's default script timeout.
   1910 
   1911        Usage example:
   1912 
   1913        ::
   1914 
   1915            marionette.timeout.script = 10
   1916            result = self.marionette.execute_async_script('''
   1917              // this script waits 5 seconds, and then returns the number 1
   1918              let [resolve] = arguments;
   1919              setTimeout(function() {
   1920                resolve(1);
   1921              }, 5000);
   1922            ''')
   1923            assert result == 1
   1924        """
   1925        original_timeout = None
   1926        if script_timeout is not None:
   1927            original_timeout = self.timeout.script
   1928            self.timeout.script = script_timeout / 1000.0
   1929 
   1930        try:
   1931            args = self._to_json(script_args)
   1932            stack = traceback.extract_stack()
   1933            frame = stack[-2:-1][0]  # grab the second-to-last frame
   1934            filename = (
   1935                frame[0] if sys.platform == "win32" else os.path.relpath(frame[0])
   1936            )
   1937            body = {
   1938                "script": script.strip(),
   1939                "args": args,
   1940                "newSandbox": new_sandbox,
   1941                "sandbox": sandbox,
   1942                "scriptTimeout": script_timeout,
   1943                "line": int(frame[1]),
   1944                "filename": filename,
   1945            }
   1946            rv = self._send_message("WebDriver:ExecuteAsyncScript", body, key="value")
   1947 
   1948        finally:
   1949            if script_timeout is not None:
   1950                self.timeout.script = original_timeout
   1951 
   1952        return rv
   1953 
   1954    def find_element(self, method, target, id=None):
   1955        """Returns an :class:`~marionette_driver.marionette.WebElement`
   1956        instance that matches the specified method and target in the current
   1957        context.
   1958 
   1959        An :class:`~marionette_driver.marionette.WebElement` instance may be
   1960        used to call other methods on the element, such as
   1961        :func:`~marionette_driver.marionette.WebElement.click`.  If no element
   1962        is immediately found, the attempt to locate an element will be repeated
   1963        for up to the amount of time set by
   1964        :attr:`marionette_driver.timeout.Timeouts.implicit`. If multiple
   1965        elements match the given criteria, only the first is returned. If no
   1966        element matches, a ``NoSuchElementException`` will be raised.
   1967 
   1968        :param method: The method to use to locate the element; one of:
   1969            "id", "name", "class name", "tag name", "css selector",
   1970            "link text", "partial link text" and "xpath".
   1971            Note that the "name", "link text" and "partial link test"
   1972            methods are not supported in the chrome DOM.
   1973        :param target: The target of the search.  For example, if method =
   1974            "tag", target might equal "div".  If method = "id", target would
   1975            be an element id.
   1976        :param id: If specified, search for elements only inside the element
   1977            with the specified id.
   1978        """
   1979        body = {"value": target, "using": method}
   1980        if id:
   1981            body["element"] = id
   1982 
   1983        return self._send_message("WebDriver:FindElement", body, key="value")
   1984 
   1985    def find_elements(self, method, target, id=None):
   1986        """Returns a list of all
   1987        :class:`~marionette_driver.marionette.WebElement` instances that match
   1988        the specified method and target in the current context.
   1989 
   1990        An :class:`~marionette_driver.marionette.WebElement` instance may be
   1991        used to call other methods on the element, such as
   1992        :func:`~marionette_driver.marionette.WebElement.click`.  If no element
   1993        is immediately found, the attempt to locate an element will be repeated
   1994        for up to the amount of time set by
   1995        :attr:`marionette_driver.timeout.Timeouts.implicit`.
   1996 
   1997        :param method: The method to use to locate the elements; one
   1998            of: "id", "name", "class name", "tag name", "css selector",
   1999            "link text", "partial link text" and "xpath".
   2000            Note that the "name", "link text" and "partial link test"
   2001            methods are not supported in the chrome DOM.
   2002        :param target: The target of the search.  For example, if method =
   2003            "tag", target might equal "div".  If method = "id", target would be
   2004            an element id.
   2005        :param id: If specified, search for elements only inside the element
   2006            with the specified id.
   2007        """
   2008        body = {"value": target, "using": method}
   2009        if id:
   2010            body["element"] = id
   2011 
   2012        return self._send_message("WebDriver:FindElements", body)
   2013 
   2014    def generate_test_report(self, message, group=None):
   2015        """Generates a test report to be observed by registered reporting observers
   2016 
   2017        :param message: The message string to be used as the body of the generated report
   2018        :param group: The name of the endpoint that will receive the report
   2019        """
   2020        body = {"message": message}
   2021        if group is not None:
   2022            body["group"] = group
   2023 
   2024        self._send_message("Reporting:GenerateTestReport", body)
   2025 
   2026    def get_active_element(self):
   2027        el_or_ref = self._send_message("WebDriver:GetActiveElement", key="value")
   2028        return el_or_ref
   2029 
   2030    def add_cookie(self, cookie):
   2031        """Adds a cookie to your current session.
   2032 
   2033        :param cookie: A dictionary object, with required keys - "name"
   2034            and "value"; optional keys - "path", "domain", "secure",
   2035            "expiry".
   2036 
   2037        Usage example:
   2038 
   2039        ::
   2040 
   2041            driver.add_cookie({"name": "foo", "value": "bar"})
   2042            driver.add_cookie({"name": "foo", "value": "bar", "path": "/"})
   2043            driver.add_cookie({"name": "foo", "value": "bar", "path": "/",
   2044                               "secure": True})
   2045        """
   2046        self._send_message("WebDriver:AddCookie", {"cookie": cookie})
   2047 
   2048    def delete_all_cookies(self):
   2049        """Delete all cookies in the scope of the current session.
   2050 
   2051        Usage example:
   2052 
   2053        ::
   2054 
   2055            driver.delete_all_cookies()
   2056        """
   2057        self._send_message("WebDriver:DeleteAllCookies")
   2058 
   2059    def delete_cookie(self, name):
   2060        """Delete a cookie by its name.
   2061 
   2062        :param name: Name of cookie to delete.
   2063 
   2064        Usage example:
   2065 
   2066        ::
   2067 
   2068            driver.delete_cookie("foo")
   2069        """
   2070        self._send_message("WebDriver:DeleteCookie", {"name": name})
   2071 
   2072    def get_cookie(self, name):
   2073        """Get a single cookie by name. Returns the cookie if found,
   2074        None if not.
   2075 
   2076        :param name: Name of cookie to get.
   2077        """
   2078        cookies = self.get_cookies()
   2079        for cookie in cookies:
   2080            if cookie["name"] == name:
   2081                return cookie
   2082        return None
   2083 
   2084    def get_cookies(self):
   2085        """Get all the cookies for the current domain.
   2086 
   2087        This is the equivalent of calling `document.cookie` and
   2088        parsing the result.
   2089 
   2090        :returns: A list of cookies for the current domain.
   2091        """
   2092        return self._send_message("WebDriver:GetCookies")
   2093 
   2094    def save_screenshot(self, fh, element=None, full=True, scroll=True):
   2095        """Takes a screenhot of a web element or the current frame and
   2096        saves it in the filehandle.
   2097 
   2098        It is a wrapper around screenshot()
   2099        :param fh: The filehandle to save the screenshot at.
   2100 
   2101        The rest of the parameters are defined like in screenshot()
   2102        """
   2103        data = self.screenshot(element, "binary", full, scroll)
   2104        fh.write(data)
   2105 
   2106    def screenshot(self, element=None, format="base64", full=True, scroll=True):
   2107        """Takes a screenshot of a web element or the current frame.
   2108 
   2109        The screen capture is returned as a lossless PNG image encoded
   2110        as a base 64 string by default. If the `element` argument is defined the
   2111        capture area will be limited to the bounding box of that
   2112        element.  Otherwise, the capture area will be the bounding box
   2113        of the current frame.
   2114 
   2115        :param element: The element to take a screenshot of.  If None, will
   2116            take a screenshot of the current frame.
   2117 
   2118        :param format: if "base64" (the default), returns the screenshot
   2119            as a base64-string. If "binary", the data is decoded and
   2120            returned as raw binary. If "hash", the data is hashed using
   2121            the SHA-256 algorithm and the result is returned as a hex digest.
   2122 
   2123        :param full: If True (the default), the capture area will be the
   2124            complete frame. Else only the viewport is captured. Only applies
   2125            when `element` is None.
   2126 
   2127        :param scroll: When `element` is provided, scroll to it before
   2128            taking the screenshot (default).  Otherwise, avoid scrolling
   2129            `element` into view.
   2130        """
   2131 
   2132        if element:
   2133            element = element.id
   2134 
   2135        body = {"id": element, "full": full, "hash": False, "scroll": scroll}
   2136        if format == "hash":
   2137            body["hash"] = True
   2138 
   2139        data = self._send_message("WebDriver:TakeScreenshot", body, key="value")
   2140 
   2141        if format in {"base64", "hash"}:
   2142            return data
   2143        elif format == "binary":
   2144            return base64.b64decode(data.encode("ascii"))
   2145        else:
   2146            raise ValueError(
   2147                "format parameter must be either 'base64'"
   2148                f" or 'binary', not {repr(format)}"
   2149            )
   2150 
   2151    @property
   2152    def orientation(self):
   2153        """Get the current browser orientation.
   2154 
   2155        Will return one of the valid primary orientation values
   2156        portrait-primary, landscape-primary, portrait-secondary, or
   2157        landscape-secondary.
   2158        """
   2159        try:
   2160            return self._send_message("Marionette:GetScreenOrientation", key="value")
   2161        except errors.UnknownCommandException:
   2162            return self._send_message("getScreenOrientation", key="value")
   2163 
   2164    def set_orientation(self, orientation):
   2165        """Set the current browser orientation.
   2166 
   2167        The supplied orientation should be given as one of the valid
   2168        orientation values.  If the orientation is unknown, an error
   2169        will be raised.
   2170 
   2171        Valid orientations are "portrait" and "landscape", which fall
   2172        back to "portrait-primary" and "landscape-primary"
   2173        respectively, and "portrait-secondary" as well as
   2174        "landscape-secondary".
   2175 
   2176        :param orientation: The orientation to lock the screen in.
   2177        """
   2178        body = {"orientation": orientation}
   2179        try:
   2180            self._send_message("Marionette:SetScreenOrientation", body)
   2181        except errors.UnknownCommandException:
   2182            self._send_message("setScreenOrientation", body)
   2183 
   2184    def minimize_window(self):
   2185        """Iconify the browser window currently receiving commands.
   2186        The action should be equivalent to the user pressing the minimize
   2187        button in the OS window.
   2188 
   2189        Note that this command is not available on Fennec.  It may also
   2190        not be available in certain window managers.
   2191 
   2192        :returns Window rect.
   2193        """
   2194        return self._send_message("WebDriver:MinimizeWindow")
   2195 
   2196    def maximize_window(self):
   2197        """Resize the browser window currently receiving commands.
   2198        The action should be equivalent to the user pressing the maximize
   2199        button in the OS window.
   2200 
   2201 
   2202        Note that this command is not available on Fennec.  It may also
   2203        not be available in certain window managers.
   2204 
   2205        :returns: Window rect.
   2206        """
   2207        return self._send_message("WebDriver:MaximizeWindow")
   2208 
   2209    def fullscreen(self):
   2210        """Synchronously sets the user agent window to full screen as
   2211        if the user had done "View > Enter Full Screen",  or restores
   2212        it if it is already in full screen.
   2213 
   2214        :returns: Window rect.
   2215        """
   2216        return self._send_message("WebDriver:FullscreenWindow")
   2217 
   2218    def set_permission(self, descriptor, state):
   2219        """Set the permission for the origin of the current page."""
   2220        body = {
   2221            "descriptor": descriptor,
   2222            "state": state,
   2223        }
   2224        return self._send_message("WebDriver:SetPermission", body)