Skip to content

Commit

Permalink
Merge pull request #8 from kieras/develop
Browse files Browse the repository at this point in the history
New version with improvements and documentation
  • Loading branch information
kieras authored Apr 6, 2018
2 parents dec654a + dd9edf2 commit 4828254
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 14 deletions.
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,151 @@
# Parakeet

![Parakeet](parakeet-logo-small.png?raw=true "Parakeet")

UI tests automation utilities library.

The aim is to easy the development of tests for applications that follow a
known set of technologies:

- Angular JS
- Angular JS Material
- Google OAuth2
- Chrome Browser

This library depends, and sometime implements useful abstractions, on the following libraries:

- [Lettuce](http://lettuce.it/)
- [Splinter](https://splinter.readthedocs.io/)
- [Selenium](https://www.seleniumhq.org/projects/webdriver/)
- [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/)

We designed it overcome some challenges of developing BDD tests considering the technologies below:

![Parakeet stack](parakeet-stack.png?raw=true "Parakeet Stack")

## Installation

You can install Parakeet using the package manager PIP.

```
pip install parakeet -U
```

## Configuration

Parakeet reads a **config.yaml** file that can be used to parameterize it's behaviours.

It need to be saved on the home directory of the operation system running the tests.

<table>
<tr style="background: grey;font-weight: bold;">
<td>Parameter</td>
<td>Description</td>
<td>Values</td>
<td>Default</td>
</tr>
<tr>
<td>browser</td>
<td>This parameter define which browser will be used to run the tests. Today we only support the Chrome Browser</td>
<td>chrome</td>
<td>chrome</td>
</tr>
<tr>
<td>headless</td>
<td>Define if the tests will open the browser or perform the tests with graphical mode off. It is very useful in developer mode, when the developer need to debug or understand some issues. Case True, doesn't open the browser.</td>
<td>True<br>False</td>
<td>False</td>
</tr>
<tr>
<td>default_implicitly_wait_seconds</td>
<td>Setup time in seconds that the tests will wait until some component are ready to be used.</td>
<td>Integer Number</td>
<td>30</td>
</tr>
<tr>
<td>default_poll_frequency_seconds</td>
<td>Interval time during the default_implicitly_wait_seconds, that the application will wait until check if the component are ready to be used.</td>
<td>Integer Number</td>
<td>2</td>
</tr>
<tr>
<td>log_level</td>
<td>You can define the level of the application logs.</td>
<td>INFO<br>WARN<br>DEBUG<br>TRACE<br>ERROR</td>
<td>INFO</td>
</tr>
<tr>
<td>log_name</td>
<td>You can setup the name of the logs that you want to see. For example if you want to see everyone you don't need to setup this field. But you can setup this parameter to just see the Parakeet logs or just the loggers that you application are logging.</td>
<td>Ex.: google.tests.e2e</td>
<td>Empty</td>
</tr>
<tr>
<td>retry</td>
<td>Some resources on the application use the retry feature. This item give to the user the ability to setup how many times the test will try to click on some button for example.</td>
<td>Integer Number</td>
<td>1</td>
</tr>
<tr>
<td>login_provider</td>
<td>The parakeet provide to you an abrastraction mechanism in order to login in the Google account (if your application have something like it). Thinking of it, we created a parameter where the user can setup the version of this mechanism.</td>
<td>google_oauth</td>
<td>google_oauth<br/>google_oauth_gapi2</td>
</tr>
<tr>
<td>window_size:<br/>&nbsp;&nbsp;&nbsp;&nbsp;width: 9999<br/>&nbsp;&nbsp;&nbsp;&nbsp;height: 9999</td>
<td>Setup the size of the screen where the tests will be performed.</td>
<td>Integer Number</td>
<td>Empty</td>
</tr>
<tr>
<td>users:<br/>&nbsp;&nbsp;&nbsp;&nbsp;file: 'users.yaml'</td>
<td>The parakeet provide a way to setup which users you will use on your tests. So this file can be setup here</td>
<td>File Path</td>
<td>Empty</td>
</tr>
<tr>
<td>home_url</td>
<td>The home url used to access the application that will be tested.</td>
<td>http://localhost</td>
<td>Empty</td>
</tr>
<tr>
<td>system_page_title</td>
<td>The system page title, this is used in order to check if the user are logged on the application. That's the way we check if the test already passed the login phase.</td>
<td>Text defined on the tag title on the application</td>
<td>Empty</td>
</tr>
</table>

### Configuration example

(TODO - create/maintain example files and reference it here)

## Users configuration

Parakeet reads a **users.yaml** file that can be used to store users credentials
that will be used in the tests..

It need to be saved on the home directory of the operation system running the tests.

### Users configuration example

(TODO - create/maintain example files and reference it here)

## Lettuce terrain

In order to bootstrap and start using this library in your Lettuce tests, you
should initialize it in the "terrain" file.

### Lettuce terrain example

(TODO - create/maintain example files and reference it here)

## Why Parakeet?

Github suggested it.

### Logo attribution

We found our "logo" image here: <a href="https://www.vecteezy.com/">Free Vector Graphics by www.vecteezy.com</a>
Binary file added parakeet-logo-small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added parakeet-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added parakeet-stack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion parakeet/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# |_| \__,_|_| \__,_|_|\_\___|\___|\__|


__version__ = '0.0.12'
__version__ = '0.0.13'
8 changes: 7 additions & 1 deletion parakeet/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ def __init__(self, browser, home_title):

def fill_email(self, email):
LOG.debug('fill_email')
self.browser.find_element_by_id('identifierId').type(email)
try:
self.browser.find_element_by_id('identifierId').type(email)
except TimeoutException:
LOG.debug('click_on_identifier_before_fill_email')
self.browser.find_element_by_id('identifierLink').click()
self.browser.find_element_by_id('identifierId').type(email)

return self

def click_next(self):
Expand Down
77 changes: 66 additions & 11 deletions parakeet/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from time import sleep
from parakeet.lettuce_logger import LOG

from .utils import next_image


class ParakeetElement(object):
"""
Expand Down Expand Up @@ -45,10 +47,35 @@ def click_and_wait_disappear(self):
self.wait_invisibility_of_element_located()
return self

def type(self, value):
def type(self, value, type_pause=0):
_type_pause = type_pause if type_pause > 0 else self.parakeet.type_pause
LOG.debug('type {} {}'.format(value, _type_pause))
self.element = self.wait_visibility_of_element_located()
self.element.send_keys(value)
self._type_handle(_type_pause, value)

self.debounce()

return self

def _type_handle(self, _type_pause, value):
if _type_pause > 0:
self._type_slowly(value, _type_pause)
else:
LOG.debug('type_normal {}'.format(value))
self.element.send_keys(value)

def _type_slowly(self, value, type_pause):
LOG.debug('type_slowly {}'.format(value))

if not (type_pause and isinstance(type_pause, float) and 0.0 < type_pause < 1):
raise ValueError(
'The type_pause argument need to be an float valid number between 0.01 and 0.99'
)

for character in value:
self.element.send_keys(character)
time.sleep(type_pause)

return self

def get_attribute(self, name):
Expand Down Expand Up @@ -114,16 +141,18 @@ def __init__(self, config):
self.selenium = self.splinter.driver
self.waiting_time = int(config.get('default_implicitly_wait_seconds'))
self.poll_frequency = int(config.get('default_poll_frequency_seconds'))
self.snapshot_debug = config.get('snapshot_debug', False)
self.type_pause = float(config.get('type_pause', 0))
self.retry_get_element = int(config.get('retry', 1))
self.selenium.implicitly_wait(self.waiting_time)
self.selenium.set_window_size(int(config['window_size']['width']),
int(config['window_size']['height']))

def find_element_by_id(self, element_id):
def find_element_by_id(self, element_id, waiting_time=None):
LOG.debug('find_element_by_id({})'
.format(element_id))
locator = (By.ID, element_id)
element = self.get_element_waiting_for_its_presence(locator)
element = self.get_element_waiting_for_its_presence(locator, waiting_time)
return ParakeetElement(element, locator, self)

def find_element_by_xpath(self, element_xpath):
Expand Down Expand Up @@ -160,14 +189,21 @@ def visit_home(self):
LOG.debug('visit_home')
self.visit(self.config['home_url'])

def get_element_waiting_for_its_presence(self, locator):
def get_element_waiting_for_its_presence(self, locator, waiting_time=None):
_waiting_time = waiting_time if waiting_time else self.waiting_time
LOG.debug('get_element_waiting_for_its_presence({}, {}, {})'
.format(locator, self.waiting_time, self.poll_frequency))
element = WebDriverWait(self.selenium, self.waiting_time, self.poll_frequency).until(
.format(locator, _waiting_time, self.poll_frequency))
element = WebDriverWait(self.selenium, _waiting_time, self.poll_frequency).until(
ec.presence_of_element_located(locator)
)
return element

def get_element_waiting_for_its_presence_by_xpath(self, xpath, waiting_time=None):
_waiting_time = waiting_time if waiting_time else self.waiting_time
LOG.debug('get_element_waiting_for_its_presence_by_xpath({}, {}, {})'
.format(xpath, _waiting_time, self.poll_frequency))
return self.get_element_waiting_for_its_presence((By.XPATH, xpath), _waiting_time)

# noinspection PyBroadException
def retry(self, method=None, **kwargs):
"""
Expand All @@ -185,12 +221,31 @@ def retry(self, method=None, **kwargs):
.format(_next_iterator, _retry, method.__name__))

kwargs.pop(_next, None)
result = method(**kwargs)

if isinstance(result, bool) and \
not result and \
_next_iterator < _retry:
return self._perform_method(_next, _next_iterator, kwargs, method)

return method(**kwargs)
return result
except Exception as ex:
LOG.error('Exception: {}'.format(ex.message))
if _next_iterator < _retry:
kwargs[_next] = _next_iterator + 1
sleep(randint(1, 3))
return self.retry(method=method, **kwargs)
return self._perform_method(_next, _next_iterator, kwargs, method)
self.selenium.save_screenshot('parakeet_error_{:05d}_{}.png'
.format(next_image(), method.__name__))
raise ex

def _perform_method(self, next, next_iterator, kwargs, method):
"""
Perform the method and return the value
:param next:
:param next_iterator:
:param kwargs:
:param method:
:return:
"""
kwargs[next] = next_iterator + 1
sleep(randint(1, 3))
return self.retry(method=method, **kwargs)
18 changes: 17 additions & 1 deletion parakeet/lettuce_logger.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import logging
import sys
from colorlog import ColoredFormatter
from lettuce import world

from parakeet import next_image

log_level = {'INFO': logging.INFO,
'WARNING': logging.WARNING,
'DEBUG': logging.DEBUG,
'ERROR': logging.ERROR}

APP_LOGGER = 'google.tests.e2e'
SNAPSHOT_DEBUG = 'SNAPSHOT_DEBUG'

formatter = ColoredFormatter(
"%(green)s%(asctime)s - %(name)s -%(reset)s %(log_color)s%(levelname)-8s%(reset)s"
Expand All @@ -16,14 +20,25 @@
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'INFO': 'blue',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
)


class CustomLogging(logging.getLoggerClass()):
"""
The ideia is override our custom logs and do some additional stuffs.
"""
def debug(self, msg, *args, **kwargs):
if world.browser.snapshot_debug:
world.browser.selenium.save_screenshot(
'parakeet_debug_{:05d}.png'.format(next_image()))
return super(CustomLogging, self).debug(msg, *args, **kwargs)


def init_logs(level='INFO', logger=None):
"""
Setup the logs inside of the tests.
Expand All @@ -45,6 +60,7 @@ def get_logger():
Return the default logger.
:return:
"""
logging.setLoggerClass(CustomLogging)
return logging.getLogger(APP_LOGGER)


Expand Down
26 changes: 26 additions & 0 deletions parakeet/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
import yaml
import os
from lettuce import world

NEXT_IMAGE_SEQUENCE = 'next_image_sequence'


def load_yaml(yaml_file):
"""
Expand All @@ -17,3 +21,25 @@ def load_yaml(yaml_file):
with open(_local_file, 'r') as f:
yaml_content = yaml.load(f)
return yaml_content


def next_image():
"""
Return the next image number
:return:
"""

if world.container.get(NEXT_IMAGE_SEQUENCE, None) is None:
world.container[NEXT_IMAGE_SEQUENCE] = range_generator()

return world.container[NEXT_IMAGE_SEQUENCE].next()


def range_generator(max_register=99999):
"""
Create a generator register
:return:
"""
my_list = range(1, max_register)
for i in my_list:
yield i

0 comments on commit 4828254

Please sign in to comment.