Skip to content

Commit

Permalink
Merge pull request #5 from HDE/phantomjs
Browse files Browse the repository at this point in the history
Added a lot of new APIs
  • Loading branch information
ojii committed May 25, 2017
2 parents c8ef268 + aaca642 commit 1725b8a
Show file tree
Hide file tree
Showing 13 changed files with 757 additions and 201 deletions.
7 changes: 7 additions & 0 deletions docs/contrib/extend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ the :py:meth:`arsenic.service.Service.start` coroutine method. This method
takes an instance of an :py:class:`arsenic.engines.Engine` as argument and must
return an instance of :py:class:`arsenic.webdriver.WebDriver`.

If your service uses a local subprocess, the :py:func:`arsenic.service.subprocess_based_service`
helper might be useful.

Browser
*******

Expand All @@ -106,3 +109,7 @@ For convenience there is a :py:class:`arsenic.browsers.Browser` class you can
subclass and set the :py:attr:`arsenic.browsers.Browser.defaults` to a JSON
serializable dictionary of default values. The class can be passed a dictionary
of values to override when instantiated.

If your :ref:`Browser` does not support the web driver protocol, you can chose a
different :py:class:`arsenic.session.Session` class using the
:py:attr:`arsenic.browsers.Browser.session_class` attribute.
2 changes: 0 additions & 2 deletions docs/contrib/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,4 @@ Manual
3. Run ``WEB_APP_BASE_URL=http://localhost:5000 pytest`` in the root directory.




.. _circleci local: https://circleci.com/docs/2.0/local-jobs/
7 changes: 7 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ Welcome to arsenic's documentation!
###################################


.. warning::

While this library is asynchronous, web drivers are **not**. You must call
the APIs in sequence. The purpose of this library is to allow you to control
multiple web drivers asynchronously or to use a web driver in the same thread
as an asynchronous web server.

.. toctree::
:maxdepth: 2
:caption: User Documentation
Expand Down
26 changes: 26 additions & 0 deletions docs/user/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,29 @@ Create a file ``test_app.py`` and write the following code:
Now if you run ``pytest test_app.py`` arsenic will spawn a Firefox instance and
use it to check that your website is behaving correctly.

Waiting
*******

Quite often you will need to wait for the browser context to be in a certain state.
To do so, you can use :py:meth:`arsenic.session.Session.wait` which is a low
level API to wait for certain conditions. It takes two or more arguments: A timeout
as an integer or float of seconds to wait, a coroutine callback to check if the
condition is met and an optionally exception classes which should be ignored.

The callback should return a truthy value to indicate the condition is met.

For extra convenience, there are built-in APIs to wait for an element to appear
and an element to go away, :py:meth:`arsenic.Session.wait_for_element` and
:py:meth:`arsenic.Session.wait_for_element_gone` respectively. Both take a
timeout as an integer or float of seconds as first argument and a CSS selector
as second argument. :py:meth:`arsenic.Session.wait_for_element` returns the
element after it was found.

An example to use the generic wait API to wait up to 5 seconds for an alert to
show up would be::

alert_text = await session.wait(
5,
session.get_alert_text,
NoSuchAlert
)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
package_dir={'': 'src'},
packages=find_packages(where='src'),
install_requires=[
'attrs',
'attrs>=17.1.0',
'structlog',
],
license='APLv2',
Expand Down
132 changes: 132 additions & 0 deletions src/arsenic/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import abc
from enum import Enum
from typing import List, Dict, Any, Union

import attr

from arsenic.connection import WEB_ELEMENT
from arsenic.session import Element


TOrigin = Union[Element, 'Origins']


class Origins(Enum):
viewport = 'viewport'
pointer = 'pointer'


class Button(Enum):
left = 0
middle = 1
right = 2


class Device(metaclass=abc.ABCMeta):
id: str = abc.abstractproperty()
type: str = abc.abstractproperty()
parameters: Dict[str, str] = abc.abstractproperty()

def __init__(self):
self.actions: List[Action] = []

@abc.abstractmethod
def encode_actions(self) -> List[Dict[str, Any]]:
raise NotImplementedError()


@attr.s
class Action:
type = attr.ib()
duration = attr.ib()
data = attr.ib()

def encode(self):
return {
'type': self.type,
'duration': self.duration,
**self.data
}


def encode_origin(origin):
if isinstance(origin, Element):
return {WEB_ELEMENT: origin.id}
elif isinstance(origin, Origins):
return origin.value
else:
raise TypeError()


class Mouse(Device):
id = 'mouse'
type = 'pointer'
parameters = {
'pointerType': 'mouse'
}

def move(self, origin: TOrigin, x: int, y: int, duration: int=250):
self.actions.append(Action(
type='pointerMove',
duration=duration,
data={
'origin': encode_origin(origin),
'x': x,
'y': y
}
))

def button_down(self, button: Button=Button.left):
self.actions.append(Action(
type='pointerDown',
duration=0,
data={
'button': button.value
}
))

def button_up(self, button: Button=Button.left):
self.actions.append(Action(
type='pointerUp',
duration=0,
data={
'button': button.value
}
))

def encode_actions(self):
return [action.encode() for action in self.actions]


class Actions:
def __init__(self):
self.mouse = Mouse()
self.devices: List[Device] = [self.mouse]

