Selenide Alternative in Practice
Implementation & Lessons Learned
Preface…
Implementation & Lessons Learned
Selenide Alternative in Practice
Selenide
Selenide = ?
Selenide = Effective
web test automation tool
=
tool to automate
web UI tests logic
not browser
(it should be already
automated;)
Selenide = Effective
web test automation tool
=
tool to automate
web UI tests logic
not browser
concise API
…
…
…
Selenide = Effective
web test automation tool
=
tool to automate
web UI tests logic
not browser
concise API
waiting search
…
…
Selenide = Effective
web test automation tool
=
tool to automate
web UI tests logic
not browser
concise API
waiting search
waiting asserts
…
Selenide = Effective
web test automation tool
=
tool to automate
web UI tests logic
not browser
concise API
waiting search
waiting asserts
dynamic elements
Selenide = Effective
web test automation tool
=
tool to automate
web UI tests logic
not browser
concise API
waiting search
waiting asserts
dynamic elements
Selenide Alternative?
concise API
waiting search
waiting asserts
dynamic elements
Selenide Alternative?
concise API
waiting search
waiting asserts
dynamic elements
How should it work?
Selenide Alternative?
concise API
waiting search
waiting asserts
dynamic elements
How should it work? How to build?
With examples in
Proviso
Partial sacrificing
DRY, privatisation, etc.
for simplification of examples
Concise API
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")

# vs


s(by_xpath("//*[contains(text(),'task')]"))

# vs


s(with_text('task'))
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")

# vs


s(by_xpath("//*[contains(text(),'task')]"))

# vs


s(with_text('task'))
DRY locator helpers
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")

# vs


s(by_xpath("//*[contains(text(),'task')]"))

# vs


s(with_text('task'))
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")

# vs


s(by_xpath("//*[contains(text(),'task')]"))

# vs


s(with_text('task'))
OOP
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")

# vs


s(by_xpath("//*[contains(text(),'task')]"))

# vs


s(with_text('task'))
from selenium import webdriver
driver = webdriver.Firefox()
# ... OOP
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")

# vs


s(by_xpath("//*[contains(text(),'task')]"))

# vs


s(with_text('task'))
Modular
from selenium import webdriver
driver = webdriver.Firefox()
# ... OOP
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")

# vs


s(by_xpath("//*[contains(text(),'task')]"))

# vs


s(with_text('task'))
Modular
from selenium import webdriver
driver = webdriver.Firefox()
# ...
from selene.tools import set_driver, s
set_driver(webdriver.Firefox())
# ...
OOP
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")
# vs


s(by_xpath("//*[contains(text(),'task')]")).

# vs


