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