Skip to content

Commit dad1218

Browse files
committed
Merge pull request #365 from zephraph/custom-locators
Added ```Add Location Strategy``` Keyword
2 parents 21999d1 + 11402fe commit dad1218

File tree

12 files changed

+230
-10
lines changed

12 files changed

+230
-10
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Release Notes
2828
- 'Capture Screenshot' now attempts to create its containing directory if the directory
2929
specified in the filename does not exist.
3030
- 'Choose File' now fails if the file doesn't exist
31+
- Added new keywords 'Add Location Strategy' and 'Remove Location Strategy'
3132
[zephraph]
3233

3334
- Added 'Get Window Position' and 'Set Window Position' keywords matching the

src/Selenium2Library/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
from keywords import *
33
from version import VERSION
4+
from utils import LibraryListener
45

56
__version__ = VERSION
67

@@ -79,6 +80,25 @@ class Selenium2Library(
7980
| css | Table Should Contain `|` css=table.my_class `|` text | Matches by @id or @name attribute |
8081
| xpath | Table Should Contain `|` xpath=//table/[@name="my_table"] `|` text | Matches by @id or @name attribute |
8182
83+
= Custom Locators =
84+
85+
If more complex lookups are required than what is provided through the default locators, custom lookup strategies can
86+
be created. Using custom locators is a two part process. First, create a keyword that returns the WebElement
87+
that should be acted on.
88+
89+
| Custom Locator Strategy | [Arguments] | ${browser} | ${criteria} | ${tag} | ${constraints} |
90+
| | ${retVal}= | Execute Javascript | return window.document.getElementById('${criteria}'); |
91+
| | [Return] | ${retVal} |
92+
93+
This keyword is a reimplementation of the basic functionality of the `id` locator where `${browser}` is a reference
94+
to the WebDriver instance and `${criteria}` is the text of the locator (i.e. everything that comes after the = sign).
95+
To use this locator it must first be registered with `Add Location Strategy`.
96+
97+
Add Location Strategy custom Custom Locator Strategy
98+
99+
The first argument of `Add Location Strategy` specifies the name of the lookup strategy (which must be unique). After
100+
registration of the lookup strategy, the usage is the same as other locators. See `Add Location Strategy` for more details.
101+
82102
= Timeouts =
83103
84104
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
129149
self.set_selenium_timeout(timeout)
130150
self.set_selenium_implicit_wait(implicit_wait)
131151
self.register_keyword_to_run_on_failure(run_on_failure)
152+
self.ROBOT_LIBRARY_LISTENER = LibraryListener()

src/Selenium2Library/keywords/_element.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from selenium.webdriver.common.action_chains import ActionChains
33
from Selenium2Library import utils
44
from Selenium2Library.locators import ElementFinder
5+
from Selenium2Library.locators import CustomLocator
56
from keywordgroup import KeywordGroup
67

78
class _ElementKeywords(KeywordGroup):
@@ -601,6 +602,38 @@ def xpath_should_match_x_times(self, xpath, expected_xpath_count, message='', lo
601602
self._info("Current page contains %s elements matching '%s'."
602603
% (actual_xpath_count, xpath))
603604

605+
# Public, custom
606+
def add_location_strategy(self, strategy_name, strategy_keyword, persist=False):
607+
"""Adds a custom location strategy based on a user keyword. Location strategies are
608+
automatically removed after leaving the current scope by default. Setting `persist`
609+
to any non-empty string will cause the location strategy to stay registered throughout
610+
the life of the test.
611+
612+
Trying to add a custom location strategy with the same name as one that already exists will
613+
cause the keyword to fail.
614+
615+
Custom locator keyword example:
616+
| Custom Locator Strategy | [Arguments] | ${browser} | ${criteria} | ${tag} | ${constraints} |
617+
| | ${retVal}= | Execute Javascript | return window.document.getElementById('${criteria}'); |
618+
| | [Return] | ${retVal} |
619+
620+
Usage example:
621+
| Add Location Strategy | custom | Custom Locator Strategy |
622+
| Page Should Contain Element | custom=my_id |
623+
624+
See `Remove Location Strategy` for details about removing a custom location strategy.
625+
"""
626+
strategy = CustomLocator(strategy_name, strategy_keyword)
627+
self._element_finder.register(strategy, persist)
628+
629+
def remove_location_strategy(self, strategy_name):
630+
"""Removes a previously added custom location strategy.
631+
Will fail if a default strategy is specified.
632+
633+
See `Add Location Strategy` for details about adding a custom location strategy.
634+
"""
635+
self._element_finder.unregister(strategy_name)
636+
604637
# Private
605638

606639
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):
727760
raise AssertionError(message)
728761
self._info("Current page does not contain %s '%s'."
729762
% (element_name, locator))
730-
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from elementfinder import ElementFinder
22
from tableelementfinder import TableElementFinder
33
from windowmanager import WindowManager
4+
from customlocator import CustomLocator
45

56
__all__ = [
6-
"ElementFinder",
7-
"TableElementFinder",
8-
"WindowManager"
9-
]
7+
"ElementFinder",
8+
"TableElementFinder",
9+
"WindowManager",
10+
"CustomLocator"
11+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from robot.libraries.BuiltIn import BuiltIn
2+
3+
try:
4+
string_type = basestring
5+
except NameError:
6+
string_type = str
7+
8+
class CustomLocator(object):
9+
10+
def __init__(self, name, keyword):
11+
self.name = name
12+
self.keyword = keyword
13+
14+
def find(self, *args):
15+
element = BuiltIn().run_keyword(self.keyword, *args)
16+
if hasattr(element, '__len__') and (not isinstance(element, string_type)):
17+
return element
18+
else:
19+
return [element]

src/Selenium2Library/locators/elementfinder.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from Selenium2Library import utils
22
from robot.api import logger
33
from robot.utils import NormalizedDict
4+
from robot.libraries.BuiltIn import BuiltIn
45

56

67
class ElementFinder(object):
@@ -22,6 +23,7 @@ def __init__(self):
2223
'default': self._find_by_default
2324
}
2425
self._strategies = NormalizedDict(initial=strategies, caseless=True, spaceless=True)
26+
self._default_strategies = strategies.keys()
2527

