diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb2848ec..1cb662a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -99,7 +99,109 @@ TODOs:
- can we force order of how `selene.*` is rendered on autocomplete? via `__all__`...
- deprecate `have.js_returned` in favour of `have.script_returned`
-## 2.0.0rc10 (to be released on DD.05.2024)
+## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024)
+
+### texts like conditions now accepts int and floats as text item
+
+`.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
+
+List of conditions added (still marked as experimental with `_` prefix):
+
+- `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`
+ - is an alias to `have._text_patterns_like`
+- `have._text_patterns(*regex_patterns).with_regex`
+ - 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)`
+- corresponding `have.no.*` versions of same conditions
+
+Where:
+
+- default list globs 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
+- 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
+
+Warning:
+
+- Actual implementation does not compare each list item separately, it merges all expected items into one regex pattern and matches it with merged text of all visible elements collection texts, and so it may be tricky to analyze the error message in case of failure. To keep life simpler, try to reduce the usage of such conditions to the simplest cases, preferring wildcards to regex patterns, trying even to avoid wildcards if possible, in the perfect end, sticking just to `exact_texts_like` or `texts_like` conditions with only one explicitly (for readability) customized list glob, choosing `...` as the simplest glob placeholder, for example: `browser.all('li').should(have._exact_texts_like(1, 2, 'Three', ...).where(one_or_more=...))` to assert actual texts `
12Three45` in the list.
+
+Examples of usage:
+
+```python
+from selene import browser, have
+...
+# GivenPage(browser.driver).opened_with_body(
+# '''
+# Hello:
+# - 1) One!!!
+# - 2) Two!!!
+# - 3) Three!!!
+# - 4) Four!!!
+# - 5) Five!!!
+#
+# '''
+# )
+
+browser.all('li').should(have._exact_texts_like(
+ '1) One!!!', '2) Two!!!', ..., ..., ... # = exactly one
+))
+browser.all('li').should(have._texts_like(
+ '\d\) One!+', '\d.*', ..., ..., ...
+).with_regex)
+browser.all('li').should(have._texts_like(
+ '?) One*', '?) Two*', ..., ..., ...
+).with_wildcards)
+browser.all('li').should(have._texts_like(
+ '_) One**', '_) Two*', ..., ..., ...
+).where_wildcards(zero_or_more='**', exactly_one='_'))
+browser.all('li').should(have._texts_like(
+ 'One', 'Two', ..., ..., ... # matches each text by contains
+)) # kind of "with implicit * wildcards" in the beginning and the end of each text
+
+
+browser.all('li').should(have._texts_like(
+ ..., ..., ..., 'Four', 'Five'
+))
+browser.all('li').should(have._texts_like(
+ 'One', ..., ..., 'Four', 'Five'
+))
+
+browser.all('li').should(have._texts_like(
+ 'One', 'Two', (..., ) # = one or more
+))
+browser.all('li').should(have._texts_like(
+ [(..., )], 'One', 'Two', [(..., )] # = ZERO or more ;)
+))
+browser.all('li').should(have._texts_like(
+ [...,], 'One', 'Two', 'Three', 'Four', [...] # = zero or ONE ;)
+))
+
+# If you don't need so much "globs"...
+# (here goes, actually, the 💡RECOMMENDED💡 way to use it in most cases...
+# to keep things simpler for easier support and more explicit for readability)
+# – you can use the simplest glob item with explicitly customized meaning:
+browser.all('li').should(have._exact_texts_like(
+ 'One', 'Two', ... # = one OR MORE
+).where(one_or_more=...)) # – because the ... meaning was overridden
+# Same works for other conditions that end with `_like`
+browser.all('li').should(have._exact_texts_like(
+ '1) One!!!', '2) Two!!!', ...
+).where(one_or_more=...))
+```
### Shadow DOM support via query.js.shadow_root(s)
diff --git a/poetry.lock b/poetry.lock
index 9312091f..a14c4a79 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1775,4 +1775,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
-content-hash = "debca08a00f0be727f4a7d40ab707c1da754c1c1bdfbb0afd92b6a75b32a43c4"
+content-hash = "d642bbe5e32ede7cae8856ef232cf5db474062d1e6509372b6316763b0a316f4"
diff --git a/pyproject.toml b/pyproject.toml
index 4b9b01ed..89764871 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,7 @@ Changelog = "https://github.com/yashaka/selene/releases"
python = "^3.8"
selenium = ">=4.12.0"
future = "*"
-typing-extensions = ">=4.9.0"
+typing-extensions = ">=4.11.0"
[tool.poetry.dev-dependencies]
black = "^24.2.0"
diff --git a/selene/common/predicate.py b/selene/common/predicate.py
index ff25dfa2..54ff3b8d 100644
--- a/selene/common/predicate.py
+++ b/selene/common/predicate.py
@@ -53,6 +53,10 @@ def is_less_than_or_equal(expected):
return lambda actual: actual <= expected
+def matches(pattern):
+ return lambda actual: re.match(pattern, str(actual))
+
+
def includes_ignoring_case(expected):
return lambda actual: str(expected).lower() in str(actual).lower()
diff --git a/selene/core/condition.py b/selene/core/condition.py
index 19e48eec..35566521 100644
--- a/selene/core/condition.py
+++ b/selene/core/condition.py
@@ -147,7 +147,15 @@ def fn(entity: E) -> None:
return cls(description, fn)
- def __init__(self, description: str, fn: Lambda[E, None]):
+ # TODO: should we make the description type as Callable[[Condition], str]
+ # instead of Callable[[], str]...
+ # to be able to pass condition itself...
+ # when we pass in child classes we pass self.__str__
+ # that doesn't need to receive self, it already has it
+ # but what if we want to pass some crazy lambda for description from outside
+ # to kind of providing a "description self-based strategy" for condition?
+ # maybe at least we can define it as varagrs? like Callable[..., str]
+ def __init__(self, description: str | Callable[[], str], fn: Lambda[E, None]):
self._description = description
self._fn = fn
@@ -177,7 +185,7 @@ def __str__(self):
# TODO: consider changing has to have on the fly for CollectionConditions
# TODO: or changing in collection locator rendering `all` to `collection`
# TODO: or changing in match.* names from collection_has_* to all_have_*
- return self._description
+ return self._description() if callable(self._description) else self._description
def and_(self, condition: Condition[E]) -> Condition[E]:
return Condition.by_and(self, condition)
diff --git a/selene/core/match.py b/selene/core/match.py
index 64c1329e..f6e8146d 100644
--- a/selene/core/match.py
+++ b/selene/core/match.py
@@ -19,8 +19,31 @@
# 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.
+from __future__ import annotations
+
+import re
import warnings
-from typing import List, Any, Union, Iterable
+from functools import reduce
+
+from typing_extensions import (
+ List,
+ Any,
+ Union,
+ Iterable,
+ Tuple,
+ Unpack,
+ TypedDict,
+ overload,
+ NotRequired,
+ cast,
+ Literal,
+ Dict,
+ override,
+ Callable,
+ AnyStr,
+ TypeVar,
+ Self,
+)
from selene.common import predicate, helpers
from selene.core import query
@@ -32,6 +55,7 @@
)
from selene.core.entity import Collection, Element
from selene.core._browser import Browser
+from selene.core.wait import Query
# TODO: consider moving to selene.match.element.is_visible, etc...
element_is_visible: Condition[Element] = ElementCondition.raise_if_not(
@@ -83,10 +107,10 @@ def element_has_exact_text(expected: str) -> Condition[Element]:
def element_has_js_property(name: str):
# TODO: should we keep simpler but less obvious name - *_has_property ?
- def property_value(element: Element) -> str:
- return element().get_property(name)
+ def property_value(element: Element):
+ return element.locate().get_property(name)
- def property_values(collection: Collection) -> List[str]:
+ def property_values(collection: Collection):
return [element.get_property(name) for element in collection()]
raw_property_condition = ElementCondition.raise_if_not_actual(
@@ -181,10 +205,10 @@ def values_containing(
def element_has_attribute(name: str):
- def attribute_value(element: Element) -> str:
- return element().get_attribute(name)
+ def attribute_value(element: Element):
+ return element.locate().get_attribute(name)
- def attribute_values(collection: Collection) -> List[str]:
+ def attribute_values(collection: Collection):
return [element.get_attribute(name) for element in collection()]
raw_attribute_condition = ElementCondition.raise_if_not_actual(
@@ -270,8 +294,8 @@ def collection_has_values_containing(
def element_has_css_class(expected: str) -> Condition[Element]:
- def class_attribute_value(element: Element) -> str:
- return element().get_attribute('class')
+ def class_attribute_value(element: Element):
+ return element.locate().get_attribute('class')
return ElementCondition.raise_if_not_actual(
f"has css class '{expected}'",
@@ -367,34 +391,565 @@ def collection_has_size_less_than_or_equal(
def collection_has_texts(*expected: Union[str, Iterable[str]]) -> Condition[Collection]:
expected_ = helpers.flatten(expected)
- def visible_texts(collection: Collection) -> List[str]:
+ def actual_visible_texts(collection: Collection) -> List[str]:
return [
webelement.text for webelement in collection() if webelement.is_displayed()
]
return CollectionCondition.raise_if_not_actual(
f'has texts {expected_}',
- visible_texts,
+ actual_visible_texts,
predicate.equals_by_contains_to_list(expected_),
)
def collection_has_exact_texts(
- *expected: Union[str, Iterable[str]]
-) -> Condition[Collection]:
- expected_ = helpers.flatten(expected)
-
- def visible_texts(collection: Collection) -> List[str]:
- return [
+ *expected: str | int | float | Iterable[str],
+):
+ if ... in expected: # TODO count other cases
+ raise ValueError(
+ '... is not allowed in exact_texts for "globbing"'
+ 'use _exact_texts_like condition instead'
+ )
+
+ actual_visible_texts: Query[Collection, List[str]] = Query(
+ 'visible texts',
+ lambda collection: [
webelement.text for webelement in collection() if webelement.is_displayed()
- ]
+ ],
+ )
+ # flatten expected values and convert numbers to strings
+ expected_flattened_stringified = [str(item) for item in helpers.flatten(expected)]
return CollectionCondition.raise_if_not_actual(
- f'has exact texts {expected_}',
- visible_texts,
- predicate.equals_to_list(expected_),
+ # TODO: should we use just expected here ↙ ?
+ f'has exact texts {expected_flattened_stringified}',
+ actual_visible_texts,
+ predicate.equals_to_list(expected_flattened_stringified),
+ )
+
+
+# TODO: consider implementing a mixture of exact_texts and exact_texts_like
+# with syntax: exact_texts(1, 2, 3, 4) + exact_texts.like(1, ..., 4)
+# def __exact_texts(
+# *expected: str | int | float | Iterable[str],
+# ):
+# if ... in expected: # TODO count other cases
+# raise ValueError(
+# '... is not allowed in exact_texts for "globbing"'
+# 'use exact_texts._like condition instead'
+# )
+#
+# actual_visible_texts: Query[Collection, List[str]] = Query(
+# 'visible texts',
+# lambda collection: [
+# webelement.text for webelement in collection() if webelement.is_displayed()
+# ],
+# )
+#
+# def build_raw_exact_texts(expected):
+# # flatten expected values and convert numbers to strings
+# expected_flattened_stringified = [
+# str(item) for item in helpers.flatten(expected)
+# ]
+# return CollectionCondition.raise_if_not_actual(
+# # TODO: should we use just expected here ↙ ?
+# f'has exact texts {expected_flattened_stringified}',
+# actual_visible_texts,
+# predicate.equals_to_list(expected_flattened_stringified),
+# )
+#
+# class CollectionHasExactTextsWithGlobbing(CollectionCondition):
+#
+# def _like(
+# self, *expected: str | int | float | Iterable
+# ) -> Condition[Collection]:
+# if ... not in expected: # TODO: adapt to different types of blobs
+# return build_raw_exact_texts(expected)
+#
+# expected_pattern = re.sub(
+# r'Ellipsis(,|\\])',
+# r'.+?\1',
+# (
+# r'^'
+# + re.escape(
+# str(
+# [
+# (str(item) if item is not ... else item)
+# for item in expected
+# ]
+# )
+# )
+# + r'$'
+# ),
+# )
+#
+# actual_visible_text: Query[Collection, str] = Query(
+# 'visible texts',
+# lambda collection: str(actual_visible_texts(collection)),
+# )
+#
+# return CollectionCondition.raise_if_not_actual(
+# re.sub(
+# r"'\.\.\.'([,\]])",
+# r"...\1",
+# f'has exact texts like {[value if value is not ... else r"..." for value in expected]}',
+# ),
+# actual_visible_text,
+# predicate.matches(expected_pattern),
+# )
+#
+# return CollectionHasExactTextsWithGlobbing(
+# str(build_raw_exact_texts(expected)),
+# build_raw_exact_texts(expected).call,
+# )
+
+
+class _exact_texts_like(CollectionCondition):
+ """Condition to match visible texts of all elements in a collection
+ with supported list globs for items (item placeholders
+ to include/exclude items from match).
+ """
+
+ _MATCHING_SEPARATOR = '‚' # it's not a regular ',', it's a unicode version;)
+ """A separator to be used while matching
+ to separate texts of different elements in a collection.
+
+ Should be quite unique to not interfere with actual texts characters.
+ Otherwise, will brake the match.
+
+ Should be a one character string,
+ because is used in predefined pattern for "exactly one" globbing
+ that might not work correctly if there will be more than one character.
+ """
+ _MATCHING_EMPTY_STRING_MARKER = '‹EMTPY_STRING›'
+ _RENDERING_SEPARATOR = ', '
+ _RENDERING_TRANSLATIONS = (
+ (..., '...'),
+ ([...], '[...]'),
+ ((...,), '(...,)'),
+ ([(...,)], '[(...,)])'),
+ )
+
+ _PredefinedPatternType = Literal[
+ 'exactly_one', 'zero_or_one', 'one_or_more', 'zero_or_more'
+ ]
+
+ # TODO: consider to redefine on self (or other options),
+ # to get fresh version of _MATCHING_SEPARATOR if it was patched
+ _PredefinedGlobPatterns: Dict[_PredefinedPatternType, str] = dict(
+ # TODO: ensure correctness of patterns
+ exactly_one=r'[^' + _MATCHING_SEPARATOR + r']+',
+ zero_or_one=r'[^' + _MATCHING_SEPARATOR + r']*',
+ one_or_more=r'.+?',
+ zero_or_more=r'.*?',
)
+ _DEFAULT_GLOBS: Tuple[Tuple[Any, str], ...] = (
+ (..., _PredefinedGlobPatterns['exactly_one']),
+ ([...], _PredefinedGlobPatterns['zero_or_one']),
+ ((...,), _PredefinedGlobPatterns['one_or_more']),
+ ([(...,)], _PredefinedGlobPatterns['zero_or_more']),
+ )
+
+ def __init__(
+ self,
+ *expected: str | int | float | Iterable,
+ _negated=False,
+ _globs: Tuple[Tuple[Any, str], ...] = (),
+ _name_prefix: str = 'have',
+ _name: str = 'exact texts like',
+ ): # noqa
+ if self._MATCHING_SEPARATOR.__len__() != 1:
+ raise ValueError('MATCHING_SEPARATOR should be a one character string')
+ super().__init__(self.__str__, self.__call__)
+ self._expected = expected
+ self._negated = _negated
+ self._globs = _globs if _globs else _exact_texts_like._DEFAULT_GLOBS
+ self._name_prefix = _name_prefix
+ self._name = _name
+ # actually disabling any patterns, processing as a normal string
+ self._process_patterns: Callable[[AnyStr], AnyStr] = (
+ re.escape
+ ) # HARDCODED by intent
+
+ # @overload
+ # def where(self, *globs: Tuple[Any, str]) -> _exact_texts_like: ...
+ # """
+ # Original idea was to give a possibility to customize globs in the most free way
+ # by specifying both markers and patterns,
+ # but it happened that implementation of matching logic is over-complicated
+ # and tightly coupled with chosen patterns...
+ # Thus, we removed this possibility for now, and the only way to customize globs
+ # is to define markers per corresponding predefined pattern.
+ # """
+ # @overload
+ # def where(
+ # self,
+ # *,
+ # exactly_one: Any = None,
+ # zero_or_one: Any = None,
+ # one_or_more: Any = None,
+ # zero_or_more: Any = None,
+ # ) -> _exact_texts_like: ...
+ # def where(self, *globs: Tuple[Any, str], **kwargs) -> _exact_texts_like:
+ # kwargs = cast(
+ # Dict[_exact_texts_like._PredefinedPatternType, Any],
+ # kwargs,
+ # )
+ # ... # here was actual original implementation
+
+ def where(
+ self,
+ *,
+ exactly_one: Any = None,
+ zero_or_one: Any = None,
+ one_or_more: Any = None,
+ zero_or_more: Any = None,
+ ) -> Self:
+
+ kwargs: Dict[_exact_texts_like._PredefinedPatternType, Any] = {
+ 'exactly_one': exactly_one,
+ 'zero_or_one': zero_or_one,
+ 'one_or_more': one_or_more,
+ 'zero_or_more': zero_or_more,
+ }
+
+ # TODO: since we removed customization via tuple of tuples,
+ # consider refactoring to using dict for globs management
+ return self.__class__(
+ *self._expected,
+ _negated=self._negated,
+ _globs=tuple(
+ (glob_marker, self._PredefinedGlobPatterns[glob_pattern_type])
+ for glob_pattern_type, glob_marker in kwargs.items()
+ ),
+ )
+
+ @property
+ def _glob_markers(self):
+ return [glob_marker for glob_marker, _ in self._globs]
+
+ def __call__(self, entity: Collection):
+
+ visible_texts = [
+ webelement.text
+ for webelement in entity.locate()
+ if webelement.is_displayed()
+ ]
+ # TODO: should we just check for * in pattern here and further for zero_like?
+ # TODO: consider moving to self
+ zero_like = lambda item_marker: item_marker in [
+ marker
+ for marker, pattern in self._globs
+ if pattern
+ in (
+ _exact_texts_like._PredefinedGlobPatterns['zero_or_one'],
+ _exact_texts_like._PredefinedGlobPatterns['zero_or_more'],
+ )
+ ]
+ actual_to_match = (
+ # seems like not needed anymore, once we refactored from join to reduce
+ # in order to be able to add '?' for zero_like in the end...
+ # see more exaplanation below...
+ #
+ # (
+ # # zero_like globs in the START needs an extra separator
+ # # to match correctly
+ # _exact_texts_like._MATCHING_SEPARATOR
+ # if zero_like_at_start
+ # else ''
+ # )
+ # +
+ _exact_texts_like._MATCHING_SEPARATOR.join(
+ text if text != '' else _exact_texts_like._MATCHING_EMPTY_STRING_MARKER
+ for text in visible_texts
+ )
+ # zero_like globs in the END needed an extra separator ...
+ # + (
+ # # zero_like globs in the END needs an extra separator at the end
+ # # to match correctly
+ # _exact_texts_like._MATCHING_SEPARATOR
+ # if zero_like_at_end
+ # else ''
+ # )
+ # but to make zero_like work on actual zero items in the middle
+ # we had to add this extra separator for all types of globs...
+ # actually we needed a different thing but the latter happened
+ # as a side effect... so in order to not "clean side effects"
+ # we just add here the same separator for all cases in the end:
+ + _exact_texts_like._MATCHING_SEPARATOR
+ )
+ actual_to_render = _exact_texts_like._RENDERING_SEPARATOR.join(visible_texts)
+
+ glob_pattern_by = lambda marker: next( # noqa
+ glob_pattern
+ for glob_marker, glob_pattern in self._globs
+ if glob_marker == marker
+ )
+ with_added_empty_string_marker = lambda item: (
+ str(item)
+ if item is not ''
+ else _exact_texts_like._MATCHING_EMPTY_STRING_MARKER
+ )
+ MATCHING_SEPARATOR = re.escape(_exact_texts_like._MATCHING_SEPARATOR)
+ expected_pattern = (
+ r'^'
+ # + re.escape(_exact_texts_like._MATCHING_SEPARATOR).join(
+ # (
+ # re.escape(with_added_empty_string_marker(item))
+ # if item not in self._glob_markers
+ # else glob_pattern_by(item)
+ # )
+ # for item in self._expected
+ # )
+ # – refactored join to reduce, being able to modify separator for cases
+ # where next marker is zero_like (e.g. from , to ,?)
+ + str(
+ reduce(
+ lambda acc, item: (
+ acc
+ + (
+ self._process_patterns(with_added_empty_string_marker(item))
+ + MATCHING_SEPARATOR
+ if item not in self._glob_markers
+ else (
+ glob_pattern_by(item)
+ + MATCHING_SEPARATOR
+ + ('?' if zero_like(item) else '')
+ )
+ )
+ ),
+ self._expected,
+ '', # start from '' as acc to add separator after item not before
+ )
+ )
+ + r'$'
+ )
+
+ if not self._match(expected_pattern, actual_to_match):
+ # TODO: implement pattern_explained
+ # probably after refactoring from tuple to dict as globs storage
+ # pattern_explained = [
+ # next(...) if item in self._glob_markers else item
+ # for item in self._expected
+ # ]
+ raise AssertionError(
+ f'actual visible texts:\n {actual_to_render}\n'
+ '\n'
+ # f'Pattern explained:\n {pattern_explained}\n'
+ f'Pattern used for matching:\n {expected_pattern}\n'
+ f'Actual text used to match:\n {actual_to_match}'
+ )
+
+ def __str__(self):
+ return (
+ f'{self._name_prefix} {"no " if self._negated else ""}{self._name}:\n '
+ + _exact_texts_like._RENDERING_SEPARATOR.join(
+ (
+ str(item)
+ if item
+ not in [
+ marker
+ for marker, _ in _exact_texts_like._RENDERING_TRANSLATIONS
+ ]
+ else next(
+ translation
+ for marker, translation in _exact_texts_like._RENDERING_TRANSLATIONS
+ if marker == item
+ )
+ )
+ for item in self._expected
+ )
+ )
+
+ # on subclassing this class, in case of new params to init
+ # you have to ensure that such new params are counted in overriden not_
+ @override
+ @property
+ def not_(self) -> Self:
+ return self.__class__(
+ *self._expected, _negated=not self._negated, _globs=self._globs
+ )
+
+ def _match(self, pattern, actual):
+ answer = re.match(pattern, actual)
+ return not answer if self._negated else answer
+
+ # TODO: will other methods like or_, and_ – do work? o_O
+
+
+# texts_pattern can be a good alias for text_patterns_like
+# assuming that s_pattern covers both
+# – item as globs (_like) and items as regex (patternS)
+# but for consistency, let's not break the convention of adding _like everywhere
+# where we use item globs
+class _text_patterns_like(_exact_texts_like):
+ """Condition to match visible texts of all elements in a collection
+ with supported item placeholders to include/exclude items from match
+ (like [_exact_texts_like][selene.match._exact_texts_like] condition)
+ and with additionally supported wildcards (implicit and explicit)
+ for the corresponding matching of each item text in a collection"""
+
+ def __init__(
+ self,
+ *expected: str | int | float | Iterable,
+ # by default nothing is processed,
+ # i.e. items will be considered as regex patterns
+ # with behavior similar to implicit ^ and $ for each item text
+ _process_patterns: Callable[[AnyStr], AnyStr] = lambda item: item,
+ _negated=False,
+ _name_prefix='have',
+ _name='text patterns like',
+ # even though we don't customize globs in this class
+ # the child classes can, so at least we have to pass through globs
+ _globs=(),
+ ): # noqa
+ super().__init__(
+ *expected,
+ _negated=_negated,
+ _globs=_globs,
+ _name_prefix=_name_prefix,
+ _name=_name,
+ )
+ self._process_patterns = _process_patterns # type: ignore
+
+
+# TODO: add an alias from texts(*expected).with_regex to text_patterns_like
+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
+ (like [_exact_texts_like][selene.match._exact_texts_like] condition)
+ and with additionally supported wildcards (implicit and explicit)
+ for the corresponding matching of each item text in a collection"""
+
+ def __init__(
+ self,
+ *expected: str | int | float | Iterable,
+ # by default nothing is processed,
+ # i.e. items will be considered as regex patterns
+ # with behavior similar to implicit ^ and $ for each item text
+ _process_patterns: Callable[[AnyStr], AnyStr] = lambda item: item,
+ _negated=False,
+ _globs=(), # just to match interface (will be actually skipped)
+ _name_prefix='have',
+ _name='text patterns',
+ ): # noqa
+ super().__init__(
+ *expected,
+ _process_patterns=_process_patterns,
+ _negated=_negated,
+ _name_prefix=_name_prefix,
+ _name=_name,
+ )
+ # disable globs (doing after __init__ to override defaults)
+ self._globs = ()
+
+ # 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?
+ # then ^ and $ will be explicit instead of implicit as for now
+
+
+class _texts_like(_text_patterns_like):
+ """Condition to match visible texts of all elements in a collection
+ with supported item globs (placeholders to include/exclude items from match,
+ like in [_exact_texts_like][selene.match._exact_texts_like] condition)
+ and matching each expected item text (that is not an item glob) – "by contains".
+ Has additional support for classic wildcards via `.with_wildcards` method
+ (* matches any number of any characters including none,
+ ? matches any single character).
+ """
+
+ def __init__(
+ self,
+ *expected: str | int | float | Iterable,
+ _negated=False,
+ _globs=(),
+ _name_prefix='have',
+ _name='texts like',
+ ):
+ def match_text_by_contains(item: str) -> str:
+ return r'.*?' + re.escape(item) + r'.*?'
+
+ super().__init__(
+ *expected,
+ _process_patterns=match_text_by_contains,
+ _negated=_negated,
+ _globs=_globs, # just in case – passing through
+ _name_prefix=_name_prefix,
+ _name=_name,
+ )
+
+ @property
+ def with_regex(self):
+ """An alias to [_text_patterns_like][selene.match._text_patterns_like] condition
+ : switches to regex matching mode for all items in the expected list"""
+ return _text_patterns_like(
+ *self._expected,
+ _negated=self._negated,
+ _globs=self._globs,
+ )
+
+ @property
+ def with_wildcards(self):
+ # TODO: do we really need _chars suffix?
+ return self.where_wildcards(zero_or_more_chars='*', exactly_one_char='?')
+
+ def where_wildcards(self, zero_or_more_chars=None, exactly_one_char=None):
+ def process_wildcards(item: str) -> str:
+ """
+ TODO: support more wildcards: [abc], [a-z], [!abc], [!a-z]
+ """
+
+ # return re.escape(item).replace(r'\*', r'.*?').replace(r'\?', r'.')
+ # return re.sub(
+ # re.escape(zero_or_more_chars),
+ # '.*?',
+ # re.sub(re.escape(exactly_one_char), '.', re.escape(item)),
+ # )
+ if zero_or_more_chars is None and exactly_one_char is None:
+ return self._process_patterns(item)
+
+ # return (
+ # re.escape(item)
+ # .replace(re.escape(zero_or_more_chars), r'.*?')
+ # .replace(re.escape(exactly_one_char), r'.')
+ # )
+
+ wildcards_with_pattern = filter(
+ lambda pair: pair[0] != '',
+ (
+ (re.escape(zero_or_more_chars or ''), r'.*?'),
+ (re.escape(exactly_one_char or ''), r'.'),
+ ),
+ )
+
+ return reduce(
+ lambda acc, wildcard_with_pattern: acc.replace(*wildcard_with_pattern),
+ wildcards_with_pattern,
+ re.escape(item),
+ )
+
+ # return base class to disable doubled `.with_wildcards.with_wildcards`
+ return _text_patterns_like(
+ *self._expected,
+ _process_patterns=process_wildcards,
+ _negated=self._negated,
+ _globs=self._globs,
+ _name='texts with wildcards like',
+ )
+
+
+# # refactored to class
+# def _texts_like(
+# *expected: str | int | float | Iterable,
+# ):
+# def match_text_by_contains(item: str) -> str:
+# return r'.*?' + re.escape(item) + r'.*?'
+#
+# return _text_patterns_like(*expected, _process_wildcards=match_text_by_contains)
+
# TODO: consider refactoring the code like below by moving outside fns like url, title, etc...
# TODO: probably we will do that nevertheless when reusing "commands&queries" inside element class definitions
diff --git a/selene/support/conditions/have.py b/selene/support/conditions/have.py
index 56bc2c12..a25d8d29 100644
--- a/selene/support/conditions/have.py
+++ b/selene/support/conditions/have.py
@@ -19,6 +19,7 @@
# 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.
+from __future__ import annotations
import warnings
from typing import Any, Union, Iterable, Optional
@@ -140,10 +141,26 @@ def texts(*partial_values: Union[str, Iterable[str]]) -> Condition[Collection]:
return match.collection_has_texts(*partial_values)
-def exact_texts(*values: Union[str, Iterable[str]]) -> Condition[Collection]:
+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 _text_patterns_like(*values: str | int | float | Iterable):
+ return match._text_patterns_like(*values)
+
+
+def _text_patterns(*values: str | int | float | Iterable):
+ return match._text_patterns(*values)
+
+
+def _texts_like(*values: str | int | float | Iterable):
+ return match._texts_like(*values)
+
+
def url(exact_value: str) -> Condition[Browser]:
return match.browser_has_url(exact_value)
diff --git a/selene/support/conditions/not_.py b/selene/support/conditions/not_.py
index 8acd6910..074c466d 100644
--- a/selene/support/conditions/not_.py
+++ b/selene/support/conditions/not_.py
@@ -19,8 +19,9 @@
# 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.
+from __future__ import annotations
import warnings
-from typing import Any
+from typing import Any, Iterable
from selene.core import match as _match
@@ -225,10 +226,26 @@ def texts(*partial_values: str) -> Condition[Collection]:
return _match.collection_has_texts(*partial_values).not_
-def exact_texts(*values: str) -> Condition[Collection]:
+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 _text_patterns_like(*values: str | int | float | Iterable):
+ return _match._text_patterns_like(*values).not_
+
+
+def _text_patterns(*values: str | int | float | Iterable):
+ return _match._text_patterns(*values).not_
+
+
+def _texts_like(*values: str | int | float | Iterable):
+ return _match._texts_like(*values).not_
+
+
def url(exact_value: str) -> Condition[Browser]:
return _match.browser_has_url(exact_value).not_
diff --git a/tests/integration/condition__collection__have_exact_texts_like__with_ellipsis_globbing_test.py b/tests/integration/condition__collection__have_exact_texts_like__with_ellipsis_globbing_test.py
new file mode 100644
index 00000000..54f74272
--- /dev/null
+++ b/tests/integration/condition__collection__have_exact_texts_like__with_ellipsis_globbing_test.py
@@ -0,0 +1,1050 @@
+# 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.exceptions import TimeoutException
+from tests.integration.helpers.givenpage import GivenPage
+
+# TODO: review tests: clean up, add more cases if needed, break down into smaller tests
+
+
+def test_should_have_exact_texts_like__does_not_match_merged_with_comma_items(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.25)
+ li = browser.all('li')
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1
+ - 2, 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+ - X
+
+ '''
+ )
+ X = 'X'
+
+ # WHEN
+ have_exact_texts = lambda *values: have._exact_texts_like(*values).where(
+ one_or_more=...
+ )
+ have_no_exact_texts = have.no._exact_texts_like
+
+ # THEN
+ li.should(have_no_exact_texts(1, 2, 3, 4, 5, 6, 7, 8, 9, X))
+ li.should(have_no_exact_texts(1, 2, 3, 4, 5, 6, 7, 8, 9, X))
+ li.should(have_no_exact_texts(1, '2, 3', '4, 5', 6, 7, 8, 9, X))
+
+
+def test_should_have_exact_texts_and_no_exact_texts__with_custom_one_or_more_extensive(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.25)
+ li = browser.all('li')
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+ - X
+
+ '''
+ )
+ X = 'X'
+ # AND just a simple check to ensure normal have.exact_texts works
+ li.should(have.exact_texts('1', '2', '3', '4', '5', '6', '7', '8', '9', X))
+ li.should(have.exact_texts(1, 2, 3, 4, 5, 6, 7, 8, 9, X))
+
+ # WHEN
+ have_exact_texts = lambda *values: have._exact_texts_like(*values).where(
+ one_or_more=...
+ )
+ have_no_exact_texts = have.no._exact_texts_like
+
+ # THEN
+ li.should(have_exact_texts('1', '2', '3', '4', '5', '6', '7', '8', '9', X))
+ li.should(have_exact_texts(1, 2, 3, 4, 5, 6, 7, 8, 9, X))
+ li.should(have_no_exact_texts(1, ..., '3, 4', 5, 6, 7, 8, 9, X))
+
+ li.should(have_no_exact_texts(..., 1, 2, 3, 4, 5, 6, 7, 8, 9, X))
+ li.should(have_no_exact_texts(1, 2, 3, 4, 5, 6, 7, 8, 9, X, ...))
+ li.should(have_no_exact_texts(..., 1, 2, 3, 4, 5, 6, 7, 8, 9, X, ...))
+ li.should(have_exact_texts(..., 2, 3, 4, 5, 6, 7, 8, 9, X))
+ li.should(have_no_exact_texts(..., 2, 3, 4, 5, 6, 7, 8, 9, X, ...))
+ li.should(have_exact_texts(1, 2, 3, 4, 5, 6, 7, 8, 9, ...))
+ li.should(have_no_exact_texts(..., 1, 2, 3, 4, 5, 6, 7, 8, 9, ...))
+ li.should(have_exact_texts(1, 2, 3, 4, 5, ..., 7, 8, 9, X))
+ li.should(have_no_exact_texts(1, 2, 3, 4, 5, 7, 8, 9, X))
+ li.should(have_exact_texts(1, 2, 3, 4, ..., ..., 7, 8, 9, X)) # valid but redundant
+ li.should(have_exact_texts(1, 2, 3, 4, ..., 7, 8, 9, X)) # same as previous
+ li.should(have_no_exact_texts(2, 3, 4, ..., 7, 8, 9, X))
+ li.should(have_no_exact_texts(1, 2, 3, 4, ..., 7, 8, 9))
+ li.should(have_exact_texts(..., 2, 3, 4, ..., 7, 8, 9, ...))
+ li.should(have_exact_texts(..., 2, ..., 4, ..., 7, ..., 9, ...))
+ li.should(have_no_exact_texts(..., 2, ..., 4, ..., 7, 9, ...))
+ li.should(have_no_exact_texts(..., 2, 4, ..., 7, ..., 9, ...))
+ li.should(have_no_exact_texts(..., 2, 4, ..., 7, 9, ...))
+
+ li.should(have_no_exact_texts(1, 2))
+ li.should(have_exact_texts(1, 2, ...))
+ li.should(have_no_exact_texts(2, 3, ...))
+ li.should(have_exact_texts(..., 2, 3, ...))
+ li.should(have_no_exact_texts(1, 3, ...))
+ li.should(have_exact_texts(1, ..., 3, ...))
+ li.should(have_exact_texts(1, ..., 4, ...))
+
+ li.should(have_no_exact_texts(9, X))
+ li.should(have_exact_texts(..., 9, X))
+ li.should(have_no_exact_texts(..., 8, 9))
+ li.should(have_exact_texts(..., 8, 9, ...))
+ li.should(have_no_exact_texts(..., 8, X)) # TODO: test how fails
+ li.should(have_exact_texts(..., 8, ..., X))
+ li.should(have_exact_texts(..., 7, ..., X))
+
+ li.should(have_no_exact_texts(5, 6))
+ li.should(have_no_exact_texts(5, 6, ...))
+ li.should(have_no_exact_texts(..., 5, 6))
+ li.should(have_exact_texts(..., 5, 6, ...))
+ li.should(have_no_exact_texts(..., 5, ..., 6, ...))
+
+
+def test_exact_texts_like__on__mixed_numbers_and_quoted_text__with_custom_one_or_more(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1
+ - Two
+ - 3
+ - 4
+ - 'Five'
+ - 6
+ - '7'
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(..., 'Two', ..., 4, "'Five'", ...).where(one_or_more=...)
+ )
+
+
+def test_exact_texts_like__with_default_exactly_one_and_zero_or_more(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+ - 1
+ - Two
+ - 3
+ - 4
+ - 'Five'
+ - 6
+ - 7
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ 'Zero',
+ 1,
+ 'Two',
+ [(...,)], # means zero or MORE and so does match 2 texts
+ "'Five'",
+ 6,
+ 7, # here list ends
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ 1,
+ 'Two',
+ [(...,)], # means zero or MORE and so does match 2 texts
+ "'Five'",
+ 6,
+ '7', # here list ends
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ ...,
+ 'Two',
+ [(...,)], # means zero or MORE and so does match 2 texts
+ "'Five'",
+ 6,
+ '7',
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ ...,
+ 'Two',
+ [(...,)], # means zero or MORE and so does match 2 texts
+ "'Five'",
+ ...,
+ ...,
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ ...,
+ 'Two',
+ ...,
+ 4,
+ "'Five'",
+ 6,
+ ..., # means exactly one and so does match 1 text
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ ...,
+ 'Two',
+ ...,
+ 4,
+ "'Five'",
+ 6,
+ (...,), # means one or more and so does match 1 text
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ ..., # means exactly one and so does NOT match because of 2 texts (Zero, 1)
+ 'Two',
+ [(...,)], # means zero or MORE and so does match 2 texts
+ "'Five'",
+ 6,
+ '7',
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)], # means zero or more and so does match 2 texts at start
+ 'Two',
+ ...,
+ 4,
+ "'Five'",
+ 6,
+ '7',
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ ...,
+ 'Two',
+ ...,
+ 4,
+ "'Five'",
+ 6,
+ '7', # here list ends
+ [(...,)], # means ZERO or more and so does match 0 texts
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)], # means ZERO or more and so does match 0 texts
+ 'Zero', # here list STARTs
+ 1,
+ 'Two',
+ ...,
+ 4,
+ "'Five'",
+ 6,
+ '7',
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ ...,
+ 'Two',
+ ...,
+ 4,
+ "'Five'",
+ [(...,)], # means ZERO or more and so does match 0 texts
+ 6,
+ '7',
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ ..., # means exactly one and so does NOT match
+ 4,
+ "'Five'",
+ 6,
+ '7',
+ [(...,)],
+ )
+ )
+
+
+def test_exact_texts_like__with_default_exactly_one_one_or_more_and_zero_or_more(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+ - 1
+ - Two
+ - 3
+ - 4
+ - 'Five'
+ - 6
+ - 7
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ (...,), # means zero or MORE and so does match 2 texts
+ 'Two',
+ [(...,)],
+ "'Five'",
+ ...,
+ '7',
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ (...,),
+ 'Two',
+ [(...,)],
+ "'Five'",
+ ...,
+ ..., # means zero or MORE and so does NOT match this absent text
+ '7',
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ ...,
+ ...,
+ 'Two',
+ [(...,)],
+ "'Five'",
+ '6',
+ (...,),
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ ...,
+ "'Five'",
+ '6',
+ (...,),
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ ...,
+ "'Five'",
+ [(...,)],
+ '6',
+ (...,),
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ ...,
+ "'Five'",
+ [(...,)],
+ '6',
+ 7,
+ [(...,)],
+ )
+ )
+
+
+def test_exact_texts_like__with_default_exactly_one_one_or_more_zero_or_more_zero_or_1(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+ - 1
+ - Two
+ - 3
+ - 4
+ - 'Five'
+ - 6
+ - 7
+ - 8
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...],
+ "'Five'",
+ (...,),
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...],
+ 6,
+ (...,),
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...],
+ 7,
+ (...,),
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Zero',
+ 1,
+ 'Two',
+ ...,
+ 4,
+ [...],
+ "'Five'",
+ (...,),
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [...],
+ 'Zero',
+ 1,
+ 'Two',
+ ...,
+ 4,
+ [(...,)],
+ "'Five'",
+ (...,),
+ 8,
+ [...],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [...],
+ 1,
+ 'Two',
+ ...,
+ 4,
+ [(...,)],
+ "'Five'",
+ (...,),
+ 8,
+ [...],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [...],
+ 'Two',
+ ...,
+ 4,
+ [(...,)],
+ "'Five'",
+ (...,),
+ 8,
+ [...],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [...],
+ 1,
+ 'Two',
+ ...,
+ 4,
+ [(...,)],
+ "'Five'",
+ 6,
+ 7,
+ [...],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [...],
+ 1,
+ 'Two',
+ ...,
+ 4,
+ [(...,)],
+ "'Five'",
+ 6,
+ [...],
+ )
+ )
+
+
+def test_exact_texts_like__with_default_doubled_globs(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+ - 1
+ - Two
+ - 3
+ - 4
+ - 'Five'
+ - 6
+ - 7
+ - 8
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # zero
+ [...], # or two
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # zero
+ [...], # or two
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # zero or one
+ [...], # or two
+ 6,
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # zero or one
+ [...], # or two
+ "'Five'",
+ 6,
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ ..., # one
+ [...], # or two
+ 6,
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ ..., # one
+ [...], # or two
+ "'Five'",
+ 6,
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ ..., # one
+ [...], # or two
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ ..., # one
+ [...], # or two
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # one
+ ..., # or two
+ 6,
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # one
+ ..., # or two
+ "'Five'",
+ 6,
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # one
+ ..., # or two
+ 7,
+ 8,
+ [(...,)],
+ )
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(
+ [(...,)],
+ 'Two',
+ ...,
+ 4,
+ [...], # one
+ ..., # or two
+ 8,
+ [(...,)],
+ )
+ )
+
+ # TODO: cover other "doubled" cases?
+
+
+def test_exact_texts_like__where_overrides_original_globs(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1
+ - Two
+ - 3
+ - 4
+ - 'Five'
+ - 6
+ - '7'
+
+ '''
+ )
+
+ browser.all('li').should(
+ have.no._exact_texts_like(..., 'Two', ..., 4, "'Five'", 6, '7', [(...,)]).where(
+ one_or_more=...
+ )
+ )
+
+
+def test_exact_texts_like__on__mixed_numbers_and_quoted_text__with_default_one_or_more(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 0
+ - 1
+ - Two
+ - 3
+ - 4
+ - 'Five'
+ - 6
+ - '7'
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like((...,), 'Two', (...,), 4, "'Five'", (...,))
+ )
+
+
+def test_exact_texts_like__on__mixed_numbers_emtpy_and_quoted_text(session_browser):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+ - 1
+ - Two
+
+ - 4
+ - 'Five'
+ - 6
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(..., 'Two', ..., 4, "'Five'", ...).where(one_or_more=...)
+ )
+
+
+def test_exact_texts_like__on__mixed__with_expected_empty_text(session_browser):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 0
+ - 1
+ - Two
+
+ - 4
+ - 'Five'
+ - 6
+
+ '''
+ )
+
+ browser.all('li').should(
+ have._exact_texts_like(0, ..., '', ..., "'Five'", ...).where(one_or_more=...)
+ )
+
+
+def test_correct_exact_texts_like_exception_message_with_custom_globs(session_browser):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+
+ - Two
+
+ - 4
+ - 'Five'
+ - 6
+
+ '''
+ )
+
+ try:
+ browser.all('li').should(
+ have._exact_texts_like(..., 'Two', '', ..., "'Five'").where(one_or_more=...)
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert (
+ 'Message: \n'
+ '\n'
+ 'Timed out after 0.1s, while waiting for:\n'
+ "browser.all(('css selector', 'li')).have exact texts like:\n"
+ " ..., Two, , ..., 'Five'\n"
+ '\n'
+ 'Reason: AssertionError: actual visible texts:\n'
+ " Zero, , Two, , 4, 'Five', 6\n"
+ '\n'
+ 'Pattern used for matching:\n'
+ " ^.+?‚Two‚‹EMTPY_STRING›‚.+?‚'Five'‚$\n"
+ 'Actual text used to match:\n'
+ " Zero‚‹EMTPY_STRING›‚Two‚‹EMTPY_STRING›‚4‚'Five'‚6‚\n"
+ 'Screenshot: '
+ ) in str(error)
+
+
+def test_correct_no_exact_texts_like_exception_message__with_custom_globs(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+
+ - Two
+
+ - 4
+ - 'Five'
+ - 6
+
+ '''
+ )
+
+ try:
+ browser.all('li').should(
+ have.no._exact_texts_like(..., 'Two', '', ..., "'Five'", 6).where(
+ one_or_more=...
+ )
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert (
+ 'Message: \n'
+ '\n'
+ 'Timed out after 0.1s, while waiting for:\n'
+ "browser.all(('css selector', 'li')).have no exact texts like:\n"
+ " ..., Two, , ..., 'Five', 6\n"
+ '\n'
+ 'Reason: AssertionError: actual visible texts:\n'
+ " Zero, , Two, , 4, 'Five', 6\n"
+ '\n'
+ 'Pattern used for matching:\n'
+ " ^.+?‚Two‚‹EMTPY_STRING›‚.+?‚'Five'‚6‚$\n"
+ 'Actual text used to match:\n'
+ " Zero‚‹EMTPY_STRING›‚Two‚‹EMTPY_STRING›‚4‚'Five'‚6‚\n"
+ 'Screenshot: '
+ ) in str(error)
+
+
+def test_correct_no_exact_texts_like_exception_message__with_custom_globs_mixed(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+
+ - Two
+
+ - 4
+ - 'Five'
+ - 6
+ - 7
+ - 8
+
+ '''
+ )
+
+ try:
+ browser.all('li').should(
+ have._exact_texts_like(
+ [...],
+ 1, # fails here: 1 != empty string
+ 'Two',
+ ...,
+ 4,
+ [(...,)],
+ "'Five'",
+ (...,),
+ 8,
+ [...],
+ )
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert (
+ 'Message: \n'
+ '\n'
+ 'Timed out after 0.1s, while waiting for:\n'
+ "browser.all(('css selector', 'li')).have exact texts like:\n"
+ " [...], 1, Two, ..., 4, [(...,)]), 'Five', (...,), 8, [...]\n"
+ '\n'
+ 'Reason: AssertionError: actual visible texts:\n'
+ " Zero, , Two, , 4, 'Five', 6, 7, 8\n"
+ '\n'
+ 'Pattern used for matching:\n'
+ " ^[^‚]*‚?1‚Two‚[^‚]+‚4‚.*?‚?'Five'‚.+?‚8‚[^‚]*‚?$\n"
+ 'Actual text used to match:\n'
+ " Zero‚‹EMTPY_STRING›‚Two‚‹EMTPY_STRING›‚4‚'Five'‚6‚7‚8‚\n"
+ 'Screenshot: '
+ ) in str(error)
+
+
+def test_correct_no_exact_texts_like_exception_message__with_default_globs(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - Zero
+
+ - Two
+
+ - 4
+ - 'Five'
+ - 6
+
+ '''
+ )
+
+ try:
+ browser.all('li').should(
+ have.no._exact_texts_like((...,), 'Two', '', (...,), "'Five'", 6)
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert (
+ 'Message: \n'
+ '\n'
+ 'Timed out after 0.1s, while waiting for:\n'
+ "browser.all(('css selector', 'li')).have no exact texts like:\n"
+ " (...,), Two, , (...,), 'Five', 6\n"
+ '\n'
+ 'Reason: AssertionError: actual visible texts:\n'
+ " Zero, , Two, , 4, 'Five', 6\n"
+ '\n'
+ 'Pattern used for matching:\n'
+ " ^.+?‚Two‚‹EMTPY_STRING›‚.+?‚'Five'‚6‚$\n"
+ 'Actual text used to match:\n'
+ " Zero‚‹EMTPY_STRING›‚Two‚‹EMTPY_STRING›‚4‚'Five'‚6‚\n"
+ 'Screenshot: '
+ ) in str(error)
diff --git a/tests/integration/condition__collection__have_exact_texts_test.py b/tests/integration/condition__collection__have_exact_texts_test.py
index 9dbba294..60827548 100644
--- a/tests/integration/condition__collection__have_exact_texts_test.py
+++ b/tests/integration/condition__collection__have_exact_texts_test.py
@@ -95,5 +95,5 @@ def test_should_have_exact_texts_exception(session_browser):
with pytest.raises(TimeoutException) as error:
browser.all('li').should(have.exact_texts('Alex'))
- assert "has exact texts ('Alex',)" in error.value.msg
- assert "AssertionError: actual visible_texts: ['Alex', 'Yakov']" in error.value.msg
+ assert ".has exact texts ['Alex']" in error.value.msg
+ assert "AssertionError: actual visible texts: ['Alex', 'Yakov']" in error.value.msg
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
new file mode 100644
index 00000000..564ab1c2
--- /dev/null
+++ b/tests/integration/condition__collection__have_texts_like__with_items_globs_regex_patterns_and_wildcards_test.py
@@ -0,0 +1,628 @@
+# 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_patterns_like__mixed__with_regex_patterns_support(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ # full regex support (^ and $ for each item text is kind of implicit)
+ # – for item texts in addition to support of ellipsis globs as items placeholders
+ browser.all('li').should(
+ match._text_patterns_like(
+ [...],
+ r'.*?O.e.*?',
+ ...,
+ r'.*?Thr.+.*?',
+ (...,),
+ r'.*?Six.*?',
+ [(...,)],
+ r'.*?Ten.*?',
+ [(...,)],
+ )
+ )
+ # with alias
+ browser.all('li').should(
+ have._text_patterns_like(
+ [...],
+ r'.*?O.e.*?',
+ ...,
+ r'.*?Thr.+.*?',
+ (...,),
+ r'.*?Six.*?',
+ [(...,)],
+ r'.*?Ten.*?',
+ [(...,)],
+ )
+ )
+ # with one more alias
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ r'.*?O.e.*?',
+ ...,
+ r'.*?Thr.+.*?',
+ (...,),
+ r'.*?Six.*?',
+ [(...,)],
+ r'.*?Ten.*?',
+ [(...,)],
+ ).with_regex
+ )
+ # without "_like" version will lack support of ellipsis globs as items placeholders
+ browser.all('li').should(
+ match._text_patterns(
+ r'.*?O.e.*?',
+ r'2\) Two\.\.\.',
+ r'.*?Thr.+.*?',
+ r'4\) Four\.\.\.',
+ r'5\) Five\.\.\.?',
+ r'.*?Six.*?',
+ r'7\) Seven\.\.\.',
+ r'8\) Eight\.\.\.',
+ r'9\) Nine\.\.\.',
+ r'.*?Ten.*?',
+ )
+ )
+ browser.all('li').should(
+ match._text_patterns(
+ [...],
+ r'.*?O.e.*?',
+ ...,
+ r'.*?Thr.+.*?',
+ (...,),
+ r'.*?Six.*?',
+ [(...,)],
+ r'.*?Ten.*?',
+ [(...,)],
+ ).not_
+ )
+ # with alias
+ browser.all('li').should(
+ have._text_patterns(
+ r'.*?O.e.*?',
+ r'2\) Two\.\.\.',
+ r'.*?Thr.+.*?',
+ r'4\) Four\.\.\.',
+ r'5\) Five\.\.\.?',
+ r'.*?Six.*?',
+ r'7\) Seven\.\.\.',
+ r'8\) Eight\.\.\.',
+ r'9\) Nine\.\.\.',
+ r'.*?Ten.*?',
+ )
+ )
+
+
+def test_text_patternss_like__mixed__with_regex_patterns_support__error_messages(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ try:
+ # full regex support (^ and $ for each item text is kind of implicit)
+ # – for item texts in addition to support of ellipsis globs as items placeholders
+ browser.all('li').should(
+ have._text_patterns_like(
+ [...],
+ r'.*?O.e.*?',
+ ...,
+ r'.*?Three', # fails on ending: 'Three' != 'Three...'
+ (...,),
+ r'.*?Six.*?',
+ [(...,)],
+ r'.*?Ten.*?',
+ [(...,)],
+ )
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert (
+ 'Message: \n'
+ '\n'
+ 'Timed out after 0.1s, while waiting for:\n'
+ "browser.all(('css selector', 'li')).have text patterns like:\n"
+ ' [...], .*?O.e.*?, ..., .*?Three, (...,), .*?Six.*?, [(...,)]), '
+ '.*?Ten.*?, [(...,)])\n'
+ '\n'
+ 'Reason: AssertionError: actual visible texts:\n'
+ ' 1) One..., 2) Two..., 3) Three..., 4) Four..., 5) Five..., 6) Six..., 7) '
+ 'Seven..., 8) Eight..., 9) Nine..., X) Ten...\n'
+ '\n'
+ 'Pattern used for matching:\n'
+ ' ^[^‚]*‚?.*?O.e.*?‚[^‚]+‚.*?Three‚.+?‚.*?Six.*?‚.*?‚?.*?Ten.*?‚.*?‚?$\n'
+ 'Actual text used to match:\n'
+ ' 1) One...‚2) Two...‚3) Three...‚4) Four...‚5) Five...‚6) Six...‚7) '
+ 'Seven...‚8) Eight...‚9) Nine...‚X) Ten...‚\n'
+ 'Screenshot: '
+ ) in str(error)
+
+ # with an alias
+ try:
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ r'.*?O.e.*?',
+ ...,
+ r'.*?Three', # fails on ending: 'Three' != 'Three...'
+ (...,),
+ r'.*?Six.*?',
+ [(...,)],
+ r'.*?Ten.*?',
+ [(...,)],
+ ).with_regex
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert ".have text patterns like:\n" in str(error)
+
+ # without "_like" version will lack support of ellipsis globs as items placeholders
+ try:
+ browser.all('li').should(
+ have._text_patterns(
+ r'^.*?O.e.*?$', # fails on syntax: '^' and '$' should be implicit
+ r'2\) Two\.\.\.',
+ r'.*?Thr.+.*?',
+ r'4\) Four\.\.\.',
+ r'5\) Five\.\.\.?',
+ r'.*?Six.*?',
+ r'7\) Seven\.\.\.',
+ r'8\) Eight\.\.\.',
+ r'9\) Nine\.\.\.',
+ r'.*?Ten.*?',
+ )
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert ".have text patterns:\n" in str(error)
+
+ try:
+ browser.all('li').should(
+ have._text_patterns(
+ r'.*?O.e.*?',
+ r'2\) Two\.\.\.',
+ r'.*?Thr.+.*?',
+ r'4\) Four\.\.\.',
+ r'5\) Five\.\.\.?',
+ r'.*?Six.*?',
+ r'7\) Seven\.\.\.',
+ r'8\) Eight\.\.\.',
+ r'9\) Nine\.\.\.',
+ r'.*?Ten.*?',
+ ).not_ # fails here because without not_ it matches
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert ".have no text patterns:\n" in str(error)
+
+
+def test_text_patterns_like__mixed__with_implicit_wildcards_patterns_support(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ # for the most common case (similar to classic have.texts)...
+ browser.all('li').should(
+ match._text_patterns_like(
+ [...],
+ r'.*?One.*?',
+ ...,
+ r'.*?Three.*?',
+ (...,),
+ r'.*?Six.*?',
+ [(...,)],
+ r'.*?Ten.*?',
+ [(...,)],
+ )
+ )
+ # – there is a shortcut:
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ 'One',
+ ...,
+ 'Three',
+ (...,),
+ 'Six',
+ [(...,)],
+ 'Ten',
+ [(...,)],
+ )
+ )
+ browser.all('li').should(
+ have.no._texts_like(
+ [...],
+ 'Two', # does NOT match: 'Two' != 'One'
+ ...,
+ 'Three',
+ (...,),
+ 'Six',
+ [(...,)],
+ 'Ten',
+ [(...,)],
+ )
+ )
+
+
+def test_texts_like__mixed__with_implicit_wildcards_patterns_support__with_errors(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ try:
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ 'ONE', # fails here: 'ONE' != 'One'
+ ...,
+ 'Three',
+ (...,),
+ 'Six',
+ [(...,)],
+ 'Ten',
+ [(...,)],
+ )
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.all(('css selector', 'li')).have texts like:\n"
+ ' [...], ONE, ..., Three, (...,), Six, [(...,)]), Ten, [(...,)])\n'
+ '\n'
+ 'Reason: AssertionError: actual visible texts:\n'
+ ' 1) One..., 2) Two..., 3) Three..., 4) Four..., 5) Five..., 6) Six..., 7) '
+ 'Seven..., 8) Eight..., 9) Nine..., X) Ten...\n'
+ '\n'
+ 'Pattern used for matching:\n'
+ ' ^[^‚]*‚?.*?ONE.*?‚[^‚]+‚.*?Three.*?‚.+?‚.*?Six.*?‚.*?‚?.*?Ten.*?‚.*?‚?$\n'
+ 'Actual text used to match:\n'
+ ' 1) One...‚2) Two...‚3) Three...‚4) Four...‚5) Five...‚6) Six...‚7) '
+ 'Seven...‚8) Eight...‚9) Nine...‚X) Ten...‚\n'
+ ) in str(error)
+
+
+def test_texts_like__mixed__with_explicit_wildcards_patterns_support(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ # .with_wildcards overrides default behavior
+ browser.all('li').should(
+ have.no._texts_like(
+ [...],
+ 'One', # does not match cause correct pattern '*One*' != 'One'
+ ...,
+ 'Three',
+ (...,),
+ 'Six',
+ [(...,)],
+ 'Ten',
+ [(...,)],
+ ).with_wildcards
+ )
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ '*O?e*',
+ ...,
+ '*T??ee*',
+ (...,),
+ '*Six*',
+ [(...,)],
+ '*Ten*',
+ [(...,)],
+ ).with_wildcards
+ )
+ browser.all('li').should(
+ have.no._texts_like(
+ [...],
+ '*O?e*',
+ ...,
+ '*T???ee*', # does not match: 'Three' != 'Th?ree'
+ (...,),
+ '*Six*',
+ [(...,)],
+ '*Ten*',
+ [(...,)],
+ ).with_wildcards
+ )
+
+
+def test_texts_like__mixed__with_explicit_wildcards_patterns_support__with_errors(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ try:
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ 'One', # does not match cause correct pattern '*One*' != 'One'
+ ...,
+ '*T??ee*',
+ (...,),
+ '*Six*',
+ [(...,)],
+ '*Ten*',
+ [(...,)],
+ ).with_wildcards
+ )
+ pytest.fail('expected texts mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.all(('css selector', 'li')).have texts with wildcards like:\n"
+ ' [...], One, ..., *T??ee*, (...,), *Six*, [(...,)]), *Ten*, [(...,)])\n'
+ '\n'
+ 'Reason: AssertionError: actual visible texts:\n'
+ ' 1) One..., 2) Two..., 3) Three..., 4) Four..., 5) Five..., 6) Six..., 7) '
+ 'Seven..., 8) Eight..., 9) Nine..., X) Ten...\n'
+ '\n'
+ 'Pattern used for matching:\n'
+ ' ^[^‚]*‚?One‚[^‚]+‚.*?T..ee.*?‚.+?‚.*?Six.*?‚.*?‚?.*?Ten.*?‚.*?‚?$\n'
+ 'Actual text used to match:\n'
+ ' 1) One...‚2) Two...‚3) Three...‚4) Four...‚5) Five...‚6) Six...‚7) '
+ 'Seven...‚8) Eight...‚9) Nine...‚X) Ten...‚\n'
+ ) in str(error)
+
+
+def test_texts_like__mixed__where_custom_wildcards_patterns_support(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ # .with_wildcards customized overrides default (no-wildcards) behavior
+ browser.all('li').should(
+ have.no._texts_like(
+ [...],
+ 'One',
+ ...,
+ 'Three',
+ (...,),
+ 'Six',
+ [(...,)],
+ 'Ten',
+ [(...,)],
+ ).where_wildcards(zero_or_more_chars='**', exactly_one_char='_')
+ )
+ # .with_wildcards customized overrides default explicit-wildcards behavior
+ browser.all('li').should(
+ have.no._texts_like(
+ [...],
+ '*O?e*',
+ ...,
+ '*T??ee*',
+ (...,),
+ '*Six*',
+ [(...,)],
+ '*Ten*',
+ [(...,)],
+ ).where_wildcards(zero_or_more_chars='**', exactly_one_char='_')
+ )
+ # TODO: isn't it not obvious? the context is a bit different from .where...
+ # it's also different from entity.with(**options),
+ # where not all options are overriden but just passed ones...
+ # but maybe here we say with_WILDCARDS and then we specify all wildcards...
+ # hm... maybe then ok...
+ # even one overrides everything
+ browser.all('li').should(
+ have.no._texts_like(
+ [...],
+ '**One**',
+ ...,
+ '**T??ee**', # now are not considered as wildcards = does not match
+ (...,),
+ '**Six**',
+ [(...,)],
+ '**Ten**',
+ [(...,)],
+ ).where_wildcards(zero_or_more_chars='**')
+ )
+ # even one overrides everything
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ '**One**',
+ ...,
+ '**Three**', # now are not considered as wildcards = does not match
+ (...,),
+ '**Six**',
+ [(...,)],
+ '**Ten**',
+ [(...,)],
+ ).where_wildcards(zero_or_more_chars='**')
+ )
+ browser.all('li').should(
+ have._texts_like(
+ [...],
+ '**O_e**',
+ ...,
+ '**T__ee**',
+ (...,),
+ '**Six**',
+ [(...,)],
+ '**Ten**',
+ [(...,)],
+ ).where_wildcards(zero_or_more_chars='**', exactly_one_char='_')
+ )
+
+
+def test_texts_like__mixed__where_custom_wildcards_patterns_support__with_errors(
+ session_browser,
+):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(browser.driver).opened_with_body(
+ '''
+ Hello:
+ - 1) One...
+ - 2) Two...
+ - 3) Three...
+ - 4) Four...
+ - 5) Five...
+ - 6) Six...
+ - 7) Seven...
+ - 8) Eight...
+ - 9) Nine...
+ - X) Ten...
+
+ '''
+ )
+
+ try:
+ browser.all('li').should(
+ have.no._texts_like( # fails here because actually matches without no
+ [...],
+ '**O_e**',
+ ...,
+ '**T__ee**',
+ (...,),
+ '**Six**',
+ [(...,)],
+ '**Ten**',
+ [(...,)],
+ ).where_wildcards(zero_or_more_chars='**', exactly_one_char='_')
+ )
+ pytest.fail('expected condition mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.all(('css selector', 'li')).have no texts with wildcards like:\n"
+ ) in str(error)
diff --git a/tests/integration/condition__collection__have_texts_test.py b/tests/integration/condition__collection__have_texts_test.py
index b123701d..c4609612 100644
--- a/tests/integration/condition__collection__have_texts_test.py
+++ b/tests/integration/condition__collection__have_texts_test.py
@@ -53,10 +53,15 @@ def test_should_have_texts_exception(session_browser):
'''
)
- with pytest.raises(TimeoutException) as error:
+ try:
browser.all('li').should(have.texts('Alex'))
- assert "has texts ('Alex',)" in error.value.msg
- assert "AssertionError: actual visible_texts: ['Alex', 'Yakov']" in error.value.msg
+ pytest.fail('should have failed on texts mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.all(('css selector', 'li')).has texts ('Alex',)\n"
+ '\n'
+ "Reason: AssertionError: actual actual_visible_texts: ['Alex', 'Yakov']\n"
+ ) in str(error)
def test_should_have_no_texts(session_browser):
diff --git a/tests/integration/element__get__query__js__shadow_root__all_elements_test.py b/tests/integration/element__get__query__js__shadow_root__all_elements_test.py
index 420536f8..57617e60 100644
--- a/tests/integration/element__get__query__js__shadow_root__all_elements_test.py
+++ b/tests/integration/element__get__query__js__shadow_root__all_elements_test.py
@@ -57,9 +57,9 @@ def test_actions_on_shadow_roots_of_all_elements(session_browser):
'\n'
'Timed out after 0.5s, while waiting for:\n'
"browser.all(('css selector', 'my-paragraph')): shadow roots.all(('css "
- "selector', '[name=my-text]')).has exact texts ('My WRONG text', 'My WRONG "
- "text')\n"
+ "selector', '[name=my-text]')).has exact texts ['My WRONG text', 'My WRONG "
+ "text']\n"
'\n'
- "Reason: AssertionError: actual visible_texts: ['My default text', 'My "
+ "Reason: AssertionError: actual visible texts: ['My default text', 'My "
"default text']\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 c458c467..f5c81e76 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']
'''.strip()
in handler.stream
)