From 11402fed9ec25f91054cbcdcf9611620bd5735bb Mon Sep 17 00:00:00 2001 From: zephraph Date: Wed, 25 Feb 2015 10:38:36 -0600 Subject: [PATCH] Added custom location strategy creation --- CHANGES.rst | 1 + src/Selenium2Library/__init__.py | 21 +++++++++ src/Selenium2Library/keywords/_element.py | 34 +++++++++++++- src/Selenium2Library/locators/__init__.py | 10 +++-- .../locators/customlocator.py | 19 ++++++++ .../locators/elementfinder.py | 28 +++++++++++- src/Selenium2Library/utils/__init__.py | 12 +++-- src/Selenium2Library/utils/events/__init__.py | 27 +++++++++++ src/Selenium2Library/utils/events/event.py | 7 +++ .../utils/events/scope_event.py | 18 ++++++++ src/Selenium2Library/utils/librarylistener.py | 18 ++++++++ test/acceptance/locators/custom.txt | 45 +++++++++++++++++++ 12 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 src/Selenium2Library/locators/customlocator.py create mode 100644 src/Selenium2Library/utils/events/__init__.py create mode 100644 src/Selenium2Library/utils/events/event.py create mode 100644 src/Selenium2Library/utils/events/scope_event.py create mode 100644 src/Selenium2Library/utils/librarylistener.py create mode 100644 test/acceptance/locators/custom.txt diff --git a/CHANGES.rst b/CHANGES.rst index 567eb62b9..5deb791d0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,6 +28,7 @@ Release Notes - 'Capture Screenshot' now attempts to create its containing directory if the directory specified in the filename does not exist. - 'Choose File' now fails if the file doesn't exist +- Added new keywords 'Add Location Strategy' and 'Remove Location Strategy' [zephraph] - Added 'Get Window Position' and 'Set Window Position' keywords matching the diff --git a/src/Selenium2Library/__init__.py b/src/Selenium2Library/__init__.py index 829171a5f..fffdf9c51 100644 --- a/src/Selenium2Library/__init__.py +++ b/src/Selenium2Library/__init__.py @@ -1,6 +1,7 @@ import os from keywords import * from version import VERSION +from utils import LibraryListener __version__ = VERSION @@ -79,6 +80,25 @@ class Selenium2Library( | css | Table Should Contain `|` css=table.my_class `|` text | Matches by @id or @name attribute | | xpath | Table Should Contain `|` xpath=//table/[@name="my_table"] `|` text | Matches by @id or @name attribute | + = Custom Locators = + + If more complex lookups are required than what is provided through the default locators, custom lookup strategies can + be created. Using custom locators is a two part process. First, create a keyword that returns the WebElement + that should be acted on. + + | Custom Locator Strategy | [Arguments] | ${browser} | ${criteria} | ${tag} | ${constraints} | + | | ${retVal}= | Execute Javascript | return window.document.getElementById('${criteria}'); | + | | [Return] | ${retVal} | + + This keyword is a reimplementation of the basic functionality of the `id` locator where `${browser}` is a reference + to the WebDriver instance and `${criteria}` is the text of the locator (i.e. everything that comes after the = sign). + To use this locator it must first be registered with `Add Location Strategy`. + + Add Location Strategy custom Custom Locator Strategy + + The first argument of `Add Location Strategy` specifies the name of the lookup strategy (which must be unique). After + registration of the lookup strategy, the usage is the same as other locators. See `Add Location Strategy` for more details. + = Timeouts = There are several `Wait ...` keywords that take timeout as an @@ -129,3 +149,4 @@ def __init__(self, timeout=5.0, implicit_wait=0.0, run_on_failure='Capture Page self.set_selenium_timeout(timeout) self.set_selenium_implicit_wait(implicit_wait) self.register_keyword_to_run_on_failure(run_on_failure) + self.ROBOT_LIBRARY_LISTENER = LibraryListener() diff --git a/src/Selenium2Library/keywords/_element.py b/src/Selenium2Library/keywords/_element.py index 9a4543229..2137aacb6 100644 --- a/src/Selenium2Library/keywords/_element.py +++ b/src/Selenium2Library/keywords/_element.py @@ -2,6 +2,7 @@ from selenium.webdriver.common.action_chains import ActionChains from Selenium2Library import utils from Selenium2Library.locators import ElementFinder +from Selenium2Library.locators import CustomLocator from keywordgroup import KeywordGroup class _ElementKeywords(KeywordGroup): @@ -601,6 +602,38 @@ def xpath_should_match_x_times(self, xpath, expected_xpath_count, message='', lo self._info("Current page contains %s elements matching '%s'." % (actual_xpath_count, xpath)) + # Public, custom + def add_location_strategy(self, strategy_name, strategy_keyword, persist=False): + """Adds a custom location strategy based on a user keyword. Location strategies are + automatically removed after leaving the current scope by default. Setting `persist` + to any non-empty string will cause the location strategy to stay registered throughout + the life of the test. + + Trying to add a custom location strategy with the same name as one that already exists will + cause the keyword to fail. + + Custom locator keyword example: + | Custom Locator Strategy | [Arguments] | ${browser} | ${criteria} | ${tag} | ${constraints} | + | | ${retVal}= | Execute Javascript | return window.document.getElementById('${criteria}'); | + | | [Return] | ${retVal} | + + Usage example: + | Add Location Strategy | custom | Custom Locator Strategy | + | Page Should Contain Element | custom=my_id | + + See `Remove Location Strategy` for details about removing a custom location strategy. + """ + strategy = CustomLocator(strategy_name, strategy_keyword) + self._element_finder.register(strategy, persist) + + def remove_location_strategy(self, strategy_name): + """Removes a previously added custom location strategy. + Will fail if a default strategy is specified. + + See `Add Location Strategy` for details about adding a custom location strategy. + """ + self._element_finder.unregister(strategy_name) + # Private def _element_find(self, locator, first_only, required, tag=None): @@ -727,4 +760,3 @@ def _page_should_not_contain_element(self, locator, tag, message, loglevel): raise AssertionError(message) self._info("Current page does not contain %s '%s'." % (element_name, locator)) - diff --git a/src/Selenium2Library/locators/__init__.py b/src/Selenium2Library/locators/__init__.py index c7a5d18a7..99bf42307 100644 --- a/src/Selenium2Library/locators/__init__.py +++ b/src/Selenium2Library/locators/__init__.py @@ -1,9 +1,11 @@ from elementfinder import ElementFinder from tableelementfinder import TableElementFinder from windowmanager import WindowManager +from customlocator import CustomLocator __all__ = [ - "ElementFinder", - "TableElementFinder", - "WindowManager" -] \ No newline at end of file + "ElementFinder", + "TableElementFinder", + "WindowManager", + "CustomLocator" +] diff --git a/src/Selenium2Library/locators/customlocator.py b/src/Selenium2Library/locators/customlocator.py new file mode 100644 index 000000000..c9b92c555 --- /dev/null +++ b/src/Selenium2Library/locators/customlocator.py @@ -0,0 +1,19 @@ +from robot.libraries.BuiltIn import BuiltIn + +try: + string_type = basestring +except NameError: + string_type = str + +class CustomLocator(object): + + def __init__(self, name, keyword): + self.name = name + self.keyword = keyword + + def find(self, *args): + element = BuiltIn().run_keyword(self.keyword, *args) + if hasattr(element, '__len__') and (not isinstance(element, string_type)): + return element + else: + return [element] diff --git a/src/Selenium2Library/locators/elementfinder.py b/src/Selenium2Library/locators/elementfinder.py index d4b886628..94fd5c097 100644 --- a/src/Selenium2Library/locators/elementfinder.py +++ b/src/Selenium2Library/locators/elementfinder.py @@ -1,6 +1,7 @@ from Selenium2Library import utils from robot.api import logger from robot.utils import NormalizedDict +from robot.libraries.BuiltIn import BuiltIn class ElementFinder(object): @@ -22,6 +23,7 @@ def __init__(self): 'default': self._find_by_default } self._strategies = NormalizedDict(initial=strategies, caseless=True, spaceless=True) + self._default_strategies = strategies.keys() def find(self, browser, locator, tag=None): assert browser is not None @@ -35,6 +37,30 @@ def find(self, browser, locator, tag=None): (tag, constraints) = self._get_tag_and_constraints(tag) return strategy(browser, criteria, tag, constraints) + def register(self, strategy, persist): + if strategy.name in self._strategies: + raise AttributeError("The custom locator '" + strategy.name + + "' cannot be registered. A locator of that name already exists.") + self._strategies[strategy.name] = strategy.find + + if not persist: + # Unregister after current scope ends + suite = BuiltIn().get_variable_value('${SUITE NAME}') + test = BuiltIn().get_variable_value('${TEST NAME}', '') + scope = suite + '.' + test if test != '' else suite + utils.events.on('scope_end', scope, self.unregister, strategy.name) + + def unregister(self, strategy_name): + if strategy_name in self._default_strategies: + raise AttributeError("Cannot unregister the default strategy '" + strategy_name + "'") + elif strategy_name not in self._strategies: + logger.info("Cannot unregister the non-registered strategy '" + strategy_name + "'") + else: + del self._strategies[strategy_name] + + def has_strategy(self, strategy_name): + return strategy_name in self.strategies + # Strategy routines, private def _find_by_identifier(self, browser, criteria, tag, constraints): @@ -90,7 +116,7 @@ def _find_by_tag_name(self, browser, criteria, tag, constraints): return self._filter_elements( browser.find_elements_by_tag_name(criteria), tag, constraints) - + def _find_by_sc_locator(self, browser, criteria, tag, constraints): js = "return isc.AutoTest.getElement('%s')" % criteria.replace("'", "\\'") return self._filter_elements([browser.execute_script(js)], tag, constraints) diff --git a/src/Selenium2Library/utils/__init__.py b/src/Selenium2Library/utils/__init__.py index c73348123..042d053e3 100644 --- a/src/Selenium2Library/utils/__init__.py +++ b/src/Selenium2Library/utils/__init__.py @@ -1,13 +1,17 @@ import os from fnmatch import fnmatch from browsercache import BrowserCache +from librarylistener import LibraryListener +import events __all__ = [ "get_child_packages_in", "get_module_names_under", "import_modules_under", "escape_xpath_value", - "BrowserCache" + "BrowserCache", + "LibraryListener", + "events" ] # Public @@ -18,7 +22,7 @@ def get_child_packages_in(root_dir, include_root_package_name=True, exclusions=N _discover_child_package_dirs( root_dir, _clean_exclusions(exclusions), - lambda abs_path, relative_path, name: + lambda abs_path, relative_path, name: packages.append(root_package_str + relative_path.replace(os.sep, '.'))) return packages @@ -29,7 +33,7 @@ def get_module_names_under(root_dir, include_root_package_name=True, exclusions= root_dir, _clean_exclusions(exclusions), pattern if pattern is not None else "*.*", - lambda abs_path, relative_path, name: + lambda abs_path, relative_path, name: module_names.append(root_package_str + os.path.splitext(relative_path)[0].replace(os.sep, '.'))) return module_names @@ -65,7 +69,7 @@ def _discover_child_package_dirs(root_dir, exclusions, callback, relative_dir=No item_abs_path = os.path.join(root_dir, item_relative_path) if os.path.isdir(item_abs_path): if os.path.exists(os.path.join(item_abs_path, "__init__.py")): - exclusion_matches = [ exclusion for exclusion in exclusions + exclusion_matches = [ exclusion for exclusion in exclusions if os.sep + item_relative_path.lower() + os.sep == exclusion ] if not exclusion_matches: callback(item_abs_path, item_relative_path, item) diff --git a/src/Selenium2Library/utils/events/__init__.py b/src/Selenium2Library/utils/events/__init__.py new file mode 100644 index 000000000..58da7a7b5 --- /dev/null +++ b/src/Selenium2Library/utils/events/__init__.py @@ -0,0 +1,27 @@ +from scope_event import ScopeStart, ScopeEnd + +_registered_events = [ ScopeStart, ScopeEnd ] +_events = [] + +__all__ = [ + "on", + "dispatch", + "register_event" +] + +def on(event_name, *args, **kwargs): + for event in _registered_events: + if event.name == event_name: + _events.append(event(*args, **kwargs)) + return + +def dispatch(event_name, *args, **kwargs): + for event in _events: + if event.name == event_name: + event.trigger(*args, **kwargs) + +def register_event(event): + for registered_event in _registered_events: + if event.name == registered_event.name: + raise AttributeError("An event with the name " + event.name + " already exists.") + _registered_events.append(event) diff --git a/src/Selenium2Library/utils/events/event.py b/src/Selenium2Library/utils/events/event.py new file mode 100644 index 000000000..17e42aabd --- /dev/null +++ b/src/Selenium2Library/utils/events/event.py @@ -0,0 +1,7 @@ +import abc + +class Event(object): + + @abc.abstractmethod + def trigger(self, *args, **kwargs): + pass diff --git a/src/Selenium2Library/utils/events/scope_event.py b/src/Selenium2Library/utils/events/scope_event.py new file mode 100644 index 000000000..688af554c --- /dev/null +++ b/src/Selenium2Library/utils/events/scope_event.py @@ -0,0 +1,18 @@ +from event import Event + +class ScopeEvent(Event): + def __init__(self, scope, action, *args, **kwargs): + self.scope = scope + self.action = action + self.action_args = args + self.action_kwargs = kwargs + + def trigger(self, *args, **kwargs): + if args[0] == self.scope: + self.action(*self.action_args, **self.action_kwargs) + +class ScopeStart(ScopeEvent): + name = 'scope_start' + +class ScopeEnd(ScopeEvent): + name = 'scope_end' diff --git a/src/Selenium2Library/utils/librarylistener.py b/src/Selenium2Library/utils/librarylistener.py new file mode 100644 index 000000000..cb046145f --- /dev/null +++ b/src/Selenium2Library/utils/librarylistener.py @@ -0,0 +1,18 @@ +import events as event +from robot.api import logger + +class LibraryListener(object): + + ROBOT_LISTENER_API_VERSION = 2 + + def start_suite(self, name, attrs): + event.dispatch( 'scope_start', attrs['longname'] ) + + def end_suite(self, name, attrs): + event.dispatch( 'scope_end', attrs['longname'] ) + + def start_test(self, name, attrs): + event.dispatch( 'scope_start', attrs['longname'] ) + + def end_test(self, name, attrs): + event.dispatch( 'scope_end', attrs['longname'] ) diff --git a/test/acceptance/locators/custom.txt b/test/acceptance/locators/custom.txt new file mode 100644 index 000000000..4dffb8a3a --- /dev/null +++ b/test/acceptance/locators/custom.txt @@ -0,0 +1,45 @@ +*** Settings *** +Suite Setup Go To Page "index.html" +Resource ../resource.txt + +*** Keywords *** +Setup Custom Locator + [Arguments] ${persist}=${EMPTY} + Add Location Strategy custom Custom Locator Strategy persist=${persist} + +Teardown Custom Locator + Remove Location Strategy custom + +Custom Locator Strategy + [Arguments] ${browser} ${criteria} ${tag} ${constraints} + ${retVal}= Execute Javascript return window.document.getElementById('${criteria}') || []; + [Return] ${retVal} + +*** Test Cases *** +Test Custom Locator + [Setup] Setup Custom Locator + Page Should Contain Element custom=some_id + Page Should Not Contain Element custom=invalid_id + +Ensure Locator Auto Unregisters + [Documentation] Checks to see if the custom locator registered in the last + ... test was automatically unregistered. Setup Custom Locator will fail + ... if another locator of the same name is registered. + Setup Custom Locator + +Ensure Attempting to Unregister a Default Locator Fails + Run Keyword And Expect Error * Remove Location Strategy id + +Ensure Unregistering a Non-Existent Locator Does Not Fail + Teardown Custom Locator + +Ensure a Custom Locator can be Unregistered + Setup Custom Locator persist + Teardown Custom Locator + # Test step is used for the next test. It sets up a persistant locator. + Setup Custom Locator persist + +Ensure Locators Can Persist + Page Should Contain Element custom=some_id + Run Keyword And Expect Error * Setup Custom Locator + [Teardown] Teardown Custom Locator