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