diff --git a/parakeet/__init__.py b/parakeet/__init__.py index 408828d..b8adaba 100644 --- a/parakeet/__init__.py +++ b/parakeet/__init__.py @@ -1,2 +1,4 @@ -from .config import * +from .utils import * from .auth import * +from .browser import * +from .page_objects import * diff --git a/parakeet/__version__.py b/parakeet/__version__.py index 760da53..6e3f1ac 100644 --- a/parakeet/__version__.py +++ b/parakeet/__version__.py @@ -5,4 +5,4 @@ # |_| \__,_|_| \__,_|_|\_\___|\___|\__| -__version__ = '0.0.2' +__version__ = '0.0.11' diff --git a/parakeet/auth.py b/parakeet/auth.py index 77e9c06..54444c1 100644 --- a/parakeet/auth.py +++ b/parakeet/auth.py @@ -2,7 +2,7 @@ import base64 from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support import expected_conditions as ec def decode(password): @@ -23,7 +23,7 @@ def __init__(self, browser, home_title): """ :param browser: the browser. - :type browser: splinter.Browser + :type browser: ParakeetBrowser :param home_title: the home page title you expect after logging in. :type home_title: str @@ -32,27 +32,23 @@ def __init__(self, browser, home_title): self.home_title = home_title def fill_email(self, email): - self.browser.is_element_present_by_id('identifierId', wait_time=10) - self.browser.fill('identifier', email) + self.browser.find_element_by_id('identifierId').type(email) return self def click_next(self): - self.browser.is_element_present_by_id('identifierNext', wait_time=10) - self.browser.find_by_id('identifierNext').click() + self.browser.find_element_by_id('identifierNext').click() return self def fill_password(self, password): - self.browser.is_element_visible_by_css('#password input', wait_time=10) - self.browser.type('password', decode(password)) + self.browser.splinter.is_element_visible_by_css('#password input', self.browser.waiting_time) + self.browser.splinter.type('password', decode(password)) return self def login(self): - self.browser.is_element_present_by_id('passwordNext', wait_time=10) - WebDriverWait(self.browser.driver, 10).until(EC.element_to_be_clickable((By.ID, 'passwordNext'))) - self.browser.find_by_id('passwordNext').click() + self.browser.find_element_by_id('passwordNext').click() return self def redirect_to_home(self): - WebDriverWait(self.browser.driver, 10).until(EC.title_contains(self.home_title)) - WebDriverWait(self.browser.driver, 10).until(EC.invisibility_of_element_located((By.CLASS_NAME, 'main-loading'))) + WebDriverWait(self.browser.selenium, 10).until(ec.title_contains(self.home_title)) + WebDriverWait(self.browser.selenium, 10).until(ec.invisibility_of_element_located((By.CLASS_NAME, 'main-loading'))) return self diff --git a/parakeet/browser.py b/parakeet/browser.py new file mode 100644 index 0000000..555f5eb --- /dev/null +++ b/parakeet/browser.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +from __future__ import division +import time +import re +from splinter import Browser +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as ec + + +class ParakeetElement(object): + """ + A wrapper around Selenium WebElement for AngularJS and AngularJS Material projects. + + Attributes: + element: The Selenium WebElement object. + locator: A tuple of (by, path) from Selenium API. + parakeet: The instance of ParakeetBrowser. + """ + def __init__(self, element, locator, parakeet): + self.element = element + self.locator = locator + self.parakeet = parakeet + + def clear(self): + self.element = self.wait_visibility_of_element_located() + self.element.clear() + return self + + def click(self): + self.element = self.wait_element_to_be_clickable() + self.element.click() + return self + + def click_and_wait_disappear(self): + self.click() + self.wait_invisibility_of_element_located() + return self + + def type(self, value): + self.element = self.wait_visibility_of_element_located() + self.element.send_keys(value) + self.debounce() + return self + + def get_attribute(self, name): + return self.element.get_attribute(name) + + def wait_visibility_of_element_located(self): + return WebDriverWait(self.parakeet.selenium, self.parakeet.waiting_time).until( + ec.visibility_of_element_located(self.locator) + ) + + def wait_invisibility_of_element_located(self): + return WebDriverWait(self.parakeet.selenium, self.parakeet.waiting_time).until( + ec.invisibility_of_element_located(self.locator) + ) + + def wait_element_to_be_clickable(self): + return WebDriverWait(self.parakeet.selenium, self.parakeet.waiting_time).until( + ec.element_to_be_clickable(self.locator) + ) + + def debounce(self): + """ + If the element has an AngularJS debounce set, it sleeps for 1.5x the debounce value. + """ + ng_model_options_value = self.get_attribute('ng-model-options') + + if ng_model_options_value is not None: + debounce_value = ParakeetElement.extract_debounce_value(ng_model_options_value) + if debounce_value > 0: + time.sleep(1.5 * debounce_value/1000) + + @staticmethod + def extract_debounce_value(attribute_value): + """ + Try to extract the AngularJS debounce value from ng-model-options value. + + Usually that attribute value is something like this: '{ debounce: 300 }'. + + :param attribute_value: The string representing the ng-model-options attribute value. + :return: debounce value, or 0 if does not have one. + """ + debounce_value = "0" + result_debounce = re.search("""debounce"*'*:"*'*\s*"*'*(\d*)"*'*,*\s*""", attribute_value, re.IGNORECASE) + if result_debounce: + debounce_value = result_debounce.group(1) + return int(debounce_value) + + +class ParakeetBrowser(object): + """ + A wrapper around Splinter / Selenium for AngularJS and AngularJS Material projects. + + Attributes: + config: The Parakeet/project config dictionary. + splinter: The Splinter browser/driver instance. + selenium: The Selenium driver instance. + waiting_time: Maximum time in seconds to wait for an action. + """ + + def __init__(self, config): + self.config = config + self.splinter = Browser(config['browser']) + self.selenium = self.splinter.driver + self.waiting_time = int(config['default_implicitly_wait_seconds']) + 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): + locator = (By.ID, element_id) + element = self.get_element_waiting_for_its_presence(locator) + return ParakeetElement(element, locator, self) + + def find_element_by_xpath(self, element_xpath): + locator = (By.XPATH, element_xpath) + element = self.get_element_waiting_for_its_presence(locator) + return ParakeetElement(element, locator, self) + + def is_element_present_by_id(self, element_id): + return self.splinter.is_element_present_by_id(element_id, self.waiting_time) + + def is_element_present_by_xpath(self, element_xpath): + return self.splinter.is_element_present_by_xpath(element_xpath, self.waiting_time) + + def is_text_present(self, text): + return self.splinter.is_text_present(text) + + def quit(self): + self.splinter.quit() + + def visit(self, url): + self.splinter.visit(url) + + def visit_home(self): + self.visit(self.config['home_url']) + + def get_element_waiting_for_its_presence(self, locator): + element = WebDriverWait(self.selenium, self.waiting_time).until( + ec.presence_of_element_located(locator) + ) + return element + diff --git a/parakeet/common_steps.py b/parakeet/common_steps.py new file mode 100644 index 0000000..48b60dc --- /dev/null +++ b/parakeet/common_steps.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from lettuce import step, world +from auth import LoginPage + + +@step(u'.* the logged user is "([^"]*)"') +def login(step, user_name): + # If it is already logged do not attempt to login + if world.cfg['system_page_title'] not in world.browser.selenium.title: + email = world.users[user_name]['email'] + password = world.users[user_name]['password'] + LoginPage(world.browser, world.cfg['system_page_title'])\ + .fill_email(email)\ + .click_next()\ + .fill_password(password)\ + .login()\ + .redirect_to_home() diff --git a/parakeet/config.py b/parakeet/config.py deleted file mode 100644 index d286739..0000000 --- a/parakeet/config.py +++ /dev/null @@ -1,15 +0,0 @@ -import yaml - - -def load_config(yaml_file): - """ - Load a YAML file and return it as a dict. - - :param yaml_file: path to the yaml file. - :type yaml_file: str - :return: a dict. - """ - print('Loading configs from {0} file.'.format(yaml_file)) - with open(yaml_file, 'r') as config_file: - config_dict = yaml.load(config_file) - return config_dict diff --git a/parakeet/page_objects.py b/parakeet/page_objects.py new file mode 100644 index 0000000..ad58e1d --- /dev/null +++ b/parakeet/page_objects.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + + +class BasePageObject(object): + """A base class for all page objects. + + Attributes: + browser: The ParakeetBrowser instance. + """ + + def __init__(self, browser): + self.browser = browser + + diff --git a/parakeet/utils.py b/parakeet/utils.py new file mode 100644 index 0000000..b7986be --- /dev/null +++ b/parakeet/utils.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import yaml + + +def load_yaml(yaml_file): + """ + Load a YAML file and return it as a dict. + + :param yaml_file: path to the yaml file. + :type yaml_file: str + :return: a dict. + """ + print('Loading file: {}.'.format(yaml_file)) + with open(yaml_file, 'r') as f: + yaml_content = yaml.load(f) + return yaml_content diff --git a/requirements.txt b/requirements.txt index 273890e..083f891 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ selenium==3.7.0 splinter==0.7.7 pyyaml==3.12 +lettuce==0.2.23