2628
def find(self, browser, locator, tag=None):
2729
assert browser is not None
@@ -35,6 +37,30 @@ def find(self, browser, locator, tag=None):
3537
(tag, constraints) = self._get_tag_and_constraints(tag)
3638
return strategy(browser, criteria, tag, constraints)
3739

40+
def register(self, strategy, persist):
41+
if strategy.name in self._strategies:
42+
raise AttributeError("The custom locator '" + strategy.name +
43+
"' cannot be registered. A locator of that name already exists.")
44+
self._strategies[strategy.name] = strategy.find
45+
46+
if not persist:
47+
# Unregister after current scope ends
48+
suite = BuiltIn().get_variable_value('${SUITE NAME}')
49+
test = BuiltIn().get_variable_value('${TEST NAME}', '')
50+
scope = suite + '.' + test if test != '' else suite
51+
utils.events.on('scope_end', scope, self.unregister, strategy.name)
52+
53+
def unregister(self, strategy_name):
54+
if strategy_name in self._default_strategies:
55+
raise AttributeError("Cannot unregister the default strategy '" + strategy_name + "'")
56+
elif strategy_name not in self._strategies:
57+
logger.info("Cannot unregister the non-registered strategy '" + strategy_name + "'")
58+
else:
59+
del self._strategies[strategy_name]
60+
61+
def has_strategy(self, strategy_name):
62+
return strategy_name in self.strategies
63+
3864
# Strategy routines, private
3965