s(with_text('task'))
Modular
OOP
from selenium import webdriver
driver = webdriver.Firefox()
driver.get(‘http://todomvc4tasj.herokuapp.com’)
from selene.tools import set_driver, s
set_driver(webdriver.Firefox())
visit(‘http://todomvc4tasj.herokuapp.com’)
driver.find_element(By.XPATH, "//*[contains(text(),'task')]")
# vs


s(by_xpath("//*[contains(text(),'task')]")).

# vs


s(with_text('task'))
Functions over Methods
for main actions
from selenium import webdriver
driver = webdriver.Firefox()
driver.get(‘http://todomvc4tasj.herokuapp.com’)
from selene.tools import set_driver, s
set_driver(webdriver.Firefox())
visit(‘http://todomvc4tasj.herokuapp.com’)
s(by_css(‘#new-todo’))
# vs/or

s('#new-todo')
Convenient defaults


driver.find_element_by_css_selector('#new-todo').clear()

driver.find_element_by_css_selector(‘#new-todo’)
.send_keys(‘task')


# vs


s(‘#new-todo’).clear().send_keys(‘task’)


driver.find_element_by_css_selector('#new-todo').clear()

driver.find_element_by_css_selector(‘#new-todo’)
.send_keys(‘task')


# vs


s(‘#new-todo’).clear().send_keys(‘task’)
Chainable methods


driver.find_element_by_css_selector('#new-todo')

.send_keys('task', Keys.ENTER)

# vs


s('#new-todo').send_keys('task').press_enter()


driver.find_element_by_css_selector('#new-todo')

.send_keys('task', Keys.ENTER)

# vs


s('#new-todo').send_keys('task').press_enter()
“Short-cut” methods
s(‘#new-todo’).clear().send_keys(‘task’)


# vs


s(‘#new-todo’).set(‘task’)
“Short-cut” methods
Built in Explicit Waits aka Waiting
Asserts
WebDriverWait(driver, 4).until(
element_to_be_clickable(by_css('#new-todo')))



# vs



s('#new-todo').should_be(clickable)


new_todo = by_css('#new-todo')



visit('http://todomvc4tasj.herokuapp.com')



s(new_todo).set('task 1').press_enter()

# ...

s(new_todo).set('task 2').press_enter()



# vs



new_todo = s('#new-todo')



visit('http://todomvc4tasj.herokuapp.com')



new_todo.set('task 1').press_enter()

# ...

new_todo.set('task 2').press_enter()


new_todo = by_css('#new-todo')



visit('http://todomvc4tasj.herokuapp.com')



s(new_todo).set('task 1').press_enter()

# ...

s(new_todo).set('task 2').press_enter()



# vs



new_todo = s('#new-todo')



visit('http://todomvc4tasj.herokuapp.com')



new_todo.set('task 1').press_enter()

# ...

new_todo.set('task 2').press_enter()


new_todo = by_css('#new-todo')



visit('http://todomvc4tasj.herokuapp.com')



s(new_todo).set('task 1').press_enter()

# ...

s(new_todo).set('task 2').press_enter()



# vs



new_todo = s('#new-todo')



visit('http://todomvc4tasj.herokuapp.com')



new_todo.set('task 1').press_enter()

# ...

new_todo.set('task 2').press_enter()
?
Dynamic Elements
Dynamic Elements
aka Lazy Proxy Elements
new_todo = s('#new-todo')
def s(css_selector_or_locator):

return SElement(css_selector_or_locator)
Element Factory
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, config.timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, config.timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
“waiting search”
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, config.timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
“waiting assert”


def wait_for(entity, condition, timeout):

# ...



end_time = time.time() + timeout

while True:

try:

value = condition(entity)

if value is not None:

return value

except (WebDriverException,) as exc:

# ...

time.sleep(config.poll_during_waits)

if time.time() > end_time:

break

raise TimeoutException(

"""

failed while waiting %s seconds

to assert %s%s

""" % (timeout, condition.__class__.__name__,
str(condition), ...)


def wait_for(entity, condition, timeout):

# ...



end_time = time.time() + timeout

while True:

try:

value = condition(entity)

if value is not None:

return value

except (WebDriverException,) as exc:

# ...

time.sleep(config.poll_during_waits)

if time.time() > end_time:

break

raise TimeoutException(

"""

failed while waiting %s seconds

to assert %s%s

""" % (timeout, condition.__class__.__name__,
str(condition), ...)


def wait_for(entity, condition, timeout):

# ...



end_time = time.time() + timeout

while True:

try:

value = condition(entity)

if value is not None:

return value

except (WebDriverException,) as exc:

# ...

time.sleep(config.poll_during_waits)

if time.time() > end_time:

break

raise TimeoutException(

"""

failed while waiting %s seconds

to assert %s%s

""" % (timeout, method.__class__.__name__,
str(condition), ...)


def wait_for(entity, condition, timeout):

# ...



end_time = time.time() + timeout

while True:

try:

value = condition(entity)

if value is not None:

return value

except (WebDriverException,) as exc:

# ...

time.sleep(config.poll_during_waits)

if time.time() > end_time:

break

raise TimeoutException(

"""

failed while waiting %s seconds

to assert %s%s

""" % (timeout, method.__class__.__name__,
str(condition), ...)
class Visible(object):



def __call__(self, selement):
self.selement = selement

found = selement.finder()

return found if found.is_displayed() else None



def __str__(self):

return """

for element found by: %s...

""" % (self.selement.locator, …)


visible = Visible()
class Visible(object):



def __call__(self, selement):
self.selement = selement

found = selement.finder()

return found if found.is_displayed() else None



def __str__(self):

return """

for element found by: %s...

""" % (self.selement.locator, …)


visible = Visible()
class Visible(object):



def __call__(self, webelement):
self.selement = selement

found = selement.finder()

return webelement.is_displayed()



def __str__(self):

return """

for element found by: %s...

""" % (self.selement.locator, …)


visible = Visible()
Original jSelenide style
with selement.finder() being moved to wait_for
class Visible(object):



def __call__(self, webelement):
self.selement = selement

found = selement.finder()

return webelement.is_displayed()



def __str__(self):

return """

for element found by: %s...

""" % (self.selement.locator, …)


visible = Visible()
Original jSelenide style
with selement.finder() being moved to wait_for
more simple

and secure

but 

less powerful
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, config.timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, config.timeout)

return self


insist = assure

should = assure

should_be = assure

should_have = assure

def click(self):

return self.do(lambda element: element.click())
Speed?
class SElement(...):

def __init__(self, css_selector_or_locator, context=...):

self.locator = parse(css_selector_or_locator)
# ...



def finder(self):

return self.context.find_element(*self.locator)
def assure(self, condition):

wait_for(self, condition, config.timeout)

return self
def do(self, command):

self.assure(visible)

command(self.finder())
return self


def click(self):

return self.do(lambda element: element.click())
redundant finder call


def finder(self):

return self.context.find_element(*self.locator)
def refind(self):

self.found = self.finder()

return self.found
def assure(self, condition):

self.found = wait_for(self, condition, config.timeout)

return self
def do(self, command):

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
reusing self.found


def finder(self):

return self.context.find_element(*self.locator)
def refind(self):

self.found = self.finder()

return self.found
def assure(self, condition):

self.found = wait_for(self, condition, config.timeout)

return self
def do(self, command):

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
even when not needed
wait always
def refind(self):

self.found = self.finder()

return self.found
def assure(self, condition):

self.found = wait_for(self, condition, config.timeout)

return self
def do(self, command):

try:

self.refind()

command()

except (WebDriverException):

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
smarter:)
def refind(self):

self.found = self.finder()

return self.found
def assure(self, condition):

self.found = wait_for(self, condition, config.timeout)

return self
def do(self, command):

try:

self.refind()

command()

except (WebDriverException):

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
smarter:)
Makes Selene as fast
as Selenium “with research” :)
def refind(self):

self.found = self.finder()

return self.found
def assure(self, condition):

self.found = wait_for(self, condition, config.timeout)

return self
def do(self, command):

try:

self.refind()

command()

except (WebDriverException):

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
def refind(self):

self.found = self.finder()

return self.found
def assure(self, condition):

self.found = wait_for(self, condition, config.timeout)

return self
def do(self, command):

try:

self.refind()

command()

except (WebDriverException):

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
What if I sometimes…

I want “raw selenium” speed?


def cash(self):

self.is_cached = True

self.finder = lambda: self.found

return self
f
def do(self, command):

try:
if not self.is_cached:

self.refind()

command()

# ...

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
Introducing simple cashing


def cash(self):

self.is_cached = True

self.finder = lambda: self.found

return self
f
def do(self, command):

try:
if not self.is_cached:

self.refind()

command()

# ...

self.assure(visible)

command()

return self


def click(self):

return self.do(lambda: self.found.click())
Introducing simple cashing
Making Selene almost as fast
as raw Selenium :)
Proof Demo
Total Laziness
todo_list = s('#todo-list')


active_tasks = todo_list.find_all(‘.active’)


first_completed_task = todo_list.find(‘.completed')



visit('http://todomvc4tasj.herokuapp.com')

# ...
Usage
tasks = ss(‘#todo-list>li')


first_task = tasks.get(1)


task_a = tasks.find(exact_text(“a"))


visible_tasks = tasks.filter(visible)



visit('http://todomvc4tasj.herokuapp.com')

# ...
Usage
visible_tasks = ss(‘#todo-list>li').filter(visible)



visit('http://todomvc4tasj.herokuapp.com')

# ...
def ss(css_selector_or_locator):

return SElementsCollection(css_selector_or_locator)
class SElementsCollection(...):

def __init__(self, css_selector_or_locator, ..., ...,):

self.locator = parse(css_selector_or_locator)

...



def finder(self):

return ...



def filter(self, condition):

return ...
...
class SElementsCollection(...):

def __init__(self, css_selector_or_locator, ..., ...,):

self.locator = parse(css_selector_or_locator)

...



def finder(self):

return ...



def filter(self, condition):

return ...
...
locator = lambda index: '%s[%s]' % (self.locator, index)

webelements = self.context.find_elements(*self.locator)

return [SElement(locator(index)).cash_with(element)

for index, element in enumerate(webelements)]
SElementsCollection#finder
locator = lambda index: '%s[%s]' % (self.locator, index)

webelements = self.context.find_elements(*self.locator)

return [SElement(locator(index)).cash_with(element)

for index, element in enumerate(webelements)]
SElementsCollection#finder
locator = lambda index: '%s[%s]' % (self.locator, index)

webelements = self.context.find_elements(*self.locator)

return [SElement(locator(index)).cash_with(element)

for index, element in enumerate(webelements)]
SElementsCollection#finder
class SElementsCollection(...):

def __init__(self, css_selector_or_locator, ..., ...,):

self.locator = parse(css_selector_or_locator)

...



def finder(self):

return ...



def filter(self, condition):

return FilteredSElementsCollection(self, condition)
...
class FilteredSElementsCollection(SElementsCollection):

def __init__(self, selements_collection, condition):

self.coll = selements_collection

self.condition = condition

# ...



def finder(self):

filtered_elements = ...

return filtered_elements
class FilteredSElementsCollection(SElementsCollection):

def __init__(self, selements_collection, condition):

self.coll = selements_collection

self.condition = condition

# ...



def finder(self):

filtered_elements = ...

return filtered_elements
class FilteredSElementsCollection(SElementsCollection):

def __init__(self, selements_collection, condition):

self.coll = selements_collection

self.condition = condition

# ...



def finder(self):

filtered_elements = ...

return filtered_elements
self.coll = selements_collection

self.condition = condition

locator = "(%s).filter(%s)" % (

self.coll.locator,

self.condition.__class__.__name__)

SElementsCollection.__init__(self, ('selene', locator))

# ...
FilteredSElementsCollection#__init__
self.coll = selements_collection

self.condition = condition

locator = "(%s).filter(%s)" % (

self.coll.locator,

self.condition.__class__.__name__)

SElementsCollection.__init__(self, ('selene', locator))

# ...
FilteredSElementsCollection#__init__
filtered_elements = [selement for selement in self.coll

if self.condition(selement)]


return filtered_elements
FilteredSElementsCollection#finder
Meta-programming?
class SElement(...):

def __init__(self, ..., context=...):
# …
def finder(self):

return self.context.find_element(*self.locator)
# ...



class SElementsCollection(...):

def __init__(self, ..., context=..., selement_class=…):
# ...

# ...
def __init__(self, ..., context=RootSElement()):
class RootSElement(object):

def __getattr__(self, item):

return getattr(selene.tools.get_driver(), item)
The most innocent example :)
def __getattr__(self, item):

return self.do(lambda: getattr(self.found, item))
# VS
def click(self):

return self.do(lambda: self.found.click())



def submit(self):

return self.do(lambda: self.found.submit())



def clear(self):

return self.do(lambda: self.found.clear())
...
Proxying vs Overriding
def __getattr__(self, item):

return self.do(lambda: getattr(self.found, item))
# VS
def click(self):

return self.do(lambda: self.found.click())



def submit(self):

return self.do(lambda: self.found.submit())



def clear(self):

return self.do(lambda: self.found.clear())
...
Proxying vs Overriding
Widgets
class Task(SElement):

def delete(self):

self.hover()

self.s(".destroy").click()





def test_custom_selement():

given_active("a", "b")

Task("#todo-list>li:nth-child(1)").delete()
Just works :)
class Task(SElement):

def delete(self):

self.hover()

self.s(".destroy").click()





def test_custom_selement():

given_active("a", "b")

Task("#todo-list>li:nth-child(1)").delete()
Just works :)
Because…
class SElement(...):

def __init__(self, ..., context=RootSElement()):

#...

def finder(self):

return self.context.find_element(*self.locator)



def s(self, css_selector_or_locator):

return SElement(css_selector_or_locator, context=self)
#...
Because…
class SElement(...):

def __init__(self, ..., context=RootSElement()):

#...

def finder(self):

return self.context.find_element(*self.locator)



def s(self, css_selector_or_locator):

return SElement(css_selector_or_locator, context=self)
#...
Collection of widgets
class Task(SElement):

def delete(self):

self.hover()

self.s(".destroy").click()



def test_selements_collection_of_custom_selements():

given_active("a", "b", "c")

for task in ss("#todo-list>li", of=Task):
task.delete()
ss("#todo-list>li", of=Task).should_be(empty)
Collection of widgets
class Task(SElement):

def delete(self):

self.hover()

self.s(".destroy").click()



def test_selements_collection_of_custom_selements():

given_active("a", "b", "c")

for task in ss("#todo-list>li", of=Task):
task.delete()
ss("#todo-list>li", of=Task).should_be(empty)
class SElementsCollection(...):


def __init__(self, ..., ...):

...

...

...



def finder(self):
...

return [
SElement(locator(index)).cash_with(element)

for index, element in enumerate(webelements)]

...
recalling basic implementation…
class SElementsCollection(...):


def __init__(self, ..., ..., of=SElement):

...

self.wrapper_class = of

...



def finder(self):
...

return [
self.wrapper_class(locator(index)).cash_with(element)

for index, element in enumerate(webelements)]

...
needs more…
and sometimes even much more…
including meta-programming…
Collection of widgets:
selection of one of them
visible_tasks = ss("#todo-list>li", of=Task).filter(visible)
...
...

task = visible_tasks.find(exact_text("a")):
task.delete()
...
class SElementsCollectionElementByCondition(SElement):

def __init__(self, selements_collection, condition):

self.coll = selements_collection

self.condition = condition

locator = "(%s).found_by(%s)" % (

self.coll.locator,

self.condition.__class__.__name__)

...



def finder(self):

for selement in self.coll:

if self.condition(selement):

return selement.found
find(exact_text("a")) =>
self.coll = selements_collection

self.condition = condition

locator = "(%s).found_by(%s)" % (

self.coll.locator,

self.condition.__class__.__name__)

SElementsCollectionElementByCondition.__init__(self,
(“selene”, locator))

...
SElementsCollectionElementByCondition#__init__
self.coll = selements_collection

self.condition = condition

locator = "(%s).found_by(%s)" % (

self.coll.locator,

self.condition.__class__.__name__)

SElementsCollectionElementByCondition.__init__(self,
(“selene”, locator))

extend(self, self.coll.wrapper_class, ("selene", locator))
SElementsCollectionElementByCondition#__init__
def extend(obj, cls, *init_args, **init_kwargs):

obj.__class__ = type(obj.__class__.__name__,
(obj.__class__, cls),
{})

cls.__init__(obj, *init_args, **init_kwargs)
Dynamic class extension
def extend(obj, cls, *init_args, **init_kwargs):

obj.__class__ = type(obj.__class__.__name__,
(obj.__class__, cls),
{})

cls.__init__(obj, *init_args, **init_kwargs)
Dynamic class extension
def extend(obj, cls, *init_args, **init_kwargs):

obj.__class__ = type(obj.__class__.__name__,
(obj.__class__, cls),
{})

cls.__init__(obj, *init_args, **init_kwargs)
Dynamic class extension
e.g. to initialise probable widget’s subelements
class Task(SElement):

def init(self):

self.destroy_button = self.s(".destroy")

...



...

task = ss("#todo-list>li", of=Task).find(text("a")):
task.hover()
task.destroy_button.click()
...
like in this example ;)
__init__ vs init ? o_O
class SElement(...):

def __init__(self):
...

if hasattr(self, 'init'):

self.init()
__init__ vs init ? o_O
Proxying vs dynamic extension
SElement(...)
def __getattr__(self, item):

return self.do(lambda: getattr(self.found, item))
SElementsCollectionElementByCondition(SElement)
__init__
extend(self, self.coll.wrapper_class, ('selene', locator))
# VS
SElement(...)
def __getattr__(self, item):

return self.do(lambda: getattr(self.found, item))
SElementsCollectionElementByCondition(SElement)
__init__
extend(self, self.coll.wrapper_class, ('selene', locator))
Install & Docs
$ pip install selene
https://github.com/yashaka/selene
visit(‘/questions_and_answers')


s('#question').set('<YOUR QUESTION>’).press_enter()


ss('.answer').should_not_be(empty)
github.com/yashaka
github.com/yashaka/selene
yashaka@gmail.com
@yashaka
Thank You

Selenide Alternative in Practice - Implementation & Lessons learned [SeleniumCamp 2016]