1from . import error 2from . import protocol 3from . import transport 4 5from six import string_types 6from six.moves.urllib import parse as urlparse 7 8 9def command(func): 10 def inner(self, *args, **kwargs): 11 if hasattr(self, "session"): 12 session = self.session 13 else: 14 session = self 15 16 if session.session_id is None: 17 session.start() 18 19 return func(self, *args, **kwargs) 20 21 inner.__name__ = func.__name__ 22 inner.__doc__ = func.__doc__ 23 24 return inner 25 26 27class Timeouts(object): 28 29 def __init__(self, session): 30 self.session = session 31 32 def _get(self, key=None): 33 timeouts = self.session.send_session_command("GET", "timeouts") 34 if key is not None: 35 return timeouts[key] 36 return timeouts 37 38 def _set(self, key, secs): 39 body = {key: secs * 1000} 40 self.session.send_session_command("POST", "timeouts", body) 41 return None 42 43 @property 44 def script(self): 45 return self._get("script") 46 47 @script.setter 48 def script(self, secs): 49 return self._set("script", secs) 50 51 @property 52 def page_load(self): 53 return self._get("pageLoad") 54 55 @page_load.setter 56 def page_load(self, secs): 57 return self._set("pageLoad", secs) 58 59 @property 60 def implicit(self): 61 return self._get("implicit") 62 63 @implicit.setter 64 def implicit(self, secs): 65 return self._set("implicit", secs) 66 67 def __str__(self): 68 name = "%s.%s" % (self.__module__, self.__class__.__name__) 69 return "<%s script=%d, load=%d, implicit=%d>" % \ 70 (name, self.script, self.page_load, self.implicit) 71 72 73class ActionSequence(object): 74 """API for creating and performing action sequences. 75 76 Each action method adds one or more actions to a queue. When perform() 77 is called, the queued actions fire in order. 78 79 May be chained together as in:: 80 81 ActionSequence(session, "key", id) \ 82 .key_down("a") \ 83 .key_up("a") \ 84 .perform() 85 """ 86 def __init__(self, session, action_type, input_id, pointer_params=None): 87 """Represents a sequence of actions of one type for one input source. 88 89 :param session: WebDriver session. 90 :param action_type: Action type; may be "none", "key", or "pointer". 91 :param input_id: ID of input source. 92 :param pointer_params: Optional dictionary of pointer parameters. 93 """ 94 self.session = session 95 self._id = input_id 96 self._type = action_type 97 self._actions = [] 98 self._pointer_params = pointer_params 99 100 @property 101 def dict(self): 102 d = { 103 "type": self._type, 104 "id": self._id, 105 "actions": self._actions, 106 } 107 if self._pointer_params is not None: 108 d["parameters"] = self._pointer_params 109 return d 110 111 @command 112 def perform(self): 113 """Perform all queued actions.""" 114 self.session.actions.perform([self.dict]) 115 116 def _key_action(self, subtype, value): 117 self._actions.append({"type": subtype, "value": value}) 118 119 def _pointer_action(self, subtype, button): 120 self._actions.append({"type": subtype, "button": button}) 121 122 def pause(self, duration): 123 self._actions.append({"type": "pause", "duration": duration}) 124 return self 125 126 def pointer_move(self, x, y, duration=None, origin=None): 127 """Queue a pointerMove action. 128 129 :param x: Destination x-axis coordinate of pointer in CSS pixels. 130 :param y: Destination y-axis coordinate of pointer in CSS pixels. 131 :param duration: Number of milliseconds over which to distribute the 132 move. If None, remote end defaults to 0. 133 :param origin: Origin of coordinates, either "viewport", "pointer" or 134 an Element. If None, remote end defaults to "viewport". 135 """ 136 action = { 137 "type": "pointerMove", 138 "x": x, 139 "y": y 140 } 141 if duration is not None: 142 action["duration"] = duration 143 if origin is not None: 144 action["origin"] = origin 145 self._actions.append(action) 146 return self 147 148 def pointer_up(self, button=0): 149 """Queue a pointerUp action for `button`. 150 151 :param button: Pointer button to perform action with. 152 Default: 0, which represents main device button. 153 """ 154 self._pointer_action("pointerUp", button) 155 return self 156 157 def pointer_down(self, button=0): 158 """Queue a pointerDown action for `button`. 159 160 :param button: Pointer button to perform action with. 161 Default: 0, which represents main device button. 162 """ 163 self._pointer_action("pointerDown", button) 164 return self 165 166 def click(self, element=None, button=0): 167 """Queue a click with the specified button. 168 169 If an element is given, move the pointer to that element first, 170 otherwise click current pointer coordinates. 171 172 :param element: Optional element to click. 173 :param button: Integer representing pointer button to perform action 174 with. Default: 0, which represents main device button. 175 """ 176 if element: 177 self.pointer_move(0, 0, origin=element) 178 return self.pointer_down(button).pointer_up(button) 179 180 def key_up(self, value): 181 """Queue a keyUp action for `value`. 182 183 :param value: Character to perform key action with. 184 """ 185 self._key_action("keyUp", value) 186 return self 187 188 def key_down(self, value): 189 """Queue a keyDown action for `value`. 190 191 :param value: Character to perform key action with. 192 """ 193 self._key_action("keyDown", value) 194 return self 195 196 def send_keys(self, keys): 197 """Queue a keyDown and keyUp action for each character in `keys`. 198 199 :param keys: String of keys to perform key actions with. 200 """ 201 for c in keys: 202 self.key_down(c) 203 self.key_up(c) 204 return self 205 206 207class Actions(object): 208 def __init__(self, session): 209 self.session = session 210 211 @command 212 def perform(self, actions=None): 213 """Performs actions by tick from each action sequence in `actions`. 214 215 :param actions: List of input source action sequences. A single action 216 sequence may be created with the help of 217 ``ActionSequence.dict``. 218 """ 219 body = {"actions": [] if actions is None else actions} 220 actions = self.session.send_session_command("POST", "actions", body) 221 """WebDriver window should be set to the top level window when wptrunner 222 processes the next event. 223 """ 224 self.session.switch_frame(None) 225 return actions 226 227 @command 228 def release(self): 229 return self.session.send_session_command("DELETE", "actions") 230 231 def sequence(self, *args, **kwargs): 232 """Return an empty ActionSequence of the designated type. 233 234 See ActionSequence for parameter list. 235 """ 236 return ActionSequence(self.session, *args, **kwargs) 237 238 239class Window(object): 240 identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f" 241 242 def __init__(self, session): 243 self.session = session 244 245 @property 246 @command 247 def rect(self): 248 return self.session.send_session_command("GET", "window/rect") 249 250 @property 251 @command 252 def size(self): 253 """Gets the window size as a tuple of `(width, height)`.""" 254 rect = self.rect 255 return (rect["width"], rect["height"]) 256 257 @size.setter 258 @command 259 def size(self, new_size): 260 """Set window size by passing a tuple of `(width, height)`.""" 261 width, height = new_size 262 body = {"width": width, "height": height} 263 self.session.send_session_command("POST", "window/rect", body) 264 265 @property 266 @command 267 def position(self): 268 """Gets the window position as a tuple of `(x, y)`.""" 269 rect = self.rect 270 return (rect["x"], rect["y"]) 271 272 @position.setter 273 @command 274 def position(self, new_position): 275 """Set window position by passing a tuple of `(x, y)`.""" 276 x, y = new_position 277 body = {"x": x, "y": y} 278 self.session.send_session_command("POST", "window/rect", body) 279 280 @command 281 def maximize(self): 282 return self.session.send_session_command("POST", "window/maximize") 283 284 @command 285 def minimize(self): 286 return self.session.send_session_command("POST", "window/minimize") 287 288 @command 289 def fullscreen(self): 290 return self.session.send_session_command("POST", "window/fullscreen") 291 292 @classmethod 293 def from_json(cls, json, session): 294 uuid = json[Window.identifier] 295 return cls(uuid, session) 296 297 298class Frame(object): 299 identifier = "frame-075b-4da1-b6ba-e579c2d3230a" 300 301 def __init__(self, session): 302 self.session = session 303 304 @classmethod 305 def from_json(cls, json, session): 306 uuid = json[Frame.identifier] 307 return cls(uuid, session) 308 309 310class Find(object): 311 def __init__(self, session): 312 self.session = session 313 314 @command 315 def css(self, element_selector, all=True, frame="window"): 316 if (frame != "window"): 317 self.session.switch_frame(frame) 318 elements = self._find_element("css selector", element_selector, all) 319 return elements 320 321 def _find_element(self, strategy, selector, all): 322 route = "elements" if all else "element" 323 body = {"using": strategy, 324 "value": selector} 325 return self.session.send_session_command("POST", route, body) 326 327 328class Cookies(object): 329 def __init__(self, session): 330 self.session = session 331 332 def __getitem__(self, name): 333 self.session.send_session_command("GET", "cookie/%s" % name, {}) 334 335 def __setitem__(self, name, value): 336 cookie = {"name": name, 337 "value": None} 338 339 if isinstance(name, string_types): 340 cookie["value"] = value 341 elif hasattr(value, "value"): 342 cookie["value"] = value.value 343 self.session.send_session_command("POST", "cookie/%s" % name, {}) 344 345 346class UserPrompt(object): 347 def __init__(self, session): 348 self.session = session 349 350 @command 351 def dismiss(self): 352 self.session.send_session_command("POST", "alert/dismiss") 353 354 @command 355 def accept(self): 356 self.session.send_session_command("POST", "alert/accept") 357 358 @property 359 @command 360 def text(self): 361 return self.session.send_session_command("GET", "alert/text") 362 363 @text.setter 364 @command 365 def text(self, value): 366 body = {"text": value} 367 self.session.send_session_command("POST", "alert/text", body=body) 368 369 370class Session(object): 371 def __init__(self, 372 host, 373 port, 374 url_prefix="/", 375 capabilities=None, 376 extension=None): 377 self.transport = transport.HTTPWireProtocol(host, port, url_prefix) 378 self.requested_capabilities = capabilities 379 self.capabilities = None 380 self.session_id = None 381 self.timeouts = None 382 self.window = None 383 self.find = None 384 self.extension = None 385 self.extension_cls = extension 386 387 self.timeouts = Timeouts(self) 388 self.window = Window(self) 389 self.find = Find(self) 390 self.alert = UserPrompt(self) 391 self.actions = Actions(self) 392 393 def __repr__(self): 394 return "<%s %s>" % (self.__class__.__name__, self.session_id or "(disconnected)") 395 396 def __eq__(self, other): 397 return (self.session_id is not None and isinstance(other, Session) and 398 self.session_id == other.session_id) 399 400 def __enter__(self): 401 self.start() 402 return self 403 404 def __exit__(self, *args, **kwargs): 405 self.end() 406 407 def __del__(self): 408 self.end() 409 410 def start(self): 411 """Start a new WebDriver session. 412 413 :return: Dictionary with `capabilities` and `sessionId`. 414 415 :raises error.WebDriverException: If the remote end returns 416 an error. 417 """ 418 if self.session_id is not None: 419 return 420 421 body = {"capabilities": {}} 422 423 if self.requested_capabilities is not None: 424 body["capabilities"] = self.requested_capabilities 425 426 value = self.send_command("POST", "session", body=body) 427 self.session_id = value["sessionId"] 428 self.capabilities = value["capabilities"] 429 430 if self.extension_cls: 431 self.extension = self.extension_cls(self) 432 433 return value 434 435 def end(self): 436 """Try to close the active session.""" 437 if self.session_id is None: 438 return 439 440 try: 441 self.send_command("DELETE", "session/%s" % self.session_id) 442 except error.InvalidSessionIdException: 443 pass 444 finally: 445 self.session_id = None 446 447 def send_command(self, method, url, body=None, timeout=None): 448 """ 449 Send a command to the remote end and validate its success. 450 451 :param method: HTTP method to use in request. 452 :param uri: "Command part" of the HTTP request URL, 453 e.g. `window/rect`. 454 :param body: Optional body of the HTTP request. 455 456 :return: `None` if the HTTP response body was empty, otherwise 457 the `value` field returned after parsing the response 458 body as JSON. 459 460 :raises error.WebDriverException: If the remote end returns 461 an error. 462 :raises ValueError: If the response body does not contain a 463 `value` key. 464 """ 465 response = self.transport.send( 466 method, url, body, 467 encoder=protocol.Encoder, decoder=protocol.Decoder, 468 session=self, timeout=timeout) 469 470 if response.status != 200: 471 err = error.from_response(response) 472 473 if isinstance(err, error.InvalidSessionIdException): 474 # The driver could have already been deleted the session. 475 self.session_id = None 476 477 raise err 478 479 if "value" in response.body: 480 value = response.body["value"] 481 """ 482 Edge does not yet return the w3c session ID. 483 We want the tests to run in Edge anyway to help with REC. 484 In order to run the tests in Edge, we need to hack around 485 bug: 486 https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14641972 487 """ 488 if url == "session" and method == "POST" and "sessionId" in response.body and "sessionId" not in value: 489 value["sessionId"] = response.body["sessionId"] 490 else: 491 raise ValueError("Expected 'value' key in response body:\n" 492 "%s" % response) 493 494 return value 495 496 def send_session_command(self, method, uri, body=None, timeout=None): 497 """ 498 Send a command to an established session and validate its success. 499 500 :param method: HTTP method to use in request. 501 :param url: "Command part" of the HTTP request URL, 502 e.g. `window/rect`. 503 :param body: Optional body of the HTTP request. Must be JSON 504 serialisable. 505 506 :return: `None` if the HTTP response body was empty, otherwise 507 the result of parsing the body as JSON. 508 509 :raises error.WebDriverException: If the remote end returns 510 an error. 511 """ 512 url = urlparse.urljoin("session/%s/" % self.session_id, uri) 513 return self.send_command(method, url, body, timeout) 514 515 @property 516 @command 517 def url(self): 518 return self.send_session_command("GET", "url") 519 520 @url.setter 521 @command 522 def url(self, url): 523 if urlparse.urlsplit(url).netloc is None: 524 return self.url(url) 525 body = {"url": url} 526 return self.send_session_command("POST", "url", body) 527 528 @command 529 def back(self): 530 return self.send_session_command("POST", "back") 531 532 @command 533 def forward(self): 534 return self.send_session_command("POST", "forward") 535 536 @command 537 def refresh(self): 538 return self.send_session_command("POST", "refresh") 539 540 @property 541 @command 542 def title(self): 543 return self.send_session_command("GET", "title") 544 545 @property 546 @command 547 def source(self): 548 return self.send_session_command("GET", "source") 549 550 @property 551 @command 552 def window_handle(self): 553 return self.send_session_command("GET", "window") 554 555 @window_handle.setter 556 @command 557 def window_handle(self, handle): 558 body = {"handle": handle} 559 return self.send_session_command("POST", "window", body=body) 560 561 def switch_frame(self, frame): 562 if frame == "parent": 563 url = "frame/parent" 564 body = None 565 else: 566 url = "frame" 567 body = {"id": frame} 568 569 return self.send_session_command("POST", url, body) 570 571 @command 572 def close(self): 573 handles = self.send_session_command("DELETE", "window") 574 if handles is not None and len(handles) == 0: 575 # With no more open top-level browsing contexts, the session is closed. 576 self.session_id = None 577 578 return handles 579 580 @property 581 @command 582 def handles(self): 583 return self.send_session_command("GET", "window/handles") 584 585 @property 586 @command 587 def active_element(self): 588 return self.send_session_command("GET", "element/active") 589 590 @command 591 def cookies(self, name=None): 592 if name is None: 593 url = "cookie" 594 else: 595 url = "cookie/%s" % name 596 return self.send_session_command("GET", url, {}) 597 598 @command 599 def set_cookie(self, name, value, path=None, domain=None, 600 secure=None, expiry=None, http_only=None): 601 body = { 602 "name": name, 603 "value": value, 604 } 605 606 if domain is not None: 607 body["domain"] = domain 608 if expiry is not None: 609 body["expiry"] = expiry 610 if http_only is not None: 611 body["httpOnly"] = http_only 612 if path is not None: 613 body["path"] = path 614 if secure is not None: 615 body["secure"] = secure 616 self.send_session_command("POST", "cookie", {"cookie": body}) 617 618 def delete_cookie(self, name=None): 619 if name is None: 620 url = "cookie" 621 else: 622 url = "cookie/%s" % name 623 self.send_session_command("DELETE", url, {}) 624 625 #[...] 626 627 @command 628 def execute_script(self, script, args=None): 629 if args is None: 630 args = [] 631 632 body = { 633 "script": script, 634 "args": args 635 } 636 return self.send_session_command("POST", "execute/sync", body) 637 638 @command 639 def execute_async_script(self, script, args=None): 640 if args is None: 641 args = [] 642 643 body = { 644 "script": script, 645 "args": args 646 } 647 return self.send_session_command("POST", "execute/async", body) 648 649 #[...] 650 651 @command 652 def screenshot(self): 653 return self.send_session_command("GET", "screenshot") 654 655 656class Element(object): 657 """ 658 Representation of a web element. 659 660 A web element is an abstraction used to identify an element when 661 it is transported via the protocol, between remote- and local ends. 662 """ 663 identifier = "element-6066-11e4-a52e-4f735466cecf" 664 665 def __init__(self, id, session): 666 """ 667 Construct a new web element representation. 668 669 :param id: Web element UUID which must be unique across 670 all browsing contexts. 671 :param session: Current ``webdriver.Session``. 672 """ 673 self.id = id 674 self.session = session 675 676 def __repr__(self): 677 return "<%s %s>" % (self.__class__.__name__, self.id) 678 679 def __eq__(self, other): 680 return (isinstance(other, Element) and self.id == other.id and 681 self.session == other.session) 682 683 @classmethod 684 def from_json(cls, json, session): 685 uuid = json[Element.identifier] 686 return cls(uuid, session) 687 688 def send_element_command(self, method, uri, body=None): 689 url = "element/%s/%s" % (self.id, uri) 690 return self.session.send_session_command(method, url, body) 691 692 @command 693 def find_element(self, strategy, selector): 694 body = {"using": strategy, 695 "value": selector} 696 return self.send_element_command("POST", "element", body) 697 698 @command 699 def click(self): 700 self.send_element_command("POST", "click", {}) 701 702 @command 703 def tap(self): 704 self.send_element_command("POST", "tap", {}) 705 706 @command 707 def clear(self): 708 self.send_element_command("POST", "clear", {}) 709 710 @command 711 def send_keys(self, text): 712 return self.send_element_command("POST", "value", {"text": text}) 713 714 @property 715 @command 716 def text(self): 717 return self.send_element_command("GET", "text") 718 719 @property 720 @command 721 def name(self): 722 return self.send_element_command("GET", "name") 723 724 @command 725 def style(self, property_name): 726 return self.send_element_command("GET", "css/%s" % property_name) 727 728 @property 729 @command 730 def rect(self): 731 return self.send_element_command("GET", "rect") 732 733 @property 734 @command 735 def selected(self): 736 return self.send_element_command("GET", "selected") 737 738 @command 739 def screenshot(self): 740 return self.send_element_command("GET", "screenshot") 741 742 @command 743 def attribute(self, name): 744 return self.send_element_command("GET", "attribute/%s" % name) 745 746 # This MUST come last because otherwise @property decorators above 747 # will be overridden by this. 748 @command 749 def property(self, name): 750 return self.send_element_command("GET", "property/%s" % name) 751