AX: AXIsolatedTree::updateChildren sometimes fails to update isolated subtrees when...
[WebKit-https.git] / WebDriverTests / imported / w3c / tools / webdriver / webdriver / client.py
1 from . import error
2 from . import protocol
3 from . import transport
4 from .bidi.client import BidiSession
5
6 from urllib import parse as urlparse
7
8
9 def 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
27 class 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
73 class 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=None, x=None, y=None, duration=None, origin=None, width=None,
120                         height=None, pressure=None, tangential_pressure=None, tilt_x=None,
121                         tilt_y=None, twist=None, altitude_angle=None, azimuth_angle=None):
122         action = {
123             "type": subtype
124         }
125         if button is not None:
126             action["button"] = button
127         if x is not None:
128             action["x"] = x
129         if y is not None:
130             action["y"] = y
131         if duration is not None:
132             action["duration"] = duration
133         if origin is not None:
134             action["origin"] = origin
135         if width is not None:
136             action["width"] = width
137         if height is not None:
138             action["height"] = height
139         if pressure is not None:
140             action["pressure"] = pressure
141         if tangential_pressure is not None:
142             action["tangentialPressure"] = tangential_pressure
143         if tilt_x is not None:
144             action["tiltX"] = tilt_x
145         if tilt_y is not None:
146             action["tiltY"] = tilt_y
147         if twist is not None:
148             action["twist"] = twist
149         if altitude_angle is not None:
150             action["altitudeAngle"] = altitude_angle
151         if azimuth_angle is not None:
152             action["azimuthAngle"] = azimuth_angle
153         self._actions.append(action)
154
155     def pause(self, duration):
156         self._actions.append({"type": "pause", "duration": duration})
157         return self
158
159     def pointer_move(self, x, y, duration=None, origin=None, width=None, height=None,
160                      pressure=None, tangential_pressure=None, tilt_x=None, tilt_y=None,
161                      twist=None, altitude_angle=None, azimuth_angle=None):
162         """Queue a pointerMove action.
163
164         :param x: Destination x-axis coordinate of pointer in CSS pixels.
165         :param y: Destination y-axis coordinate of pointer in CSS pixels.
166         :param duration: Number of milliseconds over which to distribute the
167                          move. If None, remote end defaults to 0.
168         :param origin: Origin of coordinates, either "viewport", "pointer" or
169                        an Element. If None, remote end defaults to "viewport".
170         """
171         self._pointer_action("pointerMove", x=x, y=y, duration=duration, origin=origin,
172                              width=width, height=height, pressure=pressure,
173                              tangential_pressure=tangential_pressure, tilt_x=tilt_x, tilt_y=tilt_y,
174                              twist=twist, altitude_angle=altitude_angle, azimuth_angle=azimuth_angle)
175         return self
176
177     def pointer_up(self, button=0):
178         """Queue a pointerUp action for `button`.
179
180         :param button: Pointer button to perform action with.
181                        Default: 0, which represents main device button.
182         """
183         self._pointer_action("pointerUp", button=button)
184         return self
185
186     def pointer_down(self, button=0, width=None, height=None, pressure=None,
187                      tangential_pressure=None, tilt_x=None, tilt_y=None,
188                      twist=None, altitude_angle=None, azimuth_angle=None):
189         """Queue a pointerDown action for `button`.
190
191         :param button: Pointer button to perform action with.
192                        Default: 0, which represents main device button.
193         """
194         self._pointer_action("pointerDown", button=button, width=width, height=height,
195                              pressure=pressure, tangential_pressure=tangential_pressure,
196                              tilt_x=tilt_x, tilt_y=tilt_y, twist=twist, altitude_angle=altitude_angle,
197                              azimuth_angle=azimuth_angle)
198         return self
199
200     def click(self, element=None, button=0):
201         """Queue a click with the specified button.
202
203         If an element is given, move the pointer to that element first,
204         otherwise click current pointer coordinates.
205
206         :param element: Optional element to click.
207         :param button: Integer representing pointer button to perform action
208                        with. Default: 0, which represents main device button.
209         """
210         if element:
211             self.pointer_move(0, 0, origin=element)
212         return self.pointer_down(button).pointer_up(button)
213
214     def key_up(self, value):
215         """Queue a keyUp action for `value`.
216
217         :param value: Character to perform key action with.
218         """
219         self._key_action("keyUp", value)
220         return self
221
222     def key_down(self, value):
223         """Queue a keyDown action for `value`.
224
225         :param value: Character to perform key action with.
226         """
227         self._key_action("keyDown", value)
228         return self
229
230     def send_keys(self, keys):
231         """Queue a keyDown and keyUp action for each character in `keys`.
232
233         :param keys: String of keys to perform key actions with.
234         """
235         for c in keys:
236             self.key_down(c)
237             self.key_up(c)
238         return self
239
240     def scroll(self, x, y, delta_x, delta_y, duration=None, origin=None):
241         """Queue a scroll action.
242
243         :param x: Destination x-axis coordinate of pointer in CSS pixels.
244         :param y: Destination y-axis coordinate of pointer in CSS pixels.
245         :param delta_x: scroll delta on x-axis in CSS pixels.
246         :param delta_y: scroll delta on y-axis in CSS pixels.
247         :param duration: Number of milliseconds over which to distribute the
248                          scroll. If None, remote end defaults to 0.
249         :param origin: Origin of coordinates, either "viewport" or an Element.
250                        If None, remote end defaults to "viewport".
251         """
252         action = {
253             "type": "scroll",
254             "x": x,
255             "y": y,
256             "deltaX": delta_x,
257             "deltaY": delta_y
258         }
259         if duration is not None:
260             action["duration"] = duration
261         if origin is not None:
262             action["origin"] = origin
263         self._actions.append(action)
264         return self
265
266
267 class Actions(object):
268     def __init__(self, session):
269         self.session = session
270
271     @command
272     def perform(self, actions=None):
273         """Performs actions by tick from each action sequence in `actions`.
274
275         :param actions: List of input source action sequences. A single action
276                         sequence may be created with the help of
277                         ``ActionSequence.dict``.
278         """
279         body = {"actions": [] if actions is None else actions}
280         actions = self.session.send_session_command("POST", "actions", body)
281         return actions
282
283     @command
284     def release(self):
285         return self.session.send_session_command("DELETE", "actions")
286
287     def sequence(self, *args, **kwargs):
288         """Return an empty ActionSequence of the designated type.
289
290         See ActionSequence for parameter list.
291         """
292         return ActionSequence(self.session, *args, **kwargs)
293
294
295 class Window(object):
296     identifier = "window-fcc6-11e5-b4f8-330a88ab9d7f"
297
298     def __init__(self, session):
299         self.session = session
300
301     @command
302     def close(self):
303         handles = self.session.send_session_command("DELETE", "window")
304         if handles is not None and len(handles) == 0:
305             # With no more open top-level browsing contexts, the session is closed.
306             self.session.session_id = None
307
308         return handles
309
310     # The many "type: ignore" comments here and below are to silence mypy's
311     # "Decorated property not supported" error, which is due to a limitation
312     # in mypy, see https://github.com/python/mypy/issues/1362.
313     @property  # type: ignore
314     @command
315     def rect(self):
316         return self.session.send_session_command("GET", "window/rect")
317
318     @property  # type: ignore
319     @command
320     def size(self):
321         """Gets the window size as a tuple of `(width, height)`."""
322         rect = self.rect
323         return (rect["width"], rect["height"])
324
325     @size.setter  # type: ignore
326     @command
327     def size(self, new_size):
328         """Set window size by passing a tuple of `(width, height)`."""
329         width, height = new_size
330         body = {"width": width, "height": height}
331         self.session.send_session_command("POST", "window/rect", body)
332
333     @property  # type: ignore
334     @command
335     def position(self):
336         """Gets the window position as a tuple of `(x, y)`."""
337         rect = self.rect
338         return (rect["x"], rect["y"])
339
340     @position.setter  # type: ignore
341     @command
342     def position(self, new_position):
343         """Set window position by passing a tuple of `(x, y)`."""
344         x, y = new_position
345         body = {"x": x, "y": y}
346         self.session.send_session_command("POST", "window/rect", body)
347
348     @command
349     def maximize(self):
350         return self.session.send_session_command("POST", "window/maximize")
351
352     @command
353     def minimize(self):
354         return self.session.send_session_command("POST", "window/minimize")
355
356     @command
357     def fullscreen(self):
358         return self.session.send_session_command("POST", "window/fullscreen")
359
360     @classmethod
361     def from_json(cls, json, session):
362         uuid = json[Window.identifier]
363         return cls(uuid, session)
364
365
366 class Frame(object):
367     identifier = "frame-075b-4da1-b6ba-e579c2d3230a"
368
369     def __init__(self, session):
370         self.session = session
371
372     @classmethod
373     def from_json(cls, json, session):
374         uuid = json[Frame.identifier]
375         return cls(uuid, session)
376
377
378 class ShadowRoot(object):
379     identifier = "shadow-6066-11e4-a52e-4f735466cecf"
380
381     def __init__(self, session, id):
382         """
383         Construct a new shadow root representation.
384
385         :param id: Shadow root UUID which must be unique across
386             all browsing contexts.
387         :param session: Current ``webdriver.Session``.
388         """
389         self.id = id
390         self.session = session
391
392     @classmethod
393     def from_json(cls, json, session):
394         uuid = json[ShadowRoot.identifier]
395         return cls(session, uuid)
396
397     def send_shadow_command(self, method, uri, body=None):
398         url = "shadow/{}/{}".format(self.id, uri)
399         return self.session.send_session_command(method, url, body)
400
401     @command
402     def find_element(self, strategy, selector):
403         body = {"using": strategy,
404                 "value": selector}
405         return self.send_shadow_command("POST", "element", body)
406
407     @command
408     def find_elements(self, strategy, selector):
409         body = {"using": strategy,
410                 "value": selector}
411         return self.send_shadow_command("POST", "elements", body)
412
413
414 class Find(object):
415     def __init__(self, session):
416         self.session = session
417
418     @command
419     def css(self, element_selector, all=True):
420         elements = self._find_element("css selector", element_selector, all)
421         return elements
422
423     def _find_element(self, strategy, selector, all):
424         route = "elements" if all else "element"
425         body = {"using": strategy,
426                 "value": selector}
427         return self.session.send_session_command("POST", route, body)
428
429
430 class Cookies(object):
431     def __init__(self, session):
432         self.session = session
433
434     def __getitem__(self, name):
435         self.session.send_session_command("GET", "cookie/%s" % name, {})
436
437     def __setitem__(self, name, value):
438         cookie = {"name": name,
439                   "value": None}
440
441         if isinstance(name, str):
442             cookie["value"] = value
443         elif hasattr(value, "value"):
444             cookie["value"] = value.value
445         self.session.send_session_command("POST", "cookie/%s" % name, {})
446
447
448 class UserPrompt(object):
449     def __init__(self, session):
450         self.session = session
451
452     @command
453     def dismiss(self):
454         self.session.send_session_command("POST", "alert/dismiss")
455
456     @command
457     def accept(self):
458         self.session.send_session_command("POST", "alert/accept")
459
460     @property  # type: ignore
461     @command
462     def text(self):
463         return self.session.send_session_command("GET", "alert/text")
464
465     @text.setter  # type: ignore
466     @command
467     def text(self, value):
468         body = {"text": value}
469         self.session.send_session_command("POST", "alert/text", body=body)
470
471
472 class Session(object):
473     def __init__(self,
474                  host,
475                  port,
476                  url_prefix="/",
477                  enable_bidi=False,
478                  capabilities=None,
479                  extension=None):
480
481         if enable_bidi:
482             if capabilities is not None:
483                 capabilities.setdefault("alwaysMatch", {}).update({"webSocketUrl": True})
484             else:
485                 capabilities = {"alwaysMatch": {"webSocketUrl": True}}
486
487         self.transport = transport.HTTPWireProtocol(host, port, url_prefix)
488         self.requested_capabilities = capabilities
489         self.capabilities = None
490         self.session_id = None
491         self.timeouts = None
492         self.window = None
493         self.find = None
494         self.enable_bidi = enable_bidi
495         self.bidi_session = None
496         self.extension = None
497         self.extension_cls = extension
498
499         self.timeouts = Timeouts(self)
500         self.window = Window(self)
501         self.find = Find(self)
502         self.alert = UserPrompt(self)
503         self.actions = Actions(self)
504
505     def __repr__(self):
506         return "<%s %s>" % (self.__class__.__name__, self.session_id or "(disconnected)")
507
508     def __eq__(self, other):
509         return (self.session_id is not None and isinstance(other, Session) and
510                 self.session_id == other.session_id)
511
512     def __enter__(self):
513         self.start()
514         return self
515
516     def __exit__(self, *args, **kwargs):
517         self.end()
518
519     def __del__(self):
520         self.end()
521
522     def match(self, capabilities):
523         return self.requested_capabilities == capabilities
524
525     def start(self):
526         """Start a new WebDriver session.
527
528         :return: Dictionary with `capabilities` and `sessionId`.
529
530         :raises error.WebDriverException: If the remote end returns
531             an error.
532         """
533         if self.session_id is not None:
534             return
535
536         self.transport.close()
537
538         body = {"capabilities": {}}
539
540         if self.requested_capabilities is not None:
541             body["capabilities"] = self.requested_capabilities
542
543         value = self.send_command("POST", "session", body=body)
544         self.session_id = value["sessionId"]
545         self.capabilities = value["capabilities"]
546
547         if "webSocketUrl" in self.capabilities:
548             self.bidi_session = BidiSession.from_http(self.session_id,
549                                                       self.capabilities)
550         elif self.enable_bidi:
551             self.end()
552             raise error.SessionNotCreatedException(
553                 "Requested bidi session, but webSocketUrl capability not found")
554
555         if self.extension_cls:
556             self.extension = self.extension_cls(self)
557
558         return value
559
560     def end(self):
561         """Try to close the active session."""
562         if self.session_id is None:
563             return
564
565         try:
566             self.send_command("DELETE", "session/%s" % self.session_id)
567         except (OSError, error.InvalidSessionIdException):
568             pass
569         finally:
570             self.session_id = None
571             self.transport.close()
572
573     def send_command(self, method, url, body=None, timeout=None):
574         """
575         Send a command to the remote end and validate its success.
576
577         :param method: HTTP method to use in request.
578         :param uri: "Command part" of the HTTP request URL,
579             e.g. `window/rect`.
580         :param body: Optional body of the HTTP request.
581
582         :return: `None` if the HTTP response body was empty, otherwise
583             the `value` field returned after parsing the response
584             body as JSON.
585
586         :raises error.WebDriverException: If the remote end returns
587             an error.
588         :raises ValueError: If the response body does not contain a
589             `value` key.
590         """
591
592         response = self.transport.send(
593             method, url, body,
594             encoder=protocol.Encoder, decoder=protocol.Decoder,
595             session=self, timeout=timeout)
596
597         if response.status != 200:
598             err = error.from_response(response)
599
600             if isinstance(err, error.InvalidSessionIdException):
601                 # The driver could have already been deleted the session.
602                 self.session_id = None
603
604             raise err
605
606         if "value" in response.body:
607             value = response.body["value"]
608             """
609             Edge does not yet return the w3c session ID.
610             We want the tests to run in Edge anyway to help with REC.
611             In order to run the tests in Edge, we need to hack around
612             bug:
613             https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14641972
614             """
615             if url == "session" and method == "POST" and "sessionId" in response.body and "sessionId" not in value:
616                 value["sessionId"] = response.body["sessionId"]
617         else:
618             raise ValueError("Expected 'value' key in response body:\n"
619                 "%s" % response)
620
621         return value
622
623     def send_session_command(self, method, uri, body=None, timeout=None):
624         """
625         Send a command to an established session and validate its success.
626
627         :param method: HTTP method to use in request.
628         :param url: "Command part" of the HTTP request URL,
629             e.g. `window/rect`.
630         :param body: Optional body of the HTTP request.  Must be JSON
631             serialisable.
632
633         :return: `None` if the HTTP response body was empty, otherwise
634             the result of parsing the body as JSON.
635
636         :raises error.WebDriverException: If the remote end returns
637             an error.
638         """
639         url = urlparse.urljoin("session/%s/" % self.session_id, uri)
640         return self.send_command(method, url, body, timeout)
641
642     @property  # type: ignore
643     @command
644     def url(self):
645         return self.send_session_command("GET", "url")
646
647     @url.setter  # type: ignore
648     @command
649     def url(self, url):
650         if urlparse.urlsplit(url).netloc is None:
651             return self.url(url)
652         body = {"url": url}
653         return self.send_session_command("POST", "url", body)
654
655     @command
656     def back(self):
657         return self.send_session_command("POST", "back")
658
659     @command
660     def forward(self):
661         return self.send_session_command("POST", "forward")
662
663     @command
664     def refresh(self):
665         return self.send_session_command("POST", "refresh")
666
667     @property  # type: ignore
668     @command
669     def title(self):
670         return self.send_session_command("GET", "title")
671
672     @property  # type: ignore
673     @command
674     def source(self):
675         return self.send_session_command("GET", "source")
676
677     @command
678     def new_window(self, type_hint="tab"):
679         body = {"type": type_hint}
680         value = self.send_session_command("POST", "window/new", body)
681
682         return value["handle"]
683
684     @property  # type: ignore
685     @command
686     def window_handle(self):
687         return self.send_session_command("GET", "window")
688
689     @window_handle.setter  # type: ignore
690     @command
691     def window_handle(self, handle):
692         body = {"handle": handle}
693         return self.send_session_command("POST", "window", body=body)
694
695     def switch_frame(self, frame):
696         if frame == "parent":
697             url = "frame/parent"
698             body = None
699         else:
700             url = "frame"
701             body = {"id": frame}
702
703         return self.send_session_command("POST", url, body)
704
705     @property  # type: ignore
706     @command
707     def handles(self):
708         return self.send_session_command("GET", "window/handles")
709
710     @property  # type: ignore
711     @command
712     def active_element(self):
713         return self.send_session_command("GET", "element/active")
714
715     @command
716     def cookies(self, name=None):
717         if name is None:
718             url = "cookie"
719         else:
720             url = "cookie/%s" % name
721         return self.send_session_command("GET", url, {})
722
723     @command
724     def set_cookie(self, name, value, path=None, domain=None,
725             secure=None, expiry=None, http_only=None):
726         body = {
727             "name": name,
728             "value": value,
729         }
730
731         if domain is not None:
732             body["domain"] = domain
733         if expiry is not None:
734             body["expiry"] = expiry
735         if http_only is not None:
736             body["httpOnly"] = http_only
737         if path is not None:
738             body["path"] = path
739         if secure is not None:
740             body["secure"] = secure
741         self.send_session_command("POST", "cookie", {"cookie": body})
742
743     def delete_cookie(self, name=None):
744         if name is None:
745             url = "cookie"
746         else:
747             url = "cookie/%s" % name
748         self.send_session_command("DELETE", url, {})
749
750     #[...]
751
752     @command
753     def execute_script(self, script, args=None):
754         if args is None:
755             args = []
756
757         body = {
758             "script": script,
759             "args": args
760         }
761         return self.send_session_command("POST", "execute/sync", body)
762
763     @command
764     def execute_async_script(self, script, args=None):
765         if args is None:
766             args = []
767
768         body = {
769             "script": script,
770             "args": args
771         }
772         return self.send_session_command("POST", "execute/async", body)
773
774     #[...]
775
776     @command
777     def screenshot(self):
778         return self.send_session_command("GET", "screenshot")
779
780 class Element(object):
781     """
782     Representation of a web element.
783
784     A web element is an abstraction used to identify an element when
785     it is transported via the protocol, between remote- and local ends.
786     """
787     identifier = "element-6066-11e4-a52e-4f735466cecf"
788
789     def __init__(self, id, session):
790         """
791         Construct a new web element representation.
792
793         :param id: Web element UUID which must be unique across
794             all browsing contexts.
795         :param session: Current ``webdriver.Session``.
796         """
797         self.id = id
798         self.session = session
799
800     def __repr__(self):
801         return "<%s %s>" % (self.__class__.__name__, self.id)
802
803     def __eq__(self, other):
804         return (isinstance(other, Element) and self.id == other.id and
805                 self.session == other.session)
806
807     @classmethod
808     def from_json(cls, json, session):
809         uuid = json[Element.identifier]
810         return cls(uuid, session)
811
812     def send_element_command(self, method, uri, body=None):
813         url = "element/%s/%s" % (self.id, uri)
814         return self.session.send_session_command(method, url, body)
815
816     @command
817     def find_element(self, strategy, selector):
818         body = {"using": strategy,
819                 "value": selector}
820         return self.send_element_command("POST", "element", body)
821
822     @command
823     def click(self):
824         self.send_element_command("POST", "click", {})
825
826     @command
827     def tap(self):
828         self.send_element_command("POST", "tap", {})
829
830     @command
831     def clear(self):
832         self.send_element_command("POST", "clear", {})
833
834     @command
835     def send_keys(self, text):
836         return self.send_element_command("POST", "value", {"text": text})
837
838     @property  # type: ignore
839     @command
840     def text(self):
841         return self.send_element_command("GET", "text")
842
843     @property  # type: ignore
844     @command
845     def name(self):
846         return self.send_element_command("GET", "name")
847
848     @command
849     def style(self, property_name):
850         return self.send_element_command("GET", "css/%s" % property_name)
851
852     @property  # type: ignore
853     @command
854     def rect(self):
855         return self.send_element_command("GET", "rect")
856
857     @property  # type: ignore
858     @command
859     def selected(self):
860         return self.send_element_command("GET", "selected")
861
862     @command
863     def screenshot(self):
864         return self.send_element_command("GET", "screenshot")
865
866     @property  # type: ignore
867     @command
868     def shadow_root(self):
869         return self.send_element_command("GET", "shadow")
870
871     @command
872     def attribute(self, name):
873         return self.send_element_command("GET", "attribute/%s" % name)
874
875     # This MUST come last because otherwise @property decorators above
876     # will be overridden by this.
877     @command
878     def property(self, name):
879         return self.send_element_command("GET", "property/%s" % name)