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