Source code

Revision control

Other Tools

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