def move_to(self, element: Element) -> 'Actions':
self.mouse.move(element, 0, 0)
return self

def move_by(self, x: int, y: int) -> 'Actions':
self.mouse.move(Origins.pointer, x, y)
return self

def mouse_down(self) -> 'Actions':
self.mouse.button_down()
return self

def mouse_up(self) -> 'Actions':
self.mouse.button_up()
return self

def encode(self):
return {
'actions': [
{
'id': device.id,
'type': device.type,
'parameters': device.parameters,
'actions': device.encode_actions(),
} for device in self.devices
]
}
29 changes: 25 additions & 4 deletions src/arsenic/browsers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from arsenic.session import Session, CompatSession


class Browser:
defaults = {}
session_class = Session

def __init__(self, overrides=None):
self.capabilities = {**self.defaults}
if overrides is not None:
self.capabilities.update(overrides)
def __init__(self, **overrides):
self.capabilities = {**self.defaults, **overrides}


class Firefox(Browser):
Expand All @@ -13,3 +15,22 @@ class Firefox(Browser):
'marionette': True,
'acceptInsecureCerts': True,
}


class PhantomJS(Browser):
session_class = CompatSession
defaults = {
'browserName': 'phantomjs',
'version': '',
'platform': 'ANY',
'javascriptEnabled': True,
}


class InternetExplorer(Browser):
session_class = CompatSession
defaults = {
'browserName': 'internet explorer',
'version': '',
'platform': 'WINDOWS',
}
22 changes: 22 additions & 0 deletions src/arsenic/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
from io import BytesIO
from json import JSONDecodeError
from pathlib import Path
from zipfile import ZipFile, ZIP_DEFLATED

from structlog import get_logger

Expand Down Expand Up @@ -54,6 +56,7 @@ async def request(self, *, url: str, method: str, data=None, raw=False):
try:
data = json.loads(response.body)
except JSONDecodeError as exc:
log.error('json-decode', body=response.body)
data = {
'error': '!internal',
'message': str(exc),
Expand All @@ -67,5 +70,24 @@ async def request(self, *, url: str, method: str, data=None, raw=False):
if data:
return unwrap(data.get('value', None))

async def upload_file(self, path: Path) -> Path:
return path

def prefixed(self, prefix: str) -> 'Connection':
return Connection(self.session, self.prefix + prefix)


class RemoteConnection(Connection):
async def upload_file(self, path: Path) -> Path:
log.info('upload-file', path=path)
fobj = BytesIO()
with ZipFile(fobj, 'w', ZIP_DEFLATED) as zf:
zf.write(path, path.name)
content = base64.b64encode(fobj.getvalue()).decode('utf-8')
return await self.request(
url='/file',
method='POST',
data={
'file': content
}
)
8 changes: 7 additions & 1 deletion src/arsenic/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class ArsenicError(Exception):
pass


class OperationNotSupported(ArsenicError):
pass


class WebdriverError(ArsenicError):
def __init__(self, message, screen, stacktrace):
self.message = message
Expand Down Expand Up @@ -70,7 +74,9 @@ def _value_or_default(obj, key, default):
def check_response(status, data):
if status >= 400:
error = None
if 'error' in data:
if 'status' in data:
error = data['status']
elif 'error' in data:
error = data['error']
elif 'state' in data:
error = data['state']
Expand Down
59 changes: 59 additions & 0 deletions src/arsenic/keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
NULL = '\ue000'
CANCEL = '\ue001'
HELP = '\ue002'
BACKSPACE = '\ue003'
TAB = '\ue004'
CLEAR = '\ue005'
RETURN = '\ue006'
ENTER = '\ue007'
SHIFT = '\ue008'
CONTROL = '\ue009'
ALT = '\ue00a'
PAUSE = '\ue00b'
ESCAPE = '\ue00c'
SPACE = '\ue00d'
PAGE_UP = '\ue00e'
PAGE_DOWN = '\ue00f'
END = '\ue010'
HOME = '\ue011'
LEFT = '\ue012'
UP = '\ue013'
RIGHT = '\ue014'
DOWN = '\ue015'
INSERT = '\ue016'
DELETE = '\ue017'
SEMICOLON = '\ue018'
EQUALS = '\ue019'

NUMPAD0 = '\ue01a'
NUMPAD1 = '\ue01b'
NUMPAD2 = '\ue01c'
NUMPAD3 = '\ue01d'
NUMPAD4 = '\ue01e'
NUMPAD5 = '\ue01f'
NUMPAD6 = '\ue020'
NUMPAD7 = '\ue021'
NUMPAD8 = '\ue022'
NUMPAD9 = '\ue023'
MULTIPLY = '\ue024'
ADD = '\ue025'
SEPARATOR = '\ue026'
SUBTRACT = '\ue027'
DECIMAL = '\ue028'
DIVIDE = '\ue029'

F1 = '\ue031'
F2 = '\ue032'
F3 = '\ue033'
F4 = '\ue034'
F5 = '\ue035'
F6 = '\ue036'
F7 = '\ue037'
F8 = '\ue038'
F9 = '\ue039'
F10 = '\ue03a'
F11 = '\ue03b'
F12 = '\ue03c'

META = '\ue03d'
COMMAND = '\ue03d'

0 comments on commit 1725b8a

Please sign in to comment.