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