diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..c40d97e --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,31 @@ +# Robot Framework Page Objects - Quick Start + + +## Installation and Setup +Clone this repository to a Linux system, open a terminal in the cloned directory + +Verify python is available. + +Using pip, install from the requirements.txt file in this directory with: +`$ pip install -r requirements.txt` + +Find and download [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads), make sure it is in your path: +`$ which chromedriver` +`/home/aaronpa/chromedriver/chromedriver` + +## Running an example +`$ pybot -v baseurl:http://www.conversica.com open_capture.robot` + +## Video of Setup and Running +[Screencast video demo](http://screencast.com/t/jhf74SbtYv5) - Note: browser activity not picked up by screen capture, but can be seen live instead of white canvas areas seen in this screen capture. + +### What the example does +Starting with the open_capture.robot file, a resource file is read, common.robot. In that file some external library references are defined, as well as the browser type, and browser width and height. The pybot python robot test runner provides for these variables to be overridden via command line - the width of the test run can be changed for each run. + +Once the test starts, the browser is opened to the value found in the `baseurl` variable. Selenium will wait for the page to load entirely before proceeding. + +The `Startup` and `Shutdown` keywords are defined in common.robot, but not executed. They are called via the Suite Setup and Suite Teardown settings at the stop of open_capture.robot. + +After the suite has started and the browser is open and on the intended target, the test logic can begin. In this example test, all links are scraped off of the page using a CSS locator expression. They are then logged from a FOR loop so that the href target for each link is written out. This example test has no assertions and is intended to simply demonstrate the supporting software. + + diff --git a/common.robot b/common.robot new file mode 100644 index 0000000..6381cd3 --- /dev/null +++ b/common.robot @@ -0,0 +1,29 @@ +*** Settings *** +Library Selenium2Library +Library robotpageobjects.Page +Library uuid + +*** Variables *** +${width} 1024 +${height} 768 +${browser} chrome + +*** Keywords *** +Save Selenium Screenshot + [documentation] Make sure there is a unique name to prevent overwriting + ${screenshot_index}= Get Variable Value ${screenshot_index} ${0} + Set Global Variable ${screenshot_index} ${screenshot_index.__add__(1)} + ${time}= Evaluate str(time.time()) time + Capture Page Screenshot selenium-screenshot-${time}-${screenshot_index}.png + +Startup + [documentation] Initial work to be completed in suite startup + Register Keyword To Run On Failure Save Selenium Screenshot + Open ${baseurl} + Set Window Size ${width} ${height} + +Shutdown + [documentation] Final record keeping, cleanup + Capture Page Screenshot end.png + Close + diff --git a/open_capture.robot b/open_capture.robot new file mode 100644 index 0000000..f9f8545 --- /dev/null +++ b/open_capture.robot @@ -0,0 +1,11 @@ +*** Settings *** +Resource common.robot +Suite Setup Startup +Suite Teardown Shutdown +Force Tags quickstart + +*** Test Cases *** +Sample Test + ${links}= Get Web Elements css=a + :FOR ${some link} IN @{links} + \ Log ${some link.get_attribute('href')} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7548494..1f818ec 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ +robotframework-selenium2library +robotframework-pageobjects decorator mock==1.0.1 requests==2.1.0 -robotframework<3 -robotframework-selenium2library==1.7.2 uritemplate==0.6 diff --git a/robotpageobjects/abstractedlogger.py b/robotpageobjects/abstractedlogger.py index 0cac87a..7b02980 100755 --- a/robotpageobjects/abstractedlogger.py +++ b/robotpageobjects/abstractedlogger.py @@ -23,7 +23,7 @@ def __init__(self): # Stream handler is attached from log() since # that must be decided at run-time, but here we might as well - # do the setup to keep log() clean. + # do the client_setup to keep log() clean. self.stream_handler = logging.StreamHandler(sys.stdout) self.stream_handler.setFormatter(self.formatter) diff --git a/robotpageobjects/base.py b/robotpageobjects/base.py index ba4b11b..04cb154 100755 --- a/robotpageobjects/base.py +++ b/robotpageobjects/base.py @@ -540,7 +540,7 @@ class _BaseActions(_S2LWrapper): _abstracted_logger = abstractedlogger.Logger() def __init__(self, *args, **kwargs): - """ + self.class__ = """ Initializes the options used by the actions defined in this class. """ #_ComponentsManager.__init__(self, *args, **kwargs) @@ -553,8 +553,6 @@ def __init__(self, *args, **kwargs): self.set_selenium_speed(self.selenium_speed) siw_opt = self._option_handler.get("selenium_implicit_wait") self.selenium_implicit_wait = siw_opt if siw_opt is not None else 10 - self.set_selenium_implicit_wait(self.selenium_implicit_wait) - self.set_selenium_timeout(self.selenium_implicit_wait) self.baseurl = self._option_handler.get("baseurl") @@ -710,16 +708,10 @@ def _element_find(self, locator, *args, **kwargs): if isinstance(locator, WebElement): return locator - our_wait = self.selenium_implicit_wait if kwargs.get("wait") is None else kwargs["wait"] - # If wait is set, don't pass it along to the super classe's implementation, since it has none. if "wait" in kwargs: del kwargs["wait"] - - self.driver.implicitly_wait(our_wait) - - if locator in self.selectors: locator = self.resolve_selector(locator) @@ -732,8 +724,6 @@ def _element_find(self, locator, *args, **kwargs): "\"%s\" is not a valid locator. If this is a selector name, make sure it is spelled correctly." % locator) else: raise - finally: - self.driver.implicitly_wait(self.selenium_implicit_wait) @not_keyword def find_element(self, locator, required=True, wait=None, **kwargs): diff --git a/robotpageobjects/page.py b/robotpageobjects/page.py index fb0e51b..e0c3891 100755 --- a/robotpageobjects/page.py +++ b/robotpageobjects/page.py @@ -19,26 +19,37 @@ """ from __future__ import print_function + import inspect +import json import re import urllib2 +import time +from uuid import uuid4 import decorator +import requests +import uritemplate from Selenium2Library import Selenium2Library +from applitools.errors import TestFailedError +from applitools.eyes import BatchInfo +from applitools.eyes import Eyes +from applitools.eyes import StitchMode +from applitools.eyes import MatchLevel from selenium import webdriver from selenium.common.exceptions import WebDriverException -import uritemplate +from selenium.webdriver.chrome.options import Options as ChromeOptions -from .base import _ComponentsManagerMeta, not_keyword, robot_alias, _BaseActions, _Keywords, Override, _SelectorsManager, _ComponentsManager from . import exceptions +from .base import _ComponentsManagerMeta, not_keyword, _BaseActions, _Keywords, _SelectorsManager, _ComponentsManager from .context import Context from .sig import get_method_sig - # determine if libdoc is running to avoid generating docs for automatically generated aliases ld = 'libdoc' in_ld = any([ld in str(x) for x in inspect.stack()]) + class _PageMeta(_ComponentsManagerMeta): """Meta class that allows decorating of all page object methods with must_return decorator. This ensures that all page object @@ -128,6 +139,13 @@ class Page(_BaseActions, _SelectorsManager, _ComponentsManager): """ __metaclass__ = _PageMeta ROBOT_LIBRARY_SCOPE = 'TEST SUITE' + _attempt_sauce = False + _attempt_remote = False + _attempt_eyes = False + eyes = Eyes() + _total_eyes_elapsed = 0 + _min_eyes_elapsed = 999999 + _max_eyes_elapsed = 0 def __init__(self): """ @@ -136,22 +154,67 @@ def __init__(self): """ for base in Page.__bases__: base.__init__(self) - self.browser = self._option_handler.get("browser") or "phantomjs" self.service_args = self._parse_service_args(self._option_handler.get("service_args", "")) - - self._sauce_options = [ - "sauce_username", - "sauce_apikey", - "sauce_platform", - "sauce_browserversion", - "sauce_device_orientation", - "sauce_screenresolution", - ] - for sauce_opt in self._sauce_options: - setattr(self, sauce_opt, self._option_handler.get(sauce_opt)) - - self._attempt_sauce = self._validate_sauce_options() + self.remote_url = self._option_handler.get("remote_url") + self.eyes_apikey = self._option_handler.get("eyes_apikey") + self.eyes_batch = self._option_handler.get("eyes_batch") + self.eyes_id = self._option_handler.get("eyes_id") + self.suite_name = self._option_handler.get('suite_name') + + if self.remote_url != None: + if self.remote_url.find('saucelabs.com') > -1: + self._sauce_options = [ + "sauce_username", + "sauce_apikey", + "sauce_platform", + "sauce_browserversion", + "sauce_device_orientation", + "sauce_screenresolution", + "sauce_tunnel_id", + "sauce_parent_tunnel", + ] + for sauce_opt in self._sauce_options: + setattr(self, sauce_opt, self._option_handler.get(sauce_opt)) + + self._attempt_sauce = self._validate_sauce_options() + else: + self._attempt_remote = True + + if self.eyes_apikey != None: + self._attempt_eyes = True + self.eyes.api_key = self.eyes_apikey + if not (self._attempt_sauce and self.browser == "internetexplorer"): + self.eyes.force_full_page_screenshot = True + self.eyes.stitch_mode = StitchMode.CSS + if self.eyes_batch == None: self.eyes_batch = self.suite_name + if self.eyes_id == None: self.eyes_id = uuid4().__str__() + if self.eyes.batch == None: + self.eyes.batch = BatchInfo(self.eyes_batch) + self.eyes.batch.id_ = self.eyes_id + + self._Capabilities = getattr(webdriver.DesiredCapabilities, self.browser.upper()) + for cap in self._Capabilities: + new_cap = self._option_handler.get(cap) + if new_cap is not None: + self._Capabilities[cap] = new_cap + + if self.browser == "chrome": + opts = ChromeOptions() + opts.add_argument("--disable-infobars") + opts.add_argument("--disable-popups") + opts.add_argument("--disable-save-password-bubble") + opts.add_argument("--disable-extensions") + opts.add_experimental_option('prefs', {'credentials_enable_service': False, + 'profile': {'password_manager_enabled': False}}) + self._Capabilities.update(opts.to_capabilities()) + + if self.browser == "internetexplorer": + self._Capabilities.update( + { + "requireWindowFocus": True, + } + ) # There's only a session ID when using a remote webdriver (Sauce, for example) self.session_id = None @@ -178,7 +241,7 @@ def _titleize(str): :param str: camel case string :return: title case string """ - return re.sub('([a-z0-9])([A-Z])', r'\1 \2', re.sub(r"(.)([A-Z][a-z]+)", r'\1 \2', str)) + return re.sub('([a-z0-9])([A-Z])', r'\1 \2', re.sub(r"(.)([A-Z][a-z]+)", r'\1 \2', str)) @staticmethod @not_keyword @@ -198,8 +261,7 @@ def get_keyword_names(self): # Return all method names on the class to expose keywords to Robot Framework keywords = [] - #members = inspect.getmembers(self, inspect.ismethod) - + # members = inspect.getmembers(self, inspect.ismethod) # Look through our methods and identify which ones are Selenium2Library's # (by checking it and its base classes). @@ -239,11 +301,11 @@ def get_keyword_names(self): return keywords def _attempt_screenshot(self): - try: - self.capture_page_screenshot() - except Exception, e: - if e.message.find("No browser is open") != -1: - pass + try: + self.capture_page_screenshot() + except Exception, e: + if e.message.find("No browser is open") != -1: + pass @not_keyword def run_keyword(self, alias, args, kwargs): @@ -320,7 +382,8 @@ def get_keyword_documentation(self, kwname): kw = getattr(self, kwname, None) alias = '' if kwname in _Keywords._aliases: - alias = '*Alias: %s*\n\n' % _Keywords.get_robot_aliases(kwname, self._underscore(self.name))[0].replace('_', ' ').title() + alias = '*Alias: %s*\n\n' % _Keywords.get_robot_aliases(kwname, self._underscore(self.name))[0].replace('_', + ' ').title() docstring = kw.__doc__ if kw.__doc__ else '' docstring = re.sub(r'(wrapper)', r'*\1*', docstring, flags=re.I) return alias + docstring @@ -484,8 +547,9 @@ def _resolve_url(self, *args): return uritemplate.expand(self.baseurl + self.uri, uri_vars) else: if uri_type == 'template': - raise exceptions.UriResolutionError('%s has uri template %s , but no arguments were given to resolve it' % - (pageobj_name, self.uri)) + raise exceptions.UriResolutionError( + '%s has uri template %s , but no arguments were given to resolve it' % + (pageobj_name, self.uri)) # the user wants to open the default uri return self.baseurl + self.uri @@ -504,6 +568,20 @@ def go_to(self, *args): super(_BaseActions, self).go_to(resolved_url) return self + def set_window_size(self, width, height, *args): + """ + Wrapper to make set_window_size method support applitools eyes resize. + """ + resolved_url = self._resolve_url(*args) + super(_BaseActions, self).set_window_size(width, height, *args) + if self._attempt_eyes: + self.eyes.set_viewport_size( + super(_BaseActions, self).driver + , viewport_size={'width': int(width), 'height': int(height)}) + else: + super(_BaseActions, self).set_window_size(width, height, *args) + return self + def _generic_make_browser(self, webdriver_type, desired_cap_type, remote_url, desired_caps): """Override Selenium2Library's _generic_make_browser to allow for extra params to driver constructor.""" @@ -560,47 +638,145 @@ class MyPageObject(PageObject): :type delete_cookies: Boolean :returns: _BaseActions instance """ + + caps = None + remote_url = False resolved_url = self._resolve_url(*args) - if self._attempt_sauce: - remote_url = "http://%s:%s@ondemand.saucelabs.com:80/wd/hub" % (self.sauce_username, self.sauce_apikey) - caps = getattr(webdriver.DesiredCapabilities, self.browser.upper()) - caps["platform"] = self.sauce_platform - if self.sauce_browserversion: - caps["version"] = self.sauce_browserversion - if self.sauce_device_orientation: - caps["device_orientation"] = self.sauce_device_orientation - if self.sauce_screenresolution: - caps["screenResolution"] = self.sauce_screenresolution + + if self._attempt_sauce | self._attempt_remote: + if self._attempt_sauce: + self.remote_url = "http://%s:%s@ondemand.saucelabs.com:80/wd/hub" % ( + self.sauce_username, self.sauce_apikey) + caps = getattr(webdriver.DesiredCapabilities, self.browser.upper()) + caps["platform"] = self.sauce_platform + if self.sauce_browserversion: + caps["version"] = self.sauce_browserversion + if self.sauce_device_orientation: + caps["device_orientation"] = self.sauce_device_orientation + if self.sauce_screenresolution: + caps["screenResolution"] = self.sauce_screenresolution + if self.sauce_tunnel_id: + caps["tunnelIdentifier"] = self.sauce_tunnel_id + if self.sauce_parent_tunnel: + caps["parentTunnel"] = self.sauce_parent_tunnel + caps["name"] = self._option_handler.get('suite_name') + + if self.remote_url is not None: + remote_url = self.remote_url + caps = self._Capabilities try: self.open_browser(resolved_url, self.browser, remote_url=remote_url, desired_capabilities=caps) + if self._attempt_sauce: + # username, apikey = self.get_sauce_creds() + self.rest_url = "https://%s:%s@saucelabs.com/rest/v1/%s/jobs/%s" \ + % ( + self.sauce_username, self.sauce_apikey, self.sauce_username, + self.driver.session_id) + except (urllib2.HTTPError, WebDriverException, ValueError), e: - raise exceptions.SauceConnectionError("Unable to run Sauce job.\n%s\n" - "Sauce variables were:\n" - "sauce_platform: %s\n" - "sauce_browserversion: %s\n" - "sauce_device_orientation: %s\n" - "sauce_screenresolution: %s" - - % (str(e), self.sauce_platform, - self.sauce_browserversion, self.sauce_device_orientation, - self.sauce_screenresolution) - ) + if self._attempt_sauce: + raise exceptions.SauceConnectionError("Unable to run Sauce job.\n%s\n" + "Sauce variables were:\n" + "sauce_platform: %s\n" + "sauce_browserversion: %s\n" + "sauce_device_orientation: %s\n" + "sauce_screenresolution: %s" + + % (str(e), self.sauce_platform, + self.sauce_browserversion, self.sauce_device_orientation, + self.sauce_screenresolution) + ) + else: + raise e self.session_id = self.get_current_browser().session_id self.log("session ID: %s" % self.session_id) else: - self.open_browser(resolved_url, self.browser) + self.open_browser(resolved_url, self.browser, desired_capabilities=caps) self.log("PO_BROWSER: %s" % (str(self.get_current_browser())), is_console=False) return self + def eyes_open(self, test_name, eyes_match_level=None): + if self._attempt_eyes: + if eyes_match_level == None: + self.eyes.match_level = MatchLevel.LAYOUT + elif eyes_match_level.lower == 'layout': + self.eyes.match_level = MatchLevel.LAYOUT + elif eyes_match_level.lower == 'content': + self.eyes.match_level = MatchLevel.CONTENT + elif eyes_match_level.lower == 'exact': + self.eyes.match_level = MatchLevel.EXACT + elif eyes_match_level.lower == 'strict': + self.eyes.match_level = MatchLevel.STRICT + else: + self.eyes.match_level = MatchLevel.STRICT + self.log( + "eyes.open test_name={}, batch={}, id={}, match={}".format(test_name, self.eyes_batch, self.eyes_id, + self.eyes.match_level)) + self.eyes.open(driver=self.driver, app_name='Robot Page - spike', test_name=test_name, ) + return self + + def eyes_close(self): + if self._attempt_eyes: + self.log("eyes.close") + try: + start = time.time() + self.eyes.close() + done = time.time() + elapsed = done - start + self.log(" duration: {}".format(elapsed)) + except TestFailedError as e: + self.log("Applitools Eyes error detected: {}".format(e.message), level="WARNING") + return self + + def check_javascript_error(self, throw=False): + if self.browser == 'chrome': + browser_errors = self.driver.get_log("browser") + for some_error in browser_errors: + self.log("Browser error detected: {}".format(some_error),level="ERROR") + if throw == True or throw.lower() == 'true': + assert len(browser_errors)==0,"Non-zero Javascript error count" + return self + def close(self): """ Wrapper for Selenium2Library's close_browser. :returns: None """ + if self._attempt_sauce: + try: + self.rest_url + except AttributeError: + self.rest_url = "https://%s:%s@saucelabs.com/rest/v1/%s/jobs/%s" \ + % ( + self.sauce_username, self.sauce_apikey, self.sauce_username, self.driver.session_id) + self._report_sauce_status(self._option_handler.get('suite_name'), + self._option_handler.get('suite status'), + ['test-tag', 'page.py', 'close'], + self.rest_url) + + if self._attempt_eyes: + self._attempt_eyes = False + self.eyes_close() + self.close_browser() + return self + + def _report_sauce_status(self, name, status, tags=[], rest_url=None): + # Parse username and access_key from the remote_url + payload = {'name': name, + 'passed': status == 'PASS', + 'tags': tags} + + response = requests.put(rest_url, data=json.dumps(payload)) + assert response.status_code == 200, response.text + + # Log video url from the response + video_url = json.loads(response.text).get('video_url') + if video_url: + self.log('video.flv'.format(video_url))