Expected conditions#
This is the latest development version
Some features documented on this page may not yet be available in the published stable version.
Overview#
Conditions, or "expected conditions" (as they are called in Selenium WebDriver), or matchers (as they are called in PyHamcrest) – are callable objects or functions that once called on some entity – test if the entity matches the corresponding criteria of a condition, and then, if matched, simply pass, or raise an error otherwise. They are used in testing to flexibly implement test assertions and explicit waits that are also relevant in context of "asserting dynamic behavior of some entities", like elements on the dynamically loaded page.
Note
In Selenium WebDriver the valid condition is also a simple predicate function, i.e. the function returning True or False, and it's not mandatory for the condition in Selenium to raise an error to signal "not matched" state – returning False is enough. In Selene, it's not the case – the condition should raise an Error to signal "not matched" state. In Selene such design decision is a base for powerful logging abilities of conditions in context of their failures. Since a condition owns a knowledge of its criteria to be matched, it seems to be the best place (in context of following the "high cohesion" principle) to define how this criteria will be logged if not matched – what naturally happens on raising a "not matched" error.
From other point of view – the True|False-predicate-based conditions
are easier to define. To keep similar level of easiness, Selene provides
additional helpers (like
[ConditionMismatch._to_raise_if_not(predicate)
][selene.core.exceptions.ConditionMismatch._to_raise_if_not])
and the Match
to build conditions based on
predicates. While the base class of Match – the
Condition
class currently has stable API only
for Pass|Fail-function-based matchers). More on their usage below.
Predefined conditions#
match.* VS be.* & have.*#
Usually you don't need to build conditions yourself, they are predefined
for easier reuse. In Selene, they are predefined in
match.*
and can be accessed additionally via
be.*
and have.*
syntax.
match
is handy because it's a "one term to learn and one import to use":
from selene import browser, match
browser.should(match.title('Selene'))
browser.element('#save').should(match.enabled)
browser.element('#loading').should(match.visible.not_)
browser.element('#field').should(match.exact_text('hello'))
browser.element('#field').should(match.css_class('required').not_)
be
and have
force to use "more imports" but are more "high level"
and might help to write more readable code:
from selene import browser, be, have
browser.should(have.title('Selene'))
browser.element('#save').should(be.enabled)
browser.element('#loading').should(be.not_.visible)
browser.element('#field').should(have.exact_text('hello'))
browser.element('#field').should(have.no.css_class('required'))
Extending predefined conditions (Demo)#
Because match
is "just one term", it might be easier also
to "extend Selene's predefined conditions" with your custom ones,
because you have to deal only with "one module re-definition", like in:
# Full path can be: my_tests_project/extensions/selene/match.py
from selene.core.match import *
from selene.core import match as __selene_match
from selene.support.conditions import not_ as __selene_not_
not_ = __selene_not_
# An example of a new condition definition
def no_visible_alert(browser):
try:
text = browser.driver.switch_to.alert.text
raise AssertionError('visible alert found with text: ' + text)
except NoAlertPresentException:
pass
# Maybe you don't like the match.exact_text + match.text_containing conditions naming
# and decide to override it:
def text(expected):
return __selene_match.exact_text(expected)
def partial_text(expected):
return __selene_match.text_containing(expected)
So we can use it like:
from selene import browser
from my_tests_project.extensions.selene import match
browser.should(match.no_visible_alert)
browser.should(match.title('Selene'))
browser.element('#save').should(match.enabled)
browser.element('#loading').should(match.not_.visible)
browser.element('#field').should(match.text('hello'))
browser.element('#field').should(match.partial_text('hel'))
browser.element('#field').should(match.not_.css_class('required'))
From other side, you do it once, so maybe it's not like that hard
to redefine "more readable" be
and have
to extend predefined Selene conditions and forget about it:).
The choice is yours...
Maybe even all your extended conditions will be "have" ones:
from selene import browser, be
from my_tests_project.extensions.selene import have
browser.should(have.no_visible_alert)
browser.should(have.title('Selene'))
browser.element('#save').should(be.enabled)
browser.element('#loading').should(be.not_.visible)
browser.element('#field').should(have.text('hello'))
browser.element('#field').should(have.partial_text('hel'))
browser.element('#field').should(have.no.css_class('required'))
Info
If you need a more detailed explanation of how we "extended" Selene's predefined conditions in the example above, look at the How to implement custom advanced commands? article, that explains same pattern for the case of extending Selene predefined commands.
Functional conditions definition#
Ok, now, let's go deeper into how to define custom conditions starting from function-based conditions.
Tip
Function-based conditions are the simplest ones in Selene, and they are limited in reusability during definition of new conditions based on existing ones. Object-oriented conditions are more powerful. But understanding how to define functional conditions is crucial to understand how to define object-oriented ones, as the latter are built on top of the former. Thus, we recommend not to skip this section if you are new to the topic.
Tip
If you are an experienced SDET engineer, and are familiar with the concept
of expected conditions and how to define them e.g. in Selenium WebDriver,
and want a fast way to get familiar with how to define most powerful
custom conditions in Selene, jump directly to the examples of
Match
class usage.
In case of any doubts on the topic, read on this article without skipping
any section.
Pass|Fail-function-based definition#
The simplest way to implement a condition in Selene is to define a function that raises AssertionError if the argument passed to the function does not match some criteria:
True|False-predicate-based definition#
Or in one-liner if we use
ConditionMismatch
factory method
to build a condition from predicate:
Condition application#
Then we test a condition simply by calling it:
is_positive(1) # ✅ passes
try:
is_positive(0) # ❌ fails
except AssertionError as error:
assert 'Expected positive number, but got 0' in str(error)
But really useful conditions become when used in waits:
def has_positive_number(entity):
number = entity.number
if not number > 0:
raise AssertionError(f'Expected positive number, but got {number}')
class Storage:
number = 0
# imagine that here we created an async operation
# that after some timeout will update number in storage - to > 0
from selene.core.wait import Wait
Wait(Storage, at_most=1.0).for_(has_positive_number)
Note
In Selene the Wait object is usually built under the hood, and we would see something like:
where:
class Storage:
number = 0
@classmethod
def should(cls, condition):
Wait(cls, at_most=1.0).for_(condition)
Now recall the actual Selene assertion on browser object:
😉
Rendering in error messages#
If wait did not succeed, it will raise an error with a message like:
Timed out after 1.0s, while waiting for:
<class 'Storage'>.has_positive_number
Reason: AssertionError: Expected positive number, but got 0
– as you see the message is quite informative and helps to understand
what happened. Pay attention that the name has_positive_number
of our condition-function was used in error message to explain
what we were waiting for.
Improving error messages of lambda-based conditions#
But what if we used a lambda predicate to define the condition:
from selene.core.exceptions import ConditionMismatch
has_positive_number = ConditionMismatch._to_raise_if_not(
lambda entity: entity.number > 0
)
Then error would be less informative, because lambda is anonymous – there is no way to build a name for it:
Timed out after 1.0s, while waiting for:
<class 'Storage'>.<function <lambda> at 0x106b5d8b0>
Reason: ConditionMismatch: actual: 0
To fix this, we can provide a name for the lambda by wrapping it into Query:
from selene.core.exceptions import ConditionMismatch
from selene.common._typing_functions import Query
has_positive_number = ConditionMismatch._to_raise_if_not(
Query('has positive number', lambda entity: entity.number > 0)
)
Now error would look quite informative again:
Timed out after 1.0s, while waiting for:
<class 'Storage'>.has positive number
Reason: ConditionMismatch: actual: 0
Choosing the style to define functional conditions#
Feel free to choose the way that fits your needs best among:
- Pass|Fail-function-condition
- True|False-lambda-predicate-condition
built with wrapped lambda into
Query
andConditionMismatch._to_raise_if_not
.
Basic refactoring of conditions#
Utilizing ConditionMismatch
gives also an option
to "break down the predicate logic" into two steps:
- querying the entity for needed value
- applying predicate to the value
This is performed by adding additional query function, to get something useful from the entity before applying the predicate:
has_positive_number = ConditionMismatch._to_raise_if_not(
lambda number: number > 0,
lambda entity: entity.number,
)
And now we know how to benefit from more descriptive error messages by providing names for our lambdas as follows:
has_positive_number = ConditionMismatch._to_raise_if_not(
Query('is positive', lambda number: number > 0),
Query('number', lambda entity: entity.number),
)
In case we already have somewhere defined queries:
is_positive = Query('is positive', lambda number: number > 0)
number = Query('number', lambda entity: entity.number)
The condition definition becomes even more concise and readable:
Parametrized conditions#
Another example of common usage is the definition of a parametrized condition:
has_number_more_than = lambda limit: ConditionMismatch._to_raise_if_not(
is_more_than(limit),
number,
)
– where:
is_more_than = lambda limit: Query('is more than', lambda number: number > limit)
number = Query('number', lambda entity: entity.number)
Or with regular functions:
def has_number_more_than(limit):
return ConditionMismatch._to_raise_if_not(is_more_than(limit), number)
– where:
number = Query('number', lambda entity: entity.number)
def is_more_than(limit):
return Query('is more than', lambda number: number > limit)
Object-Oriented re-composable conditions Demo#
This is enough for simpler cases, but what if we want to be able to compose new conditions based on existing ones, like in:
# wait for not negative even number
Wait(Storage, at_most=1.0).for_(has_positive_number.and_(has_even_number).not_)
Here comes in rescue the Condition
class
and its Match
subclass, allowing to build such
"re-composable" conditions.
⬇️ Classes to build and recompose conditions#
Condition
#
Bases: Generic[E]
Class to build, invert and compose "callable matcher" objects,
that conforms to Callable[[E], None | <RAISED ERROR>]
protocol,
and represent the "conditions that can pass or fail when tested or
so called "matched" against an entity". So, once called on some entity of type E
such condition object should test if the entity matches the condition object,
and then simply pass if matched or raise AssertionError otherwise.
Note
You can find using the "match" term in different forms, depending on context. It may confuse a bit. Assume the following...
If you see in the code "match", i.e. as a "verb in the imperative mood"
or as a "noun", then, usually, the "assertion" is meant, i.e.
"test a condition against an entity, then pass if matched or fail otherwise".
For example: element.should(match.visible)
or
number.should(Match('is positive'), by=lambda it: it > 0)
or
number.wait.for_(Condition('is positive', match=ConditionMismatch._to_raise_if_not(lambda it: it > 0)))
.
If you see "matching", i.e. as a "verb in present participle", then
the "predicate application" will be forced, i.e.
"test a condition against an entity, then return True if matched or False otherwise".
For example: element.matching(be.visible)
or even element.matching(match.visible)
(the "matching" meaning kind of "overrides the match one").
Yes, matching(match.*)
– look a bit weird, but firstly,
there more readable alternatives like matching(be.*)
or simply matching(*)
, and,
secondly, if your tests are implemented according to best practices,
you will rarely need applying condition as predicate ;).
There is also an alternative, a bit more readable way to check the
"matching" result: match.visible._matching(element)
– but take into account
that the _
prefix in condition._matching
marks the method as a part
of Selene's experimental API, that may change with the time.
When to use Condition-object-based definition#
For most cases you don't need this class directly,
you can simply reuse the conditions predefined in match.*
.
You will start to need this class when you want to build your own custom conditions to use in a specific to your case assertions. And even then, it might be enough to use a simpler version of this class – its Match subclass that has handier bunch of params to set on constructing new condition, that is especially handy when you build conditions inline without the need to store and reuse them later, like in:
from selene import browser
from selene.core.condition import Match
input = browser.element('#field')
input.should(Match(
'normalized value',
by=lambda it: not re.find_all(r'(\s)+', it.locate().get_attribute(value)),
))
For other examples of using Match
, up to something as concise
as input.should(Match(actual=query.value, by=is_normalized))
,
see its section.
And for simplest scenarios you may keep it most KISS with "Pass|Fail-function-based conditions" or "True|False-predicate-based conditions" as described in Functional conditions definition.
But when you start bothering about reusing existing conditions
to build new ones on top of them by applying logical and
, or
,
or not
operators you start to face some limitations of "functional-only"
conditions...
Compare:
has_positive_number = ConditionMismatch._to_raise_if_not_actual(
Query('number', lambda entity: entity.number),
Query('is positive', lambda number: number > 0),
)
has_negative_number_or_zero = ConditionMismatch._to_raise_if_not_actual(
Query('number', lambda entity: entity.number),
Query('is negative or zero', lambda number: number <= 0),
)
to:
has_positive_number = Condition(
'has positive number',
ConditionMismatch._to_raise_if_not_actual(
lambda entity: entity.number,
lambda number: number > 0,
)
)
has_negative_number_or_zero = has_positive_number.not_
Note
If you see
[ConditionMismatch._to_raise_if_not_actual][selene.core.exceptions.ConditionMismatch._to_raise_if_not_actual]
for the first time,
it's similar to
[ConditionMismatch._to_raise_if_not][selene.core.exceptions.ConditionMismatch._to_raise_if_not],
but with inverted order of params:
ConditionMismatch._to_raise_if_not_actual(number, is_positive)
==
ConditionMismatch._to_raise_if_not(is_positive, number)
Notice the following ⬇️
Specifics of the Condition-object-based definition#
- It is simply a wrapping functional condition (Pass|Fail-function-based) into a Condition object with custom name. Thus, we can use all variations of defining functional conditions to define object-oriented ones.
- Because we provided a custom name
(
'has positive number'
in the case above), it's not mandatory to wrap lambdas into Query objects to achieve readable error messages, unlike we had to do for functional conditions.
Customizing name of inverted conditions#
The name of the has_negative_number_or_zero
will be automatically
constructed as 'has not (positive number)'
. In case you want custom,
you can simply re-wrap it into a new condition:
has_positive_number = Condition(
'has positive number',
ConditionMismatch._to_raise_if_not_actual(
lambda entity: entity.number,
lambda number: number > 0,
),
)
has_negative_number_or_zero = Condition( # ⬅️
'has negative number or zero', # 💡
has_positive_number.not_
)
Same re-wrap can be done with a Match subclass of Condition, but with an explicit keyword argument only:
...
has_negative_number_or_zero = Match( # ⬅️
'has negative number or zero',
by=has_positive_number.not_
) # ↖️
Tip
The "customizing name" pattern above can give a bit more benefits,
when applied to conditions built via Match
class by providing
actual
and by
params.
has_positive_number = Match( # ⬅️
'has positive number',
actual=lambda entity: entity.number, # 💡
by=lambda number: number > 0, # 💡
)
has_negative_number_or_zero = Condition(
'has negative number or zero',
has_positive_number.not_
)
'''
# OR
has_negative_number_or_zero = Match(
'has negative number or zero',
by=has_positive_number.not_
)
'''
In this case the error message will be a bit more informative, logging also the actual value.
Tip
There is an alternative shortcut to invert a base condition with custom name
– the Condition.as_not(base_condition, new_name)
method:
...
# compare:
has_negative_number_or_zero = Condition(
'has negative number or zero',
has_positive_number.not_
)
# to:
has_negative_number_or_zero = Condition.as_not( # ⬅️
has_positive_number,
'has negative number or zero', # 💡
)
It works completely the same under the hood as inverting + wrapping via
Condition(new_name, base_condition.not_)
or Match(new_name, by=base_condition.not_)
– it's just a matter of taste.
Customizing name of other composable conditions#
Same "wrap" technique can be used to name conditions composed by logical
and
or or
like in:
hidden_in_dom = Condition('hidden in DOM', present_in_dom.and_(visible.not_))
blank = Condition('blank', tag('input').and_(value('')).or_(exact_text('')))
'''
# OR with a Match subclass
hidden_in_dom = Match('hidden in DOM', present_in_dom.and_(visible.not_))
blank = Match('blank', tag('input').and_(value('')).or_(exact_text('')))
'''
Re-composing methods summary#
Thus:
- to invert condition you use
condition.not_
property - to compose conditions by logical
and
you usecondition.and_(another_condition)
- to compose conditions by logical
or
you usecondition.or_(another_condition)
- to override condition name after "composition" you wrap it into a new condition
via
Condition(new_name, composed_condition)
Alternative signatures for Condition class initializer#
Condition class initializer has more than two params, not just name and functional condition as we have seen above
name
– a required custom name for the condition or a callable on optional entity, that will be used to describe the condition in the error message depending on the entity, that can be useful in case of multi-entity conditions, for example, asize
condition can work with both "singular" and "plural" entities, then we could provide the name aslambda entity: 'have size' if entity is not None and hasattr(entity, '__len__') else 'has size'
test
– a Pass|Fail-function based condition
⬆️ Both these parameters are "positional only". This is an "internal trick of us"
to postpone the time to decide on the final naming of them. We can hardly choose
between name
and description
for the first one, and between test
and match
for the second one:), but while they are "positional only" you have nothing
to worry about on your side;).
Other params are optional:
_actual
– a callable on entity to get the actual value from it to match. Can't be passed iftest
arg was provided. Iftest
arg was not provided, yet can be skipped, then it will be similar to passinglambda it: it
as actual_by
- a callable predicate to check for matching actual value if_actual
was provided or for matching entity itself otherwise. Can't be passed iftest
was provided. You have to choose how to define a condition – viatest
or viaby
with optionalactual
._describe_actual_result
– a callable on the actual value used during match, that will be used if match failed to describe the actual value in the error message. Currently, will be used only if_actual
was provided explicitly, but this might change in the future..._inverted
– a boolean flag to mark the condition as inverted. False by default._falsy_exceptions
– a tuple of exceptions that should be considered as "falsy" if condition was inverted. By default, it's(AssertionError, )
only.
All are marked as "experimental/internal" for the following reasons:
_actual
and_by
might be moved completely from Condition to its Match subclass in order to simplify the implementation of the base Condition class, taking into account that providing such arguments nevertheless leads to more readable code exactly in case of Match, not Condition._describe_actual_result
can be renamed (e.g. todescribe_actual
ordescribe
) and also moved to Match for same reason as_actual
_inverted
is planned to be "protected" all the time, in order to force end user to use.not_
property to invert conditions even if they are "inline" ones_falsy_exceptions
can be renamed and also are marked as "protected" to eliminate usage in the "inline" context
This bunch of params lead to different ways to define conditions. Since the Condition class has more params marked as "internal/experimental" than the Match class, we will show more of such variations in examples of the Match class usage. And now we provide just a few to see the actual difference between defining condition via Pass|Fail-function and via True|False-predicate.
Recall the initial example:
has_positive_number = Condition(
'has positive number',
ConditionMismatch._to_raise_if_not_actual(
lambda entity: entity.number,
lambda number: number > 0,
)
)
or with using _to_raise_if_not
over _to_raise_if_not_actual
– they differ
by order of params:
has_positive_number = Condition(
'has positive number',
ConditionMismatch._to_raise_if_not(
lambda number: number > 0,
lambda entity: entity.number,
)
)
But utilizing the named arguments python feature, we can define our own order (unless params are defined as positional only, and they are not in this case):
has_positive_number = Condition(
'has positive number',
ConditionMismatch._to_raise_if_not(
actual=lambda entity: entity.number,
by=lambda number: number > 0,
)
)
The core parameter: test (2nd positional parameter)#
Regardless of what happens with params to ConditionMismatch class methods, the 2nd positional-only param to the Condition initializer is allways the same – Pass|Fail-function-based condition.
Parameters: actual and by VS test#
In order to remove a bit of boilerplate on object-oriented condition definition,
there two other alternative parameters to the Condition class initializer:
_actual
and _by
– similar to the parameters of the
[Condition._to_raise_if_not
][selene.core.exceptions.ConditionMismatch._to_raise_if_not]
helper that we used to define functional True|False-predicate-based conditions.
Compare:
has_positive_number = Condition(
'has positive number',
ConditionMismatch._to_raise_if_not(
actual=lambda entity: entity.number,
by=lambda number: number > 0,
)
)
to:
has_positive_number = Condition(
'has positive number',
_actual=lambda entity: entity.number,
_by=lambda number: number > 0,
)
or by utilizing the Match
subclass of Condition, that has actual
and by
params as stable
(not experimental), and so – safer to use:
has_positive_number = Match(
'has positive number',
actual=lambda entity: entity.number,
by=lambda number: number > 0,
)
actual
(or _actual
in case of Condition) is optional by the way, so the
following is also valid:
Match over Condition for readability#
If you find passing optional actual
and mandatory by
better
than passing 2nd positional argument as Pass|Fail-function condition,
and the Condition
term is too low level for your case, consider using the
Match
subclass in full power. It also fits better
with a should
method of Selene entities in context of inline definition
of conditions – compare: entity.should(Match(...))
to
entity.should(Condition(...))
😉.
Tuning error message description of the actual result#
Remember, that it's not mandatory to wrap lambdas into Query objects
here to achieve readable error messages, because we already provided a
custom name. But in case of providing actual that has a readable name,
e.g. via wrapping as Query('named', lambda entity: ...)
, we will get
a bit more descriptive actual result description in the error message:
actual named: ...
over actual: ...
if we provided pure lambda...
Another way to tune the description of the actual result is to set the
_describe_actual_result
parameter. Compare:
has_positive_number = Match(
'has positive number',
actual=lambda entity: entity.number,
by=lambda number: number > 0,
)
has_positive_number(0)
# will fail with:
'''
ConditionMismatch: actual: 0
'''
or:
has_positive_number = Match(
'has positive number',
actual=Query('number', lambda entity: entity.number),
by=lambda number: number > 0,
)
has_positive_number(0)
# will fail with:
'''
ConditionMismatch: actual number: 0
'''
to:
has_positive_number = Match(
'has positive number',
actual=Query('number', lambda entity: entity.number),
by=lambda number: number > 0,
_describe_actual_result=lambda number: (
f'actual number {number} is not positive ❌'
),
)
has_positive_number(0)
# will fail with:
'''
ConditionMismatch: actual number 0 is not positive ❌
'''
Match
#
Bases: Condition[E]
A subclass-based alternative to Condition
class for better readability on straightforward usage of conditions
built inline with optional custom name, True|False-predicate as by
argument
instead of Pass|Fail-function-based condition as 2nd positional Condition argument,
and optional callable query on entity as actual
argument to get a value
from entity to match in by
callable.
Demo examples#
Example of full inline definition:
from selene import browser
from selene.core.condition import Match
...
browser.should(Match('title «Expected»', by=lambda its: its.title == 'Expected'))
Example of inline definition with reusing existing queries and predicates and autogenerated name:
from selene import browser, query
from selene.core.condition import Match
from selene.common import predicate
...
browser.should(Match(query.title, predicate.equals('Expected'))
Warning
Remember that in most cases you don't need to build condition
from scratch. You can reuse the ones from predefined conditions
at match.*
or among corresponding aliases at be.*
and have.*
.
For example, instead of
Match(query.title, predicate.equals('Expected')
you can simply reuse have.title('Expected')
with import
from selene import have
.
Now, let's go in details through different scenarios of constructing a Match condition-object.
Differences from Condition initializer#
Unlike its base class (Condition),
the Match
subclass has a bit different parameter variations to set
on constructing new condition. The Match
initializer:
- does not accept
test
param (2nd positional-only Condition init parameter), that is actually the core of its superclassCondition
logic, and is used to store the Pass|Fail-function (aka functional condition) to test the entity against the actual condition criteria, implemented in that function. - accepts the alternative to
test
params: theby
predicate and the optionalactual
query to transform an entity before passing to the predicate for match. Bothby
andactual
are not marked as "experimental" or "internal", as they are in case of Condition initializer for now.by
is a "keyword-only" parameter.actual
can be passed as a 2nd positional param, if bothname
andby
are present, or as a keyword-only param otherwise.
- accepts name as the first positional param only, similar to Condition class
initializer, but can be skipped if you are OK with automatically generated name
based on
by
andactual
arguments names. - accepts other not mentioned yet parameters, similar to Condition initializer:
_describe_actual_result
,_inverted
,_falsy_exceptions
.
Better fit for straightforward inline usage#
Such combination of params is especially handy when you build conditions inline without the need to store and reuse them later, like in:
from selene import browser
from selene.core.condition import Match
input = browser.element('#field')
input.should(Match(
'normalized value',
by=lambda it: not re.find_all(r'(\s)+', it.locate().get_attribute(value)),
))
Note
In the example above, it is especially important
to pass the 'normalized value'
name explicitly,
because we pass the lambda function in place
of the by
predicate argument, and Selene can't autogenerate
the name for condition based on "anonymous" lambda function.
The name can be autogenerated only from: regular named function,
a callable object with custom __str__
implementation
(like Query(name, fn)
object).
Reusing Selene's predefined queries#
To simplify the code a bit, you can reuse the predefined Selene query inside the predicate definition:
from selene import browser, query
from selene.core.condition import Match
input = browser.element('#field')
input.should(Match(
'normalized value',
by=lambda element: not re.find_all(r'(\s)+', query.value(element)),
))
Or reusing query outside of predicate definition:
from selene import browser, query
from selene.core.condition import Match
input = browser.element('#field')
input.should(Match(
'normalized value',
actual=query.value,
by=lambda value: not re.find_all(r'(\s)+', value),
))
In such specific case you can pass actual as a 2nd positional param (not a keyword param):
from selene import browser, query
from selene.core.condition import Match
input = browser.element('#field')
input.should(Match(
'normalized value',
query.value,
by=lambda value: not re.find_all(r'(\s)+', value),
))
Optionality of name#
You can skip first name positional-only parameter to get a default name, that will be autogenerated based on passed query name:
from selene import browser, query
from selene.core.condition import Match
input = browser.element('#field')
input.should(Match(
actual=query.value,
by=Query('is normalized', lambda value: not re.find_all(r'(\s)+', value)),
))
Reusing custom queries and predicates#
So, if we already have somewhere a helper:
is_normalized = Query('is normalized', lambda value: not re.find_all(r'(\s)+', value))
# or even...
# (in case of regular named function the 'is normalized' name
# will be generated from function name):
def is_normalized(value):
return not re.find_all(r'(\s)+', value)
– then we can build a condition on the fly with reusable query-blocks like:
from selene import browser, query
from selene.core.condition import Match
input = browser.element('#field')
input.should(Match(actual=query.value, by=is_normalized))
Warning
Remember, that we can't use positional arguments for actual
and by
like in:
from selene import browser, query
from selene.core.condition import Match
browser.element('#field').should(Match(query.value, is_normalized)) # ❌
This example is not valid!
Yet it is pretty tempting syntax to have, so maybe we'll implement it in future versions, stay tuned;)
Parametrized predicates#
In case you need something "more parametrized":
from selene import browser, query
from selene.core.condition import Match
browser.element('#field').should(
Match(actual=query.value, by=has_no_pattern(r'(\s)+'))
)
– where:
When custom name over autogenerated#
– But now the autogenerated name (that you will see in error messages on fail) – may be too low level. Thus, you might want to provide more high level custom name:
from selene import browser, query
from selene.core.condition import Match
browser.element('#field').should(
Match('normalized value', query.value, by=has_no_pattern(r'(\s)+'))
)
Everything for your freedom of creativity;)
Refactoring conditions for reusability via extending#
At some point of time, you may actually become interested in reusing such custom conditions. Then you may refactor the last example to something like:
from selene import browser
from my_project_tests.extensions.selene import have
browser.element('#field').should(have.normalized_value)
– where:
# my_project_tests/extensions/selene/have.py
from selene.core.condition import Match
# To reuse all existing have.* conditions, thus, extending them:
from selene.support.conditions.have import *
normalized_value = Match(
'normalized value',
query.value,
by=has_no_pattern(r'(\s)+')
)
Find more complicated examples of conditions definition in Selene's match.* module.
Have fun! ;)