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