Skip to content

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:

def is_positive(x):
    if not x > 0:
        raise AssertionError(f'Expected positive number, but got {x}')

True|False-predicate-based definition#

Or in one-liner if we use ConditionMismatch factory method to build a condition from predicate:

is_positive = ConditionMismatch._to_raise_if_not(lambda x: x > 0)

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:

Storage.should(has_positive_number)

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:

browser.element('#save').should(be.enabled)

😉

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 and ConditionMismatch._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:

has_positive_number = ConditionMismatch._to_raise_if_not(is_positive, number)
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 use condition.and_(another_condition)
  • to compose conditions by logical or you use condition.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, a size condition can work with both "singular" and "plural" entities, then we could provide the name as lambda 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 if test arg was provided. If test arg was not provided, yet can be skipped, then it will be similar to passing lambda 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 if test was provided. You have to choose how to define a condition – via test or via by with optional actual.
  • _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. to describe_actual or describe) 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:

has_positive_number = Match(
    'has positive number',
    by=lambda entity: entity.number > 0,
)
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 superclass Condition 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: the by predicate and the optional actual query to transform an entity before passing to the predicate for match. Both by and actual 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 both name and by 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 and actual 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:

import re

def has_no_pattern(regex):
    return lambda actual: not re.find_all(regex, actual)
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! ;)