client.py (30708B)
1 # mypy: allow-untyped-defs 2 3 from typing import Dict 4 from urllib import parse as urlparse 5 6 from . import error 7 from . import protocol 8 from . import transport 9 from .bidi.client import BidiSession 10 11 12 class Timeouts: 13 14 def __init__(self, session): 15 self.session = session 16 17 def _get(self, key=None): 18 timeouts = self.session.send_session_command("GET", "timeouts") 19 if key is not None: 20 return timeouts[key] 21 return timeouts 22 23 def _set(self, key, secs): 24 body = {key: secs * 1000} 25 self.session.send_session_command("POST", "timeouts", body) 26 return None 27 28 @property 29 def script(self): 30 return self._get("script") 31 32 @script.setter 33 def script(self, secs): 34 return self._set("script", secs) 35 36 @property 37 def page_load(self): 38 return self._get("pageLoad") 39 40 @page_load.setter 41 def page_load(self, secs): 42 return self._set("pageLoad", secs) 43 44 @property 45 def implicit(self): 46 return self._get("implicit") 47 48 @implicit.setter 49 def implicit(self, secs): 50 return self._set("implicit", secs) 51 52 def __str__(self): 53 name = "%s.%s" % (self.__module__, self.__class__.__name__) 54 return "<%s script=%d, load=%d, implicit=%d>" % ( 55 name, 56 self.script, 57 self.page_load, 58 self.implicit, 59 ) 60 61 62 class ActionSequence: 63 """API for creating and performing action sequences. 64 65 Each action method adds one or more actions to a queue. When perform() 66 is called, the queued actions fire in order. 67 68 May be chained together as in:: 69 70 ActionSequence(session, "key", id) \ 71 .key_down("a") \ 72 .key_up("a") \ 73 .perform() 74 """ 75 76 def __init__(self, session, action_type, input_id, pointer_params=None): 77 """Represents a sequence of actions of one type for one input source. 78 79 :param session: WebDriver session. 80 :param action_type: Action type; may be "none", "key", or "pointer". 81 :param input_id: ID of input source. 82 :param pointer_params: Optional dictionary of pointer parameters. 83 """ 84 self.session = session 85 self._id = input_id 86 self._type = action_type 87 self._actions = [] 88 self._pointer_params = pointer_params 89 90 @property 91 def dict(self): 92 d = { 93 "type": self._type, 94 "id": self._id, 95 "actions": self._actions, 96 } 97 if self._pointer_params is not None: 98 d["parameters"] = self._pointer_params 99 return d 100 101 def perform(self): 102 """Perform all queued actions.""" 103 self.session.actions.perform([self.dict]) 104 105 def _key_action(self, subtype, value): 106 self._actions.append({"type": subtype, "value": value}) 107 108 def _pointer_action( 109 self, 110 subtype, 111 button=None, 112 x=None, 113 y=None, 114 duration=None, 115 origin=None, 116 width=None, 117 height=None, 118 pressure=None, 119 tangential_pressure=None, 120 tilt_x=None, 121 tilt_y=None, 122 twist=None, 123 altitude_angle=None, 124 azimuth_angle=None, 125 ): 126 action = {"type": subtype} 127 if button is not None: 128 action["button"] = button 129 if x is not None: 130 action["x"] = x 131 if y is not None: 132 action["y"] = y 133 if duration is not None: 134 action["duration"] = duration 135 if origin is not None: 136 action["origin"] = origin 137 if width is not None: 138 action["width"] = width 139 if height is not None: 140 action["height"] = height 141 if pressure is not None: 142 action["pressure"] = pressure 143 if tangential_pressure is not None: 144 action["tangentialPressure"] = tangential_pressure 145 if tilt_x is not None: 146 action["tiltX"] = tilt_x 147 if tilt_y is not None: 148 action["tiltY"] = tilt_y 149 if twist is not None: 150 action["twist"] = twist 151 if altitude_angle is not None: 152 action["altitudeAngle"] = altitude_angle 153 if azimuth_angle is not None: 154 action["azimuthAngle"] = azimuth_angle 155 self._actions.append(action) 156 157 def pause(self, duration): 158 self._actions.append({"type": "pause", "duration": duration}) 159 return self 160 161 def pointer_move( 162 self, 163 x, 164 y, 165 duration=None, 166 origin=None, 167 width=None, 168 height=None, 169 pressure=None, 170 tangential_pressure=None, 171 tilt_x=None, 172 tilt_y=None, 173 twist=None, 174 altitude_angle=None, 175 azimuth_angle=None, 176 ): 177 """Queue a pointerMove action. 178 179 :param x: Destination x-axis coordinate of pointer in CSS pixels. 180 :param y: Destination y-axis coordinate of pointer in CSS pixels. 181 :param duration: Number of milliseconds over which to distribute the 182 move. If None, remote end defaults to 0. 183 :param origin: Origin of coordinates, either "viewport", "pointer" or 184 an Element. If None, remote end defaults to "viewport". 185 """ 186 self._pointer_action( 187 "pointerMove", 188 x=x, 189 y=y, 190 duration=duration, 191 origin=origin, 192 width=width, 193 height=height, 194 pressure=pressure, 195 tangential_pressure=tangential_pressure, 196 tilt_x=tilt_x, 197 tilt_y=tilt_y, 198 twist=twist, 199 altitude_angle=altitude_angle, 200 azimuth_angle=azimuth_angle, 201 ) 202 return self 203 204 def pointer_up(self, button=0): 205 """Queue a pointerUp action for `button`. 206 207 :param button: Pointer button to perform action with. 208 Default: 0, which represents main device button. 209 """ 210 self._pointer_action("pointerUp", button=button) 211 return self 212 213 def pointer_down( 214 self, 215 button=0, 216 width=None, 217 height=None, 218 pressure=None, 219 tangential_pressure=None, 220 tilt_x=None, 221 tilt_y=None, 222 twist=None, 223 altitude_angle=None, 224 azimuth_angle=None, 225 ): 226 """Queue a pointerDown action for `button`. 227 228 :param button: Pointer button to perform action with. 229 Default: 0, which represents main device button. 230 """ 231 self._pointer_action( 232 "pointerDown", 233 button=button, 234 width=width, 235 height=height, 236 pressure=pressure, 237 tangential_pressure=tangential_pressure, 238 tilt_x=tilt_x, 239 tilt_y=tilt_y, 240 twist=twist, 241 altitude_angle=altitude_angle, 242 azimuth_angle=azimuth_angle, 243 ) 244 return self 245 246 def click(self, element=None, button=0): 247 """Queue a click with the specified button. 248 249 If an element is given, move the pointer to that element first, 250 otherwise click current pointer coordinates. 251 252 :param element: Optional element to click. 253 :param button: Integer representing pointer button to perform action 254 with. Default: 0, which represents main device button. 255 """ 256 if element: 257 self.pointer_move(0, 0, origin=element) 258 return self.pointer_down(button).pointer_up(button) 259 260 def key_up(self, value): 261 """Queue a keyUp action for `value`. 262 263 :param value: Character to perform key action with. 264 """ 265 self._key_action("keyUp", value) 266 return self 267 268 def key_down(self, value): 269 """Queue a keyDown action for `value`. 270 271 :param value: Character to perform key action with. 272 """ 273 self._key_action("keyDown", value) 274 return self 275 276 def send_keys(self, keys): 277 """Queue a keyDown and keyUp action for each character in `keys`. 278 279 :param keys: String of keys to perform key actions with. 280 """ 281 for c in keys: 282 self.key_down(c) 283 self.key_up(c) 284 return self 285 286 def scroll(self, x, y, delta_x, delta_y, duration=None, origin=None): 287 """Queue a scroll action. 288 289 :param x: Destination x-axis coordinate of pointer in CSS pixels. 290 :param y: Destination y-axis coordinate of pointer in CSS pixels. 291 :param delta_x: scroll delta on x-axis in CSS pixels. 292 :param delta_y: scroll delta on y-axis in CSS pixels. 293 :param duration: Number of milliseconds over which to distribute the 294 scroll. If None, remote end defaults to 0. 295 :param origin: Origin of coordinates, either "viewport" or an Element. 296 If None, remote end defaults to "viewport". 297 """ 298 action = { 299 "type": "scroll", 300 "x": x, 301 "y": y, 302 "deltaX": delta_x, 303 "deltaY": delta_y, 304 } 305 if duration is not None: 306 action["duration"] = duration 307 if origin is not None: 308 action["origin"] = origin 309 self._actions.append(action) 310 return self 311 312 313 class Actions: 314 def __init__(self, session): 315 self.session = session 316 317 def perform(self, actions=None): 318 """Performs actions by tick from each action sequence in `actions`. 319 320 :param actions: List of input source action sequences. A single action 321 sequence may be created with the help of 322 ``ActionSequence.dict``. 323 """ 324 body = {"actions": [] if actions is None else actions} 325 actions = self.session.send_session_command("POST", "actions", body) 326 return actions 327 328 def release(self): 329 return self.session.send_session_command("DELETE", "actions") 330 331 def sequence(self, *args, **kwargs): 332 """Return an empty ActionSequence of the designated type. 333 334 See ActionSequence for parameter list. 335 """ 336 return ActionSequence(self.session, *args, **kwargs) 337 338 339 class BrowserWindow: 340 def __init__(self, session): 341 self.session = session 342 343 def close(self): 344 handles = self.session.send_session_command("DELETE", "window") 345 if handles is not None and len(handles) == 0: 346 # With no more open top-level browsing contexts, the session is closed. 347 self.session.session_id = None 348 349 return handles 350 351 @property 352 def rect(self): 353 return self.session.send_session_command("GET", "window/rect") 354 355 @rect.setter 356 def rect(self, new_rect): 357 self.session.send_session_command("POST", "window/rect", new_rect) 358 359 @property 360 def size(self): 361 """Gets the window size as a tuple of `(width, height)`.""" 362 rect = self.rect 363 return (rect["width"], rect["height"]) 364 365 @size.setter 366 def size(self, new_size): 367 """Set window size by passing a tuple of `(width, height)`.""" 368 try: 369 width, height = new_size 370 body = {"width": width, "height": height} 371 self.session.send_session_command("POST", "window/rect", body) 372 except (error.UnknownErrorException, error.InvalidArgumentException): 373 # silently ignore this error as the command is not implemented 374 # for Android. Revert this once it is implemented. 375 pass 376 377 @property 378 def position(self): 379 """Gets the window position as a tuple of `(x, y)`.""" 380 rect = self.rect 381 return (rect["x"], rect["y"]) 382 383 @position.setter 384 def position(self, new_position): 385 """Set window position by passing a tuple of `(x, y)`.""" 386 try: 387 x, y = new_position 388 body = {"x": x, "y": y} 389 self.session.send_session_command("POST", "window/rect", body) 390 except error.UnknownErrorException: 391 # silently ignore this error as the command is not implemented 392 # for Android. Revert this once it is implemented. 393 pass 394 395 def maximize(self): 396 return self.session.send_session_command("POST", "window/maximize") 397 398 def minimize(self): 399 return self.session.send_session_command("POST", "window/minimize") 400 401 def fullscreen(self): 402 return self.session.send_session_command("POST", "window/fullscreen") 403 404 405 class Find: 406 def __init__(self, session): 407 self.session = session 408 409 def css(self, element_selector, all=True): 410 elements = self._find_element("css selector", element_selector, all) 411 return elements 412 413 def _find_element(self, strategy, selector, all): 414 route = "elements" if all else "element" 415 body = {"using": strategy, "value": selector} 416 return self.session.send_session_command("POST", route, body) 417 418 419 class UserPrompt: 420 def __init__(self, session): 421 self.session = session 422 423 def dismiss(self): 424 self.session.send_session_command("POST", "alert/dismiss") 425 426 def accept(self): 427 self.session.send_session_command("POST", "alert/accept") 428 429 @property 430 def text(self): 431 return self.session.send_session_command("GET", "alert/text") 432 433 @text.setter 434 def text(self, value): 435 body = {"text": value} 436 self.session.send_session_command("POST", "alert/text", body=body) 437 438 439 class Session: 440 def __init__( 441 self, 442 host, 443 port, 444 url_prefix="/", 445 enable_bidi=False, 446 capabilities=None, 447 extension=None, 448 ): 449 450 if enable_bidi: 451 if capabilities is not None: 452 capabilities.setdefault("alwaysMatch", {}).update( 453 {"webSocketUrl": True} 454 ) 455 else: 456 capabilities = {"alwaysMatch": {"webSocketUrl": True}} 457 458 self.transport = transport.HTTPWireProtocol(host, port, url_prefix) 459 self.requested_capabilities = capabilities 460 self.capabilities = None 461 self.session_id = None 462 self.timeouts = None 463 self.window = None 464 self.find = None 465 self.enable_bidi = enable_bidi 466 self.bidi_session = None 467 self.extension = None 468 self.extension_cls = extension 469 470 self.timeouts = Timeouts(self) 471 self.window = BrowserWindow(self) 472 self.find = Find(self) 473 self.alert = UserPrompt(self) 474 self.actions = Actions(self) 475 self.web_extensions = WebExtensions(self) 476 477 def __repr__(self): 478 return "<%s %s>" % ( 479 self.__class__.__name__, 480 self.session_id or "(disconnected)", 481 ) 482 483 def __eq__(self, other): 484 return ( 485 self.session_id is not None 486 and isinstance(other, Session) 487 and self.session_id == other.session_id 488 ) 489 490 def __enter__(self): 491 self.start() 492 return self 493 494 def __exit__(self, *args, **kwargs): 495 self.end() 496 497 def __del__(self): 498 self.end() 499 500 def match(self, capabilities): 501 return self.requested_capabilities == capabilities 502 503 def start(self): 504 """Start a new WebDriver session. 505 506 :return: Dictionary with `capabilities` and `sessionId`. 507 508 :raises error.WebDriverException: If the remote end returns 509 an error. 510 """ 511 if self.session_id is not None: 512 return 513 514 self.transport.close() 515 516 body = {"capabilities": {}} 517 518 if self.requested_capabilities is not None: 519 body["capabilities"] = self.requested_capabilities 520 521 try: 522 value = self.send_command("POST", "session", body=body) 523 assert isinstance(value["sessionId"], str) 524 assert isinstance(value["capabilities"], Dict) 525 526 self.session_id = value["sessionId"] 527 self.capabilities = value["capabilities"] 528 529 if "webSocketUrl" in self.capabilities: 530 self.bidi_session = BidiSession.from_http( 531 self.session_id, self.capabilities 532 ) 533 elif self.enable_bidi: 534 self.end() 535 raise error.SessionNotCreatedException( 536 "Requested bidi session, but webSocketUrl capability not found" 537 ) 538 539 if self.extension_cls: 540 self.extension = self.extension_cls(self) 541 542 return value 543 544 except Exception: 545 # Make sure we end up back in a consistent state. 546 self.end() 547 raise 548 549 def end(self): 550 """Try to close the active session.""" 551 try: 552 if self.session_id is not None: 553 self.send_command("DELETE", "session/%s" % self.session_id) 554 except (OSError, error.InvalidSessionIdException): 555 pass 556 finally: 557 self.session_id = None 558 self.capabilities = None 559 self.bidi_session = None 560 self.extension = None 561 self.transport.close() 562 563 def send_command(self, method, url, body=None, timeout=None): 564 """ 565 Send a command to the remote end and validate its success. 566 567 :param method: HTTP method to use in request. 568 :param uri: "Command part" of the HTTP request URL, 569 e.g. `window/rect`. 570 :param body: Optional body of the HTTP request. 571 572 :return: `None` if the HTTP response body was empty, otherwise 573 the `value` field returned after parsing the response 574 body as JSON. 575 576 :raises error.WebDriverException: If the remote end returns 577 an error. 578 :raises ValueError: If the response body does not contain a 579 `value` key. 580 """ 581 582 response = self.transport.send( 583 method, 584 url, 585 body, 586 encoder=protocol.Encoder, 587 decoder=protocol.Decoder, 588 session=self, 589 timeout=timeout, 590 ) 591 592 if response.status != 200: 593 err = error.from_response(response) 594 595 if isinstance(err, error.InvalidSessionIdException): 596 # The driver could have already been deleted the session. 597 self.session_id = None 598 599 raise err 600 601 if "value" in response.body: 602 value = response.body["value"] 603 """ 604 Edge does not yet return the w3c session ID. 605 We want the tests to run in Edge anyway to help with REC. 606 In order to run the tests in Edge, we need to hack around 607 bug: 608 https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14641972 609 """ 610 if ( 611 url == "session" 612 and method == "POST" 613 and "sessionId" in response.body 614 and "sessionId" not in value 615 ): 616 value["sessionId"] = response.body["sessionId"] 617 else: 618 raise ValueError("Expected 'value' key in response body:\n" "%s" % response) 619 620 return value 621 622 def send_session_command(self, method, uri, body=None, timeout=None): 623 """ 624 Send a command to an established session and validate its success. 625 626 :param method: HTTP method to use in request. 627 :param url: "Command part" of the HTTP request URL, 628 e.g. `window/rect`. 629 :param body: Optional body of the HTTP request. Must be JSON 630 serialisable. 631 632 :return: `None` if the HTTP response body was empty, otherwise 633 the result of parsing the body as JSON. 634 635 :raises error.WebDriverException: If the remote end returns 636 an error. 637 """ 638 if not isinstance(self.session_id, str): 639 raise TypeError( 640 "Session.session_id must be a str to send a session command" 641 ) 642 643 url = urlparse.urljoin("session/%s/" % self.session_id, uri) 644 return self.send_command(method, url, body, timeout) 645 646 @property 647 def url(self): 648 return self.send_session_command("GET", "url") 649 650 @url.setter 651 def url(self, url): 652 if urlparse.urlsplit(url).netloc is None: 653 return self.url(url) 654 body = {"url": url} 655 return self.send_session_command("POST", "url", body) 656 657 def back(self): 658 return self.send_session_command("POST", "back") 659 660 def forward(self): 661 return self.send_session_command("POST", "forward") 662 663 def refresh(self): 664 return self.send_session_command("POST", "refresh") 665 666 @property 667 def title(self): 668 return self.send_session_command("GET", "title") 669 670 @property 671 def source(self): 672 return self.send_session_command("GET", "source") 673 674 def new_window(self, type_hint="tab"): 675 body = {"type": type_hint} 676 value = self.send_session_command("POST", "window/new", body) 677 678 return value["handle"] 679 680 @property 681 def window_handle(self): 682 return self.send_session_command("GET", "window") 683 684 @window_handle.setter 685 def window_handle(self, handle): 686 body = {"handle": handle} 687 return self.send_session_command("POST", "window", body=body) 688 689 def switch_to_frame(self, frame): 690 body = {"id": frame} 691 return self.send_session_command("POST", "frame", body=body) 692 693 def switch_to_parent_frame(self): 694 return self.send_session_command("POST", "frame/parent") 695 696 @property 697 def handles(self): 698 return self.send_session_command("GET", "window/handles") 699 700 @property 701 def active_element(self): 702 return self.send_session_command("GET", "element/active") 703 704 def cookies(self, name=None): 705 if name is None: 706 url = "cookie" 707 elif isinstance(name, str): 708 url = "cookie/%s" % name 709 else: 710 raise TypeError("cookie name must be a str or None") 711 return self.send_session_command("GET", url, {}) 712 713 def set_cookie( 714 self, 715 name, 716 value, 717 path=None, 718 domain=None, 719 secure=None, 720 expiry=None, 721 http_only=None, 722 ): 723 body = { 724 "name": name, 725 "value": value, 726 } 727 728 if domain is not None: 729 body["domain"] = domain 730 if expiry is not None: 731 body["expiry"] = expiry 732 if http_only is not None: 733 body["httpOnly"] = http_only 734 if path is not None: 735 body["path"] = path 736 if secure is not None: 737 body["secure"] = secure 738 self.send_session_command("POST", "cookie", {"cookie": body}) 739 740 def delete_cookie(self, name=None): 741 if name is None: 742 url = "cookie" 743 elif isinstance(name, str): 744 url = "cookie/%s" % name 745 else: 746 raise TypeError("cookie name must be a str or None") 747 self.send_session_command("DELETE", url, {}) 748 749 # [...] 750 751 def set_global_privacy_control(self, gpc): 752 body = { 753 "gpc": gpc, 754 } 755 return self.send_session_command("POST", "privacy", body) 756 757 def get_global_privacy_control(self): 758 return self.send_session_command("GET", "privacy") 759 760 # [...] 761 762 def execute_script(self, script, args=None): 763 if args is None: 764 args = [] 765 766 body = {"script": script, "args": args} 767 return self.send_session_command("POST", "execute/sync", body) 768 769 def execute_async_script(self, script, args=None): 770 if args is None: 771 args = [] 772 773 body = {"script": script, "args": args} 774 return self.send_session_command("POST", "execute/async", body) 775 776 # [...] 777 778 def screenshot(self): 779 return self.send_session_command("GET", "screenshot") 780 781 def print( 782 self, 783 background=None, 784 margin=None, 785 orientation=None, 786 page=None, 787 page_ranges=None, 788 scale=None, 789 shrink_to_fit=None, 790 ): 791 body = {} 792 for prop, value in { 793 "background": background, 794 "margin": margin, 795 "orientation": orientation, 796 "page": page, 797 "pageRanges": page_ranges, 798 "scale": scale, 799 "shrinkToFit": shrink_to_fit, 800 }.items(): 801 if value is not None: 802 body[prop] = value 803 return self.send_session_command("POST", "print", body) 804 805 806 class ShadowRoot: 807 identifier = "shadow-6066-11e4-a52e-4f735466cecf" 808 809 def __init__(self, session, id): 810 """ 811 Construct a new shadow root representation. 812 813 :param id: Shadow root UUID which must be unique across 814 all browsing contexts. 815 :param session: Current ``webdriver.Session``. 816 """ 817 self.id = id 818 self.session = session 819 820 @classmethod 821 def from_json(cls, json, session): 822 uuid = json[ShadowRoot.identifier] 823 assert isinstance(uuid, str) 824 825 return cls(session, uuid) 826 827 def send_shadow_command(self, method, uri, body=None): 828 if not isinstance(self.id, str): 829 raise TypeError("self.id must be a str") 830 831 if not isinstance(uri, str): 832 raise TypeError("uri must be a str") 833 834 url = f"shadow/{self.id}/{uri}" 835 return self.session.send_session_command(method, url, body) 836 837 def find_element(self, strategy, selector): 838 body = {"using": strategy, "value": selector} 839 return self.send_shadow_command("POST", "element", body) 840 841 def find_elements(self, strategy, selector): 842 body = {"using": strategy, "value": selector} 843 return self.send_shadow_command("POST", "elements", body) 844 845 846 class WebElement: 847 """ 848 Representation of a web element. 849 850 A web element is an abstraction used to identify an element when 851 it is transported via the protocol, between remote- and local ends. 852 """ 853 854 identifier = "element-6066-11e4-a52e-4f735466cecf" 855 856 def __init__(self, session, id): 857 """ 858 Construct a new web element representation. 859 860 :param session: Current ``webdriver.Session``. 861 :param id: Web element UUID which must be unique across all browsing contexts. 862 """ 863 self.id = id 864 self.session = session 865 866 def __repr__(self): 867 return "<%s %s>" % (self.__class__.__name__, self.id) 868 869 def __eq__(self, other): 870 return ( 871 isinstance(other, WebElement) 872 and self.id == other.id 873 and self.session == other.session 874 ) 875 876 @classmethod 877 def from_json(cls, json, session): 878 uuid = json[WebElement.identifier] 879 assert isinstance(uuid, str) 880 881 return cls(session, uuid) 882 883 def send_element_command(self, method, uri, body=None): 884 if not isinstance(self.id, str): 885 raise TypeError("WebElement.id must be a str") 886 887 if not isinstance(uri, str): 888 raise TypeError("uri must be a str") 889 890 url = "element/%s/%s" % (self.id, uri) 891 return self.session.send_session_command(method, url, body) 892 893 def find_element(self, strategy, selector): 894 body = {"using": strategy, "value": selector} 895 return self.send_element_command("POST", "element", body) 896 897 def click(self): 898 self.send_element_command("POST", "click", {}) 899 900 def tap(self): 901 self.send_element_command("POST", "tap", {}) 902 903 def clear(self): 904 self.send_element_command("POST", "clear", {}) 905 906 def send_keys(self, text): 907 return self.send_element_command("POST", "value", {"text": text}) 908 909 @property 910 def text(self): 911 return self.send_element_command("GET", "text") 912 913 @property 914 def name(self): 915 return self.send_element_command("GET", "name") 916 917 def style(self, property_name): 918 if not isinstance(property_name, str): 919 raise TypeError("property_name must be a str") 920 921 return self.send_element_command("GET", "css/%s" % property_name) 922 923 @property 924 def rect(self): 925 return self.send_element_command("GET", "rect") 926 927 @property 928 def selected(self): 929 return self.send_element_command("GET", "selected") 930 931 def screenshot(self): 932 return self.send_element_command("GET", "screenshot") 933 934 @property 935 def shadow_root(self): 936 return self.send_element_command("GET", "shadow") 937 938 def attribute(self, name): 939 if not isinstance(name, str): 940 raise TypeError("name must be a str") 941 942 return self.send_element_command("GET", "attribute/%s" % name) 943 944 def get_computed_label(self): 945 return self.send_element_command("GET", "computedlabel") 946 947 def get_computed_role(self): 948 return self.send_element_command("GET", "computedrole") 949 950 # This MUST come last because otherwise @property decorators above 951 # will be overridden by this. 952 def property(self, name): 953 if not isinstance(name, str): 954 raise TypeError("name must be a str") 955 956 return self.send_element_command("GET", "property/%s" % name) 957 958 959 class WebExtensions: 960 def __init__(self, session): 961 self.session = session 962 963 def install(self, type, path=None, value=None): 964 body = {"type": type} 965 if path is not None: 966 body["path"] = path 967 elif value is not None: 968 body["value"] = value 969 return self.session.send_session_command("POST", "webextension", body) 970 971 def uninstall(self, extension_id): 972 return self.session.send_session_command( 973 "DELETE", "webextension/%s" % extension_id 974 ) 975 976 977 class WebFrame: 978 identifier = "frame-075b-4da1-b6ba-e579c2d3230a" 979 980 def __init__(self, session, id): 981 self.id = id 982 self.session = session 983 984 def __repr__(self): 985 return "<%s %s>" % (self.__class__.__name__, self.id) 986 987 def __eq__(self, other): 988 return ( 989 isinstance(other, WebFrame) 990 and self.id == other.id 991 and self.session == other.session 992 ) 993 994 @classmethod 995 def from_json(cls, json, session): 996 uuid = json[WebFrame.identifier] 997 assert isinstance(uuid, str) 998 999 return cls(session, uuid) 1000 1001 1002 class WebWindow: 1003 identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f" 1004 1005 def __init__(self, session, id): 1006 self.id = id 1007 self.session = session 1008 1009 def __repr__(self): 1010 return "<%s %s>" % (self.__class__.__name__, self.id) 1011 1012 def __eq__(self, other): 1013 return ( 1014 isinstance(other, WebWindow) 1015 and self.id == other.id 1016 and self.session == other.session 1017 ) 1018 1019 @classmethod 1020 def from_json(cls, json, session): 1021 uuid = json[WebWindow.identifier] 1022 assert isinstance(uuid, str) 1023 1024 return cls(session, uuid)