Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/Selenium2Library/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from keywords import *
from version import VERSION
from utils import LibraryListener

__version__ = VERSION

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
34 changes: 33 additions & 1 deletion src/Selenium2Library/keywords/_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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))

10 changes: 6 additions & 4 deletions src/Selenium2Library/locators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from elementfinder import ElementFinder
from tableelementfinder import TableElementFinder
from windowmanager import WindowManager
from customlocator import CustomLocator

__all__ = [
"ElementFinder",
"TableElementFinder",
"WindowManager"
]
"ElementFinder",
"TableElementFinder",
"WindowManager",
"CustomLocator"
]
19 changes: 19 additions & 0 deletions src/Selenium2Library/locators/customlocator.py
Original file line number Diff line number Diff line change
@@ -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]
28 changes: 27 additions & 1 deletion src/Selenium2Library/locators/elementfinder.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 8 additions & 4 deletions src/Selenium2Library/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions src/Selenium2Library/utils/events/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions src/Selenium2Library/utils/events/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import abc

class Event(object):

@abc.abstractmethod
def trigger(self, *args, **kwargs):
pass
18 changes: 18 additions & 0 deletions src/Selenium2Library/utils/events/scope_event.py
Original file line number Diff line number Diff line change
@@ -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'
18 changes: 18 additions & 0 deletions src/Selenium2Library/utils/librarylistener.py
Original file line number Diff line number Diff line change
@@ -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'] )
45 changes: 45 additions & 0 deletions test/acceptance/locators/custom.txt
Original file line number Diff line number Diff line change
@@ -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