4066
def _find_by_identifier(self, browser, criteria, tag, constraints):
@@ -90,7 +116,7 @@ def _find_by_tag_name(self, browser, criteria, tag, constraints):
90116
return self._filter_elements(
91117
browser.find_elements_by_tag_name(criteria),
92118
tag, constraints)
93-
119+
94120
def _find_by_sc_locator(self, browser, criteria, tag, constraints):
95121
js = "return isc.AutoTest.getElement('%s')" % criteria.replace("'", "\\'")
96122
return self._filter_elements([browser.execute_script(js)], tag, constraints)

src/Selenium2Library/utils/__init__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import os
22
from fnmatch import fnmatch
33
from browsercache import BrowserCache
4+
from librarylistener import LibraryListener
5+
import events
46

57
__all__ = [
68
"get_child_packages_in",
79
"get_module_names_under",
810
"import_modules_under",
911
"escape_xpath_value",
10-
"BrowserCache"
12+
"BrowserCache",
13+
"LibraryListener",
14+
"events"
1115
]
1216

1317
# Public
@@ -18,7 +22,7 @@ def get_child_packages_in(root_dir, include_root_package_name=True, exclusions=N
1822
_discover_child_package_dirs(
1923
root_dir,
2024
_clean_exclusions(exclusions),
21-
lambda abs_path, relative_path, name:
25+
lambda abs_path, relative_path, name:
2226
packages.append(root_package_str + relative_path.replace(os.sep, '.')))
2327
return packages
2428

@@ -29,7 +33,7 @@ def get_module_names_under(root_dir, include_root_package_name=True, exclusions=
2933
root_dir,
3034
_clean_exclusions(exclusions),
3135
pattern if pattern is not None else "*.*",
32-
lambda abs_path, relative_path, name:
36+
lambda abs_path, relative_path, name:
3337
module_names.append(root_package_str + os.path.splitext(relative_path)[0].replace(os.sep, '.')))
3438
return module_names
3539

@@ -65,7 +69,7 @@ def _discover_child_package_dirs(root_dir, exclusions, callback, relative_dir=No
6569
item_abs_path = os.path.join(root_dir, item_relative_path)
6670
if os.path.isdir(item_abs_path):
6771
if os.path.exists(os.path.join(item_abs_path, "__init__.py")):
68-
exclusion_matches = [ exclusion for exclusion in exclusions
72+
exclusion_matches = [ exclusion for exclusion in exclusions
6973
if os.sep + item_relative_path.lower() + os.sep == exclusion ]
7074
if not exclusion_matches:
7175
callback(item_abs_path, item_relative_path, item)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from scope_event import ScopeStart, ScopeEnd
2+
3+
_registered_events = [ ScopeStart, ScopeEnd ]
4+
_events = []
5+
6+
__all__ = [
7+
"on",
8+
"dispatch",
9+
"register_event"
10+
]
11+
12+
def on(event_name, *args, **kwargs):
13+
for event in _registered_events:
14+
if event.name == event_name:
15+
_events.append(event(*args, **kwargs))
16+
return
17+
18+
def dispatch(event_name, *args, **kwargs):
19+
for event in _events:
20+
if event.name == event_name:
21+
event.trigger(*args, **kwargs)
22+
23+
def register_event(event):
24+
for registered_event in _registered_events:
25+
if event.name == registered_event.name:
26+
raise AttributeError("An event with the name " + event.name + " already exists.")
27+
_registered_events.append(event)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import abc
2+
3+
class Event(object):
4+
5+
@abc.abstractmethod
6+
def trigger(self, *args, **kwargs):
7+
pass
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from event import Event
2+
3+
class ScopeEvent(Event):
4+
def __init__(self, scope, action, *args, **kwargs):
5+
self.scope = scope
6+
self.action = action
7+
self.action_args = args
8+
self.action_kwargs = kwargs
9+
10+
def trigger(self, *args, **kwargs):
11+
if args[0] == self.scope:
12+
self.action(*self.action_args, **self.action_kwargs)
13+
14+
class ScopeStart(ScopeEvent):
15+
name = 'scope_start'
16+
17+
class ScopeEnd(ScopeEvent):
18+
name = 'scope_end'

0 commit comments

Comments
 (0)