Skip to content

Commit bb24748

Browse files
committed
Initial modeling of a separate finder class with examples
- Created base Finder class which has placeholders for find and pre_find_action methods. Also has a couple utility methods. Still need to figure out a couple shared methods from the old ElementFinder class. - Created (Element)Finder class which would allow for a list of (element)finders instead of just one. This is the central key to be able to look, for example, both in the ShadowDOM and in the regular DOM. This might not be the final model for the list but thought I would start with a iterative class. - Also I would really like to use the ElementFinder name for this Finder class as it better describes the very specific function of finding an element. And then rename the original ElementFinder to something more generic. Having difficulty though trying to figure out that generic name .. something about both parsing locators strings and finding the elements. - Copied/Moved core find method into the DefaultFinder class. - Made a rough model for a separate Strategy class. Not sure if this is the right model for this due to reuse across several places I think this might be the way to go. I am still trying to figure out if all these class need to be ContextAware. Overall though this seems to be a good rough start ..
1 parent 1c05660 commit bb24748

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed

src/SeleniumLibrary/locators/elementfinder.py

+153
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,159 @@
2929
from .customlocator import CustomLocator
3030

3131

32+
class Finder():
33+
def __int__(self):
34+
"""Placeholder to Finder class instantation method """
35+
pass
36+
37+
def pre_find_action(self):
38+
"""Placeholder for the pre_find_action method"""
39+
pass
40+
41+
def find(self):
42+
"""Placeholder for the find method"""
43+
pass
44+
45+
def _is_webelement(self, element):
46+
# Hook for unit tests
47+
return isinstance(element, (WebElement, EventFiringWebElement))
48+
49+
50+
def _parse_locator(self, locator):
51+
if re.match(r"\(*//", locator):
52+
return "xpath", locator
53+
index = self._get_locator_separator_index(locator)
54+
if index != -1:
55+
prefix = locator[:index].strip()
56+
if prefix in self._strategies:
57+
return prefix, locator[index + 1 :].lstrip()
58+
return "default", locator
59+
60+
61+
class FinderList():
62+
def __iter__(self):
63+
pass
64+
65+
def __getitem__(self, item):
66+
pass
67+
68+
def __len__(self):
69+
pass
70+
71+
class DefaultFinder(Finder):
72+
def find(self, locator, tag=None, first_only=True, required=True, parent=None):
73+
element_type = "Element" if not tag else tag.capitalize()
74+
if parent and not self._is_webelement(parent):
75+
raise ValueError(
76+
f"Parent must be Selenium WebElement but it was {type(parent)}."
77+
)
78+
if self._is_webelement(locator):
79+
return locator
80+
prefix, criteria = self._parse_locator(locator)
81+
strategy = self._strategies[prefix]
82+
tag, constraints = self._get_tag_and_constraints(tag)
83+
elements = strategy(criteria, tag, constraints, parent=parent or self.driver)
84+
if required and not elements:
85+
raise ElementNotFound(f"{element_type} with locator '{locator}' not found.")
86+
if first_only:
87+
if not elements:
88+
return None
89+
return elements[0]
90+
return elements
91+
92+
93+
def _get_tag_and_constraints(self, tag):
94+
if tag is None:
95+
return None, {}
96+
tag = tag.lower()
97+
constraints = {}
98+
if tag == "link":
99+
tag = "a"
100+
if tag == "partial link":
101+
tag = "a"
102+
elif tag == "image":
103+
tag = "img"
104+
elif tag == "list":
105+
tag = "select"
106+
elif tag == "radio button":
107+
tag = "input"
108+
constraints["type"] = "radio"
109+
elif tag == "checkbox":
110+
tag = "input"
111+
constraints["type"] = "checkbox"
112+
elif tag == "text field":
113+
tag = "input"
114+
constraints["type"] = [
115+
"date",
116+
"datetime-local",
117+
"email",
118+
"month",
119+
"number",
120+
"password",
121+
"search",
122+
"tel",
123+
"text",
124+
"time",
125+
"url",
126+
"week",
127+
"file",
128+
]
129+
elif tag == "file upload":
130+
tag = "input"
131+
constraints["type"] = "file"
132+
elif tag == "text area":
133+
tag = "textarea"
134+
return tag, constraints
135+
136+
137+
class Strategies:
138+
def __init__(self):
139+
strategies = {
140+
"identifier": ElementFinder._find_by_identifier,
141+
"id": ElementFinder._find_by_id,
142+
"name": ElementFinder._find_by_name,
143+
"xpath": ElementFinder._find_by_xpath,
144+
"dom": ElementFinder._find_by_dom,
145+
"link": ElementFinder._find_by_link_text,
146+
"partial link": ElementFinder._find_by_partial_link_text,
147+
"css": ElementFinder._find_by_css_selector,
148+
"class": ElementFinder._find_by_class_name,
149+
"jquery": ElementFinder._find_by_jquery_selector,
150+
"sizzle": ElementFinder._find_by_jquery_selector,
151+
"tag": ElementFinder._find_by_tag_name,
152+
"scLocator": ElementFinder._find_by_sc_locator,
153+
"data": ElementFinder._find_by_data_locator,
154+
"default": ElementFinder._find_by_default,
155+
}
156+
self._strategies = NormalizedDict(
157+
initial=strategies, caseless=True, spaceless=True
158+
)
159+
self._default_strategies = list(strategies)
160+
def register(self, strategy_name, strategy_keyword, persist=False):
161+
strategy = CustomLocator(self.ctx, strategy_name, strategy_keyword)
162+
if strategy.name in self._strategies:
163+
raise RuntimeError(
164+
f"The custom locator '{strategy.name}' cannot be registered. "
165+
"A locator of that name already exists."
166+
)
167+
self._strategies[strategy.name] = strategy.find
168+
if is_falsy(persist):
169+
# Unregister after current scope ends
170+
events.on("scope_end", "current", self.unregister, strategy.name)
171+
172+
def unregister(self, strategy_name):
173+
if strategy_name in self._default_strategies:
174+
raise RuntimeError(
175+
f"Cannot unregister the default strategy '{strategy_name}'."
176+
)
177+
if strategy_name not in self._strategies:
178+
raise RuntimeError(
179+
f"Cannot unregister the non-registered strategy '{strategy_name}'."
180+
)
181+
del self._strategies[strategy_name]
182+
183+
184+
32185
class ElementFinder(ContextAware):
33186
def __init__(self, ctx):
34187
ContextAware.__init__(self, ctx)

0 commit comments

Comments
 (0)