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