Locators¶
The problem¶
Declaring WebElement locator can be painfull. In the end they’re nothing else but static strings 🙄. Selenium needs By and value (str). By is also a string. U can see it in Python selenium docs. So all i need is Tuple[str, str] that I can pass to WebDriver methods. Simple right? Yes but … 🤡 For example - web page has navigation section where there are list elements, that only differ by label.
<ul>
<li class="menu">Foo</li>
<li class="menu">Bar</li>
<li class="menu">Baz</li>
</ul>
I want to be able to click each one of them. So the xpath values for them will be:
foo_xpath = "//ul/li[@class='menu' and contains(., 'Foo')]"
bar_xpath = "//ul/li[@class='menu' and contains(., 'Bar')]"
baz_xpath = "//ul/li[@class='menu' and contains(., 'Baz')]"
It can be painfull. Of course you can write a function and parametrize the string
def get_menu_xpath(label: str):
return f"//ul/li[@class='menu' and contains(., '{label}')]"
But then you’ll end up with more functions, more parameters, more complex implementation🤒
abswt.elements.Locator is the solution 🤯 It provides handy way of hanling parameterized selectors, and of course includes simple standard ones.
The Locator class¶
Locator provides simple api for managing selenium locators.
Example:
from abswt.elements import Locator, Using
# simple clasic static locator
simple_button = Locator(Using.ID, 'button')
simple_button.get_by()
# >>> ('id', 'button')
simple_button.is_parameterized
# False
simple_button.parameters
# []
# parameterized button
foo_action_button = Locator(Using.NAME, 'action-{foo}')
foo_action_button.get_by(foo='foo')
# >>> ('name', 'action-foo')
foo_action_button.get_by(foo='foobar')
# >>> ('name', 'action-foobar')
simple_button.is_parameterized
# True
simple_button.parameters
# ['foo']
# You can define many parameters
menu_element = Locator(Using.XPATH, "//ul/li[@class='{class_name}' and contains(., '{label}')]")
menu_element.get_by(class_name='menu', label='Foo')
# >>> ("xpath", "//ul/li[@class='menu' and contains(., 'Foo')]")
menu_element.get_by(class_name='active-menu', label='Bar')
# >>> ("xpath", "//ul/li[@class='active-menu' and contains(., 'Bar')]")
All you need to remeber is to pass parameters into get_by using keyword arguments with the same names as defined in value template 🧠.
Cool stuff.
Validation¶
Locator validates if the parameter has been passed to get_by. When not passed it will raise ValueError 🤖
button = Locator(Using.NAME, '{action}-{foo}')
button.get_by()
# ValueError: get_by method is missing keyword arguments: ['action', 'foo']
button.get_by(action='goto')
# ValueError: get_by method is missing keyword argument: foo
Locators in Page Object Pattern¶
abswt.pages.Page is basic abstraction for defining Page Object Classes. It provides access to Actions with .actions parameter.
from abswt import expected_conditions as EC # proxy import for selenium expected conditions
class SasKodzi(Page):
url = 'https://sas-kodzi.pl'
BLOG_BUTTON = Locator(Using.XPATH, '//a[@href="/blog"]')
POST_BUTTON = Locator(Using.XPATH, "//section[@class='blog-post' and contains(., '{post_title}')]//a[contains(., 'Czytaj')]")
def goto_posts(self) -> None:
self.actions.click(self.BLOG_BUTTON.get_by())
def open_post(self, post_title: str) -> None:
self.actions.click(self.POST_BUTTON.get_by(post_title=post_title), condition=EC.visibility_of_element_located)
Then use page object
page = SasKodzi(actions)
page.open()
page.goto_posts()
page.open_post('some title')