diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1cb662a6..7ca84de9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -105,35 +105,89 @@ TODOs:
`.have.exact_texts(1, 2.0, '3')` is now possible, and will be treated as `['1', '2.0', '3']`
-### list globs, text wildcards and regex support in texts_like conditions
+### regex support for element conditions that assert element text
-List of conditions added (still marked as experimental with `_` prefix):
+List of element conditions added:
-- `have._exact_texts_like(*exact_texts_or_list_globs: Union[str, int, float])`
-- `have._exact_texts_like(*exact_texts_or_list_globs: Union[str, int, float]).where(**globs_to_override)`
-- `have._texts_like(*contained_texts_or_list_globs: Union[str, int, float])`
-- `have._texts_like(*contained_texts_or_list_globs: Union[str, int, float]).where(**glob_to_override)`
-- `have._texts_like(*regex_patterns_or_list_globs: Union[str, int, float]).with_regex`
+- `have.text_matching(regex_pattern: str | int | float)`
+ - = `match.text_pattern(regex_pattern: str | int | float)`
+
+Examples of usage:
+
+```python
+from selene import browser, have
+
+...
+# GivenPage(browser.driver).opened_with_body(
+# '''
+#
Hello:
+# - 1) One!!!
+# - 2) Two...
+# - 3) Three???
+#
+# '''
+# )
+
+# in addition to:
+browser.all('li').first.should(have.text('One'))
+# this would be an alternative to previous match, but via regex:
+browser.all('li').first.should(have.text_matching(r'.*One.*'))
+# with more powerful features:
+browser.all('li').first.should(have.text_matching(r'\d\) One(.)\1\1'))
+# ^ and $ can be used but don't add much value, cause work same as previous
+browser.all('li').first.should(have.text_matching(r'^\d\) One(.)\1\1$'))
+
+# there is also a similar collection condition that
+# matches each pattern to each element text in the collection
+# in the corresponding order:
+browser.all('li').should(have.texts_matching(
+ r'\d\) One!+', r'.*', r'.*'
+))
+# that is also equivalent to:
+browser.all('li').should(have._texts_like(
+ r'\d\) One(.)\1\1', ..., ...
+).with_regex)
+# or even:
+browser.all('li').should(have._texts_like(
+ r'\d\) One(.)\1\1', (...,) # = one or more
+).with_regex)
+# And with smart approach you can mix to achieve more with less:
+browser.all('li')[:3].should(have.text_matching(
+ r'\d\) \w+(.)\1\1'
+).each)
+```
+
+### list globs, text wildcards and regex support in texts_like collection conditions
+
+List of collection conditions added (still marked as experimental with `_` prefix):
+
+- `have._exact_texts_like(*texts_or_item_placeholders: str | int | float)`
+- `have._exact_texts_like(*texts_or_item_placeholders: str | int | float).where(**placeholders_to_override)`
+- `have._texts_like(*contained_texts_or_item_placeholders: str | int | float)`
+- `have._texts_like(*contained_texts_or_item_placeholders: str | int | float).where(**placeholders_to_override)`
+- `have._texts_like(*regex_patterns_or_item_placeholders: str | int | float).with_regex`
- is an alias to `have._text_patterns_like`
-- `have._text_patterns(*regex_patterns).with_regex`
+- `have._text_patterns(*regex_patterns)`
- like `have.texts` but with regex patterns as expected, i.e. no list globs support
-- `have._texts_like(*texts_with_wildcards_or_list_globs: Union[str, int, float]).with_wildcards`
-- `have._texts_like(*texts_with_wildcards_or_list_globs: Union[str, int, float]).where_wildcards(**to_override)`
+- `have._texts_like(*texts_with_wildcards_or_item_placeholders: Union[str, int, float]).with_wildcards`
+- `have._texts_like(*texts_with_wildcards_or_item_placeholders: Union[str, int, float]).where_wildcards(**to_override)`
- corresponding `have.no.*` versions of same conditions
Where:
-- default list globs are:
+- default list glob placeholders are:
- `[...]` matches **zero or one** item of any text in the list
- `...` matches **exactly one** item of any text in the list
- `(...,)` matches one **or more** items of any text in the list
- `[(...,)]` matches **zero** or more items of any text in the list
-- all globs can be mixed in the same list of expected items in any order
+- all globbing placeholders can be mixed in the same list of expected items in any order
- regex patterns can't use `^` (start of text) and `$` (end of text)
because they are implicit, and if added explicitly will break the match
- supported wildcards can be overridden and defaults are:
- `*` matches **zero or more** of any characters in a text item
- `?` matches **exactly one** of any character in a text item
+- expected list items flattening is not supported like in `have.texts` and `have.exact_texts`
+ because `[]` are used in list globs. So, you can't use nested lists or tuples to format the expected list of items.
Warning:
@@ -160,7 +214,7 @@ browser.all('li').should(have._exact_texts_like(
'1) One!!!', '2) Two!!!', ..., ..., ... # = exactly one
))
browser.all('li').should(have._texts_like(
- '\d\) One!+', '\d.*', ..., ..., ...
+ r'\d\) One!+', r'\d.*', ..., ..., ...
).with_regex)
browser.all('li').should(have._texts_like(
'?) One*', '?) Two*', ..., ..., ...
@@ -296,6 +350,19 @@ Providing a brief overview of the modules and how to define your own custom comm
Just "autocomplete" is disabled, methods still work;)
+### Fix misleading absence of waiting in slicing behavior
+
+Now this will fail:
+
+```python
+from selene import browser, have
+...
+browser.all('.non-existing')[:1].should(have.text('something').each)
+```
+– and that's good, because we are identifying the expected number of elements in a slice.
+
+But before it would pass, that contradicted with other "get element by index" behavior:D
+
### Fix path of screenshot and pagesource for Windows
Thanks to [Cameron Shimmin](https://github.com/cshimm) and Edale Miguel for PR [#525](https://github.com/yashaka/selene/pull/525)
diff --git a/selene/core/entity.py b/selene/core/entity.py
index f7a613bc..93775abe 100644
--- a/selene/core/entity.py
+++ b/selene/core/entity.py
@@ -653,7 +653,7 @@ def __call__(self) -> typing.Sequence[WebElement]:
@property
def cached(self) -> Collection:
- webelements = self()
+ webelements = self.locate()
return Collection(Locator(f'{self}.cached', lambda: webelements), self.config)
def __iter__(self):
@@ -670,6 +670,8 @@ def __len__(self):
return self.get(query.size)
# TODO: add config.index_collection_from_1, disabled by default
+ # TODO: consider additional number param, that counts from 1
+ # if provided instead of index
def element(self, index: int) -> Element:
def find() -> WebElement:
webelements = self.locate()
@@ -720,7 +722,20 @@ def sliced(
step: int = 1,
) -> Collection:
def find() -> typing.Sequence[WebElement]:
- webelements = self()
+ webelements = self.locate()
+ length = len(webelements)
+ if start is not None and start != 0 and start >= length:
+ raise AssertionError(
+ f'not enough elements to slice collection '
+ f'from START on index={start}, '
+ f'actual elements collection length is {length}'
+ )
+ if stop is not None and stop != -1 and length < stop:
+ raise AssertionError(
+ 'not enough elements to slice collection '
+ f'from {start or "START"} to STOP at index={stop}, '
+ f'actual elements collection length is {length}'
+ )
# TODO: assert length according to provided start, stop...
@@ -758,7 +773,7 @@ def by(
condition = (
condition
if isinstance(condition, Condition)
- else Condition(str(condition), condition)
+ else Condition(str(condition), condition) # TODO: check here for fn name
)
return Collection(
diff --git a/selene/core/match.py b/selene/core/match.py
index f6e8146d..a5c272a4 100644
--- a/selene/core/match.py
+++ b/selene/core/match.py
@@ -105,6 +105,14 @@ def element_has_exact_text(expected: str) -> Condition[Element]:
return element_has_text(expected, 'has exact text', predicate.equals)
+def text_pattern(expected: str) -> Condition[Element]:
+ return ElementCondition.raise_if_not_actual(
+ f'has text matching {expected}',
+ query.text,
+ predicate.matches(expected),
+ )
+
+
def element_has_js_property(name: str):
# TODO: should we keep simpler but less obvious name - *_has_property ?
def property_value(element: Element):
@@ -398,7 +406,7 @@ def actual_visible_texts(collection: Collection) -> List[str]:
return CollectionCondition.raise_if_not_actual(
f'has texts {expected_}',
- actual_visible_texts,
+ Query('visible texts', actual_visible_texts),
predicate.equals_by_contains_to_list(expected_),
)
@@ -816,6 +824,11 @@ def __init__(
# TODO: add an alias from texts(*expected).with_regex to text_patterns_like
+# hm, but then it would be natural
+# if we disable implicit ^ and $ for each item text
+# and so we make it inconsistent with the behavior of *_like versions
+# then probably we should explicitly document that we are not going
+# to add such type of condition at all
class _text_patterns(_text_patterns_like):
"""Condition to match visible texts of all elements in a collection
with supported item placeholders to include/exclude items from match
@@ -836,7 +849,7 @@ def __init__(
_name='text patterns',
): # noqa
super().__init__(
- *expected,
+ *helpers.flatten(expected), # TODO: document
_process_patterns=_process_patterns,
_negated=_negated,
_name_prefix=_name_prefix,
@@ -845,6 +858,11 @@ def __init__(
# disable globs (doing after __init__ to override defaults)
self._globs = ()
+ # TODO: consider refactoring so this attribute is not even inherited
+ def where(self):
+ """Just a placeholder. This attribute is not supported for this condition"""
+ raise AttributeError('.where(**) is not supported on text_patterns condition')
+
# TODO: can and should we disable here the .where method?
# shouldn't we just simply implement it in a straightforward style
# similar to match.exact_texts?
diff --git a/selene/support/conditions/have.py b/selene/support/conditions/have.py
index a25d8d29..90313a90 100644
--- a/selene/support/conditions/have.py
+++ b/selene/support/conditions/have.py
@@ -32,15 +32,19 @@
no = _not_
-def exact_text(value) -> Condition[Element]:
+def exact_text(value: str) -> Condition[Element]:
return match.element_has_exact_text(value)
# TODO: consider accepting int
-def text(partial_value) -> Condition[Element]:
+def text(partial_value: str) -> Condition[Element]:
return match.element_has_text(partial_value)
+def text_matching(regex_pattern: str) -> Condition[Element]:
+ return match.text_pattern(regex_pattern)
+
+
# TODO: should we use here js.property style (and below for js.returned(...))
def js_property(name: str, value: Optional[str] = None):
if value:
@@ -137,7 +141,7 @@ def size_greater_than_or_equal(number: int) -> Condition[Collection]:
# TODO: consider accepting ints
-def texts(*partial_values: Union[str, Iterable[str]]) -> Condition[Collection]:
+def texts(*partial_values: str | Iterable[str]) -> Condition[Collection]:
return match.collection_has_texts(*partial_values)
@@ -145,20 +149,119 @@ def exact_texts(*values: str | int | float | Iterable[str]):
return match.collection_has_exact_texts(*values)
-def _exact_texts_like(*values: str | int | float | Iterable):
- return match._exact_texts_like(*values)
+def _exact_texts_like(*texts_or_item_placeholders: str | int | float | Iterable):
+ """List-globbing version of
+ [have.exact_texts(*texts)][selene.support.conditions.have.exact_texts]
+ allowing to use item placeholders instead of text items.
+
+ Default list globbing placeholders are:
+
+ - `[...]` matches **zero or one** item of any text in the list
+ - `...` matches **exactly one** item of any text in the list
+ - `(...,)` matches one **or more** items of any text in the list
+ - `[(...,)]` matches **zero** or more items of any text in the list
+
+ Placeholders can be overridden in the following manner:
+ `have._texts_like(*text_items_or_placeholders).where(**placeholders_to_override)`
+
+ Nested lists with text items for better formatting of expected texts –
+ are not supported, unlike in `have.exact_texts(*items)`,
+ because list literals are used as placeholders for list globbing."""
+ return match._exact_texts_like(*texts_or_item_placeholders)
+
+
+# could be named as texts_matching_like
+# but seems like "matching like" confuses too much...
+# yet, we want to keep _like suffix
+# as identifier of "globbing" nature of the list match
+def _text_patterns_like(
+ *regex_patterns_or_item_placeholders: str | int | float | Iterable,
+):
+ """List-globbing version of
+ [have.texts_matching(*regex_patterns)][selene.support.conditions.have.texts_matching]
+ allowing to use item placeholders instead of text items.
+
+ Default list globbing placeholders are:
+
+ - `[...]` matches **zero or one** item of any text in the list
+ - `...` matches **exactly one** item of any text in the list
+ - `(...,)` matches one **or more** items of any text in the list
+ - `[(...,)]` matches **zero** or more items of any text in the list
+
+ Placeholders can be overridden in the following manner:
+ `have._texts_like(*text_items_or_placeholders).where(**placeholders_to_override)`
+
+ !!! warning
+
+ Nested lists with text items for better formatting of expected texts –
+ are not supported,
+ unlike in [`have.texts(*texts)`][selene.support.conditions.have.texts],
+ because list literals are used as placeholders for list globbing.
+
+ !!! warning
+
+ Unlike in [`have.texts_matching(*regex_patterns)`][selene.support.conditions.have.texts_matching],
+ regex patterns for this condition
+ can't use `^` (start of text) and `$` (end of text),
+ because they are implicit as a result of merging for globbing implementation,
+ and if added explicitly will break the match.
+ """
+ return match._text_patterns_like(*regex_patterns_or_item_placeholders)
+
+
+def texts_matching(*regex_patterns: str | int | float | Iterable):
+ """Regex version of [have.texts(*partial_values)][selene.support.conditions.have.texts]
+ allowing to use regex patterns instead of text items matched by contains.
+ """
+ return match._text_patterns(*regex_patterns)
+
+
+def _texts_like(*contained_texts_or_item_placeholders: str | int | float | Iterable):
+ """List-globbing version of [have.texts(*partial_values)][selene.support.conditions.have.texts]
+ allowing to use item placeholders instead of text items.
+
+ Default list globbing placeholders are:
+
+ - `[...]` matches **zero or one** item of any text in the list
+ - `...` matches **exactly one** item of any text in the list
+ - `(...,)` matches one **or more** items of any text in the list
+ - `[(...,)]` matches **zero** or more items of any text in the list
+
+ Placeholders can be overridden in the following manner:
+ `have._texts_like(*text_items_or_placeholders).where(**placeholders_to_override)`
+
+ !!! warning
+
+ Nested lists with text items for better formatting of expected texts –
+ are not supported, unlike in
+ [`have.texts(*texts)`][selene.support.conditions.have.texts],
+ because list literals are used as placeholders for list globbing.
+ Text items are matched by contains, but can be matched by regex patterns
+ if modified via `.with_regex` property making the actual signature be equivalent to
+ `have._texts_like(*regex_patterns_or_item_placeholders).with_regex`.
+ Actually calling `.with_regex` just forward implementation to
+ [have._text_patterns_like(*regex_patterns_or_item_placeholders)][selene.support.conditions.have._text_patterns_like].
-def _text_patterns_like(*values: str | int | float | Iterable):
- return match._text_patterns_like(*values)
+ !!! warning
+ Unlike in [`have.texts_matching(*regex_patterns)`][selene.support.conditions.have.texts_matching],
+ Regex patterns can't use `^` (start of text) and `$` (end of text)
+ because they are implicit, and if added explicitly will break the match.
-def _text_patterns(*values: str | int | float | Iterable):
- return match._text_patterns(*values)
+ If modified via `.with_wildcards`
+ then switch regex to wildcards-based pattern matching,
+ making the actual signature be equivalent to:
+ `have._texts_like(*texts_with_wildcards_or_item_placeholders).with_wildcards`
+ or
+ `have._texts_like(*texts_with_wildcards_or_item_placeholders).where_wildcards(**to_override)`
+ Supported wildcards can be overridden and defaults are:
-def _texts_like(*values: str | int | float | Iterable):
- return match._texts_like(*values)
+ - `*` matches **zero or more** of any characters in a text item
+ - `?` matches **exactly one** of any character in a text item
+ """
+ return match._texts_like(*contained_texts_or_item_placeholders)
def url(exact_value: str) -> Condition[Browser]:
diff --git a/selene/support/conditions/not_.py b/selene/support/conditions/not_.py
index 074c466d..8659586f 100644
--- a/selene/support/conditions/not_.py
+++ b/selene/support/conditions/not_.py
@@ -62,6 +62,10 @@ def text(partial_value) -> Condition[Element]:
return _match.element_has_text(partial_value).not_
+def text_matching(regex_pattern) -> Condition[Element]:
+ return _match.text_pattern(regex_pattern).not_
+
+
def attribute(name: str, *args, **kwargs):
if args or 'value' in kwargs:
warnings.warn(
@@ -230,20 +234,22 @@ def exact_texts(*values: str | int | float | Iterable[str]):
return _match.collection_has_exact_texts(*values).not_
-def _exact_texts_like(*values: str | int | float | Iterable):
- return _match._exact_texts_like(*values).not_
+def _exact_texts_like(*texts_or_item_placeholders: str | int | float | Iterable):
+ return _match._exact_texts_like(*texts_or_item_placeholders).not_
-def _text_patterns_like(*values: str | int | float | Iterable):
- return _match._text_patterns_like(*values).not_
+def _text_patterns_like(
+ *regex_patterns_or_item_placeholders: str | int | float | Iterable,
+):
+ return _match._text_patterns_like(*regex_patterns_or_item_placeholders).not_
-def _text_patterns(*values: str | int | float | Iterable):
- return _match._text_patterns(*values).not_
+def _texts_matching(*regex_patterns: str | int | float | Iterable):
+ return _match._text_patterns(*regex_patterns).not_
-def _texts_like(*values: str | int | float | Iterable):
- return _match._texts_like(*values).not_
+def _texts_like(*contained_texts_or_item_placeholders: str | int | float | Iterable):
+ return _match._texts_like(*contained_texts_or_item_placeholders).not_
def url(exact_value: str) -> Condition[Browser]:
diff --git a/tests/integration/condition__collection__have_texts_like__with_items_globs_regex_patterns_and_wildcards_test.py b/tests/integration/condition__collection__have_texts_like__with_items_globs_regex_patterns_and_wildcards_test.py
index 564ab1c2..fb552e2c 100644
--- a/tests/integration/condition__collection__have_texts_like__with_items_globs_regex_patterns_and_wildcards_test.py
+++ b/tests/integration/condition__collection__have_texts_like__with_items_globs_regex_patterns_and_wildcards_test.py
@@ -123,7 +123,7 @@ def test_text_patterns_like__mixed__with_regex_patterns_support(
)
# with alias
browser.all('li').should(
- have._text_patterns(
+ have.texts_matching(
r'.*?O.e.*?',
r'2\) Two\.\.\.',
r'.*?Thr.+.*?',
@@ -219,7 +219,7 @@ def test_text_patternss_like__mixed__with_regex_patterns_support__error_messages
# without "_like" version will lack support of ellipsis globs as items placeholders
try:
browser.all('li').should(
- have._text_patterns(
+ have.texts_matching(
r'^.*?O.e.*?$', # fails on syntax: '^' and '$' should be implicit
r'2\) Two\.\.\.',
r'.*?Thr.+.*?',
@@ -238,7 +238,7 @@ def test_text_patternss_like__mixed__with_regex_patterns_support__error_messages
try:
browser.all('li').should(
- have._text_patterns(
+ have.texts_matching(
r'.*?O.e.*?',
r'2\) Two\.\.\.',
r'.*?Thr.+.*?',
diff --git a/tests/integration/condition__collection__have_texts_test.py b/tests/integration/condition__collection__have_texts_test.py
index c4609612..aa4b8936 100644
--- a/tests/integration/condition__collection__have_texts_test.py
+++ b/tests/integration/condition__collection__have_texts_test.py
@@ -60,7 +60,7 @@ def test_should_have_texts_exception(session_browser):
assert (
"browser.all(('css selector', 'li')).has texts ('Alex',)\n"
'\n'
- "Reason: AssertionError: actual actual_visible_texts: ['Alex', 'Yakov']\n"
+ "Reason: AssertionError: actual visible texts: ['Alex', 'Yakov']\n"
) in str(error)
diff --git a/tests/integration/condition__element__have_text_matching__compared_test.py b/tests/integration/condition__element__have_text_matching__compared_test.py
new file mode 100644
index 00000000..837d39e6
--- /dev/null
+++ b/tests/integration/condition__element__have_text_matching__compared_test.py
@@ -0,0 +1,113 @@
+# MIT License
+#
+# Copyright (c) 2015-2022 Iakiv Kramarenko
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import pytest
+from selene import have
+from selene.core import match
+from tests.integration.helpers.givenpage import GivenPage
+
+# TODO: review tests: clean up, add more cases if needed, break down into smaller tests,
+# find better names for tests
+
+
+def test_text_matching__regex_pattern__compared(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One!!!
+ - 2) Two...
+ - 3) Three???
+
+ '''
+ )
+
+ # in addition to:
+ browser.all('li').first.should(have.text('One'))
+ # this would be an alternative to previous match, but via regex:
+ browser.all('li').first.should(have.text_matching(r'.*One.*'))
+ # or
+ browser.all('li').first.should(match.text_pattern(r'.*One.*'))
+ # with matching whole text not just part (kind of implicit ^ and $):
+ browser.all('li').first.should(have.no.text_matching(r'One'))
+ # or
+ browser.all('li').first.should(match.text_pattern(r'One').not_)
+ # With regular regex powerful features:
+ browser.all('li').first.should(have.text_matching(r'\d\) One(.)\1\1'))
+ # ^ and $ can be used but don't add much value, cause work same as previous
+ browser.all('li').first.should(have.text_matching(r'^\d\) One(.)\1\1$'))
+
+ # there is also a similar collection condition that
+ # matches each pattern to each element text in the collection
+ # in the corresponding order:
+ browser.all('li').should(have.texts_matching(r'\d\) One!+', r'.*', r'.*'))
+ # that is also equivalent to:
+ browser.all('li').should(have._texts_like(r'\d\) One(.)\1\1', ..., ...).with_regex)
+ # or even:
+ browser.all('li').should(
+ have._texts_like(r'\d\) One(.)\1\1', (...,)).with_regex # = one or more
+ )
+ # And with smart approach you can mix to achieve more with less:
+ browser.all('li')[:3].should(have.text_matching(r'\d\) \w+(.)\1\1').each)
+
+
+def test_text_matching__regex_pattern__error_message(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One!!!
+ - 2) Two...
+ - 3) Three???
+
+ '''
+ )
+
+ try:
+ browser.all('li').first.should(have.text_matching(r'\d\) ONE(.)\1\1'))
+ pytest.fail('expected text mismatch')
+ except AssertionError as error:
+ assert (
+ 'Timed out after 0.1s, while waiting for:\n'
+ "browser.all(('css selector', 'li'))[0].has text matching \\d\\) "
+ 'ONE(.)\\1\\1\n'
+ '\n'
+ 'Reason: AssertionError: actual text: 1) One!!!\n'
+ 'Screenshot: '
+ ) in str(error)
+
+ try:
+ browser.all('li').first.should(have.no.text_matching(r'\d\) One(.)\1\1'))
+ pytest.fail('expected text match')
+ except AssertionError as error:
+ assert (
+ 'Timed out after 0.1s, while waiting for:\n'
+ "browser.all(('css selector', 'li'))[0].has no (text matching \\d\\) "
+ 'One(.)\\1\\1)\n'
+ '\n'
+ 'Reason: ConditionNotMatchedError: condition not matched\n'
+ 'Screenshot: '
+ ) in str(error)
diff --git a/tests/integration/condition_each_test.py b/tests/integration/condition_each_test.py
index 692689e2..07df0869 100644
--- a/tests/integration/condition_each_test.py
+++ b/tests/integration/condition_each_test.py
@@ -19,6 +19,8 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+import pytest
+
from selene import have
from selene.core.condition import Condition
from selene.core.exceptions import TimeoutException
@@ -75,3 +77,52 @@ def test_on_collection(session_browser):
"Not matched elements among all with indexes from 0 to 2:\n"
"browser.all(('css selector', 'li')).cached[1]: condition not matched"
) in str(error)
+
+
+def test_on_collection_with_expected_size(session_browser):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Welcome to:
+ - Harry from Hogwarts
+ - Ron from Hogwarts
+ - Hermione from Hogwarts
+
+ '''
+ )
+
+ # WHEN
+ elements = browser.all('p') # the size of elements collection is 0
+
+ # THEN this passes, because each among 0 - technically has 'from Hogwarts' :D
+ elements.should(have.text('from Hogwarts').each)
+ # AND all these too, because the STOP index is implicit so assume 0 length too
+ elements[:].should(have.text('from Hogwarts').each)
+ elements[0:].should(have.text('from Hogwarts').each)
+ elements[0:-1].should(have.text('from Hogwarts').each)
+
+ # BUT this DOES NOT:
+ try:
+ elements[:3].should(have.text('from Hogwarts').each)
+ pytest.fail("should have failed on size mismatch")
+ except TimeoutException as error:
+ assert (
+ "browser.all(('css selector', 'p'))[:3]. each has text from Hogwarts\n"
+ '\n'
+ 'Reason: AssertionError: not enough elements to slice collection from START '
+ 'to STOP at index=3, actual elements collection length is 0\n'
+ ) in str(error)
+
+ # AND while this pass
+ browser.all('li')[2:].should(have.text('from Hogwarts').each)
+ # BUT this DOES NOT too:
+ try:
+ browser.all('li')[3:].should(have.text('from Hogwarts').each)
+ pytest.fail("should have failed on size mismatch")
+ except TimeoutException as error:
+ assert (
+ "browser.all(('css selector', 'li'))[3:]. each has text from Hogwarts\n"
+ '\n'
+ 'Reason: AssertionError: not enough elements to slice collection from START '
+ 'on index=3, actual elements collection length is 3\n'
+ ) in str(error)
diff --git a/tests/integration/shared_browser/browser__config__wait_decorator_with_decorator_from_support_logging_test.py b/tests/integration/shared_browser/browser__config__wait_decorator_with_decorator_from_support_logging_test.py
index f5c81e76..1fbaed36 100644
--- a/tests/integration/shared_browser/browser__config__wait_decorator_with_decorator_from_support_logging_test.py
+++ b/tests/integration/shared_browser/browser__config__wait_decorator_with_decorator_from_support_logging_test.py
@@ -98,9 +98,9 @@ def test_logging_via__wait_decorator(quit_shared_browser_afterwards):
Message:\u0020
Timed out after 0.3s, while waiting for:
-browser.all(('css selector', '#todo-list>li')).has texts ['a', 'b', 'c']
+browser.all(('css selector', '#todo-list>li')).has texts ('a', 'b', 'c')
-Reason: AssertionError: actual visible texts: ['a', 'c']
+Reason: AssertionError: actual visible texts: ['a', 'c']\n
'''.strip()
in handler.stream
)