diff --git a/CHANGELOG.md b/CHANGELOG.md index 8954ab2e..b44937ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,33 +122,37 @@ seems like currently we do raise, but cover with tests especially relevant for have.texts to turn on/off ignoring invisible elements -## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024) - -### TODO: decide on ... vs (...,) as one_or_more +### TODO: add `<` before driver.switch_to.* tab in iframe faq doc -### TODO: ensure no warnings +### TODO: All and AllElements aliases to Collection (maybe even deprecate Collection) -### TODO: add `<` before driver.switch_to.* tab in iframe faq doc +```python +# TODO: consider renaming or at list aliased to AllElements +# for better consistency with browser.all(selector) +# and maybe even aliased by All for nicer POM support via descriptors +class Collection(WaitingEntity['Collection'], Iterable[Element]): +``` -### TODO: rename all conditions inside match.py so match can be fully used instead be + have #530 +#### TODO: also check if have.size will be still consistent fully... maybe have.count? -### TODO: ENSURE ALL Condition.as_not USAGES ARE NOW CORRECT +## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024) -... +### TODO: decide on ... vs (...,) as one_or_more -### TODO: ENSURE composed conditions work as expected (or/and, etc.) +### TODO: ensure no warnings -... +### TODO: rename all conditions inside match.py so match can be fully used instead be + have #530 ### TODO: Consider renaming description to name for Condition, Match, Query, Command, etc. -### TODO: decide on describe_actual for Match & Condition +### TODO: check if there are no type errors on passing be._empty to should -#### TODO: finalize corresponding error messages tests for present, visible, hidden +### TODO: decide on Match `__init__` fate: allow by as positional or not? -### TODO: decide on Match fate (subclass OR subclass + match* 2 in 1) +- probably not for now (let's come back after 2.0)... actual already can be passed as positional... + - passing by as positional would not add so much benefits... -#### TODO: do we need positional actual and by args for Match? +### Consider making configurable the "filtering collection for visibility" like in texts type of conditions ### Deprecated conditions @@ -164,6 +168,18 @@ especially relevant for have.texts to turn on/off ignoring invisible elements Consider `be.hidden` as "hidden somewhere, maybe in DOM with "display:none", or even on frontend/backend, i.e. totally absent from the page". Then `be.hidden_in_dom` is stricter, and means "hidden in DOM, i.e. available in the page DOM, but not visible". +### Added experimental 4-in-1 be._empty over deprecated collection-condition be.empty + +`be._empty` is similar to `be.blank`... But `be.blank` works only for singular elements: if they are a value-based elements (like inputs and textarea) then it checks for empty value, if they are text-based elements then it checks for empty text content. + +The `be._empty` condition works same but if applied to collection +then will assert that it has size 0. Additionally, when applied to "form" element, +then it will check for all form "value-like" inputs to be empty. + +Hence, the `blank` condition is more precise, while `empty` is more general. Because of its generality, the `empty` condition can be easier to remember, but can lead to some kind of confusion when applied to both element and collection in same test. Also, its behavior can be less predictable from the user perspective when applied to form with textarea, that is both "value-like" and "text-based" element. That's why it is still marked as experimental. + +Please, consider using `be._empty` to test it in your context and provide a feedback under [#544](https://github.com/yashaka/selene/issues/544), so we can decide on its fate – keep it or not. + ### Text related 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']` diff --git a/selene/common/appium_tools.py b/selene/common/appium_tools.py new file mode 100644 index 00000000..6e2fc996 --- /dev/null +++ b/selene/common/appium_tools.py @@ -0,0 +1,29 @@ +# MIT License +# +# Copyright (c) 2024 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. + + +def _is_mobile_element(element): + try: + from appium.webdriver import WebElement as MobileElement + except ImportError: + return False + return isinstance(element, MobileElement) diff --git a/selene/common/predicate.py b/selene/common/predicate.py index 943f08e8..ba6f31cd 100644 --- a/selene/common/predicate.py +++ b/selene/common/predicate.py @@ -23,6 +23,7 @@ import re +# todo: should it be more like .is_not_none? def is_truthy(something): return bool(something) if not something == '' else True diff --git a/selene/core/condition.py b/selene/core/condition.py index 46c1d434..7fa44a8f 100644 --- a/selene/core/condition.py +++ b/selene/core/condition.py @@ -803,6 +803,7 @@ def __init__( *, actual: Lambda[E, R], by: Predicate[R], + _describe_actual_result: Lambda[R, str] | None = None, _inverted=False, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): ... @@ -838,6 +839,7 @@ def __init__( *, actual: Lambda[E, R] | None = None, by: Predicate[R] | None = None, + _describe_actual_result: Lambda[R, str] | None = None, _inverted=False, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): @@ -854,11 +856,13 @@ def __init__( 'not both' ) self.__actual = actual + self.__describe_actual_result = _describe_actual_result self.__by = by self.__test = ( ConditionMismatch._to_raise_if_not( self.__by, self.__actual, + _describe_actual_result=self.__describe_actual_result, _falsy_exceptions=_falsy_exceptions, ) if self.__actual @@ -871,8 +875,7 @@ def __init__( ConditionMismatch._to_raise_if_actual( self.__actual, self.__by, - # TODO: should we DI? – remove this tight coupling to WebDriverException? - # here and elsewhere + _describe_actual_result=self.__describe_actual_result, _falsy_exceptions=_falsy_exceptions, ) if self.__actual @@ -958,6 +961,7 @@ def not_(self) -> Condition[E]: self.__description, actual=self.__actual, # type: ignore by=self.__by, + _describe_actual_result=self.__describe_actual_result, _inverted=not self.__inverted, _falsy_exceptions=self.__falsy_exceptions, ) @@ -985,7 +989,7 @@ def __describe_inverted(self) -> str: def __str__(self): return self.__describe() if not self.__inverted else self.__describe_inverted() - # TODO: we already have entity.matching for Callable[[E], bool] + # todo: we already have entity.matching for Callable[[E], bool] # is it a good idea to use same term for Callable[[E], None] raising error? # but is match vs matchING distinction clear enough? # like "Match it!" says "execute the order!" @@ -993,10 +997,39 @@ def __str__(self): # should we then add one more method to distinguish them? self.matching? # or self.is_matched? (but this will contradict with entity.matching) # still, self.match contradicts with pattern.match(string) that does not raise - # TODO: would a `test` be a better name? + # todo: would a `test` be a better name? # kind of test term relates to testing in context of assertions... # though naturally it does not feel like "assertion"... # more like "predicate" returning bool (True/False), not raising exception + # TODO: given named as test (or match, etc.)... what if we allow users to do asserts in Selene + # that are kind of "classic assertions", i.e. without waiting built in... + # then it may look something like this: + # > from selene import browser + # > from selene.core import match as assert_ + # > ... + # > browser.element('input').clear() + # > assert_.blank.test(browser.element('input')) + # > # OR: + # > assert_.blank.match(browser.element('input')) + # > # OR: + # > assert_.blank.matches(browser.element('input')) + # > # OR: + # > assert_.blank.matching(browser.element('input')) + # > # OR: + # > assert_.blank(browser.element('input')) #->❤️ + # hm... what about simply: + # > from selene import browser + # > from selene.core import match + # > ... + # > browser.element('input').clear() + # > match.blank(browser.element('input')) #->❤️ + # this looks also ok: + # > from selene import browser + # > from selene.core import match as expect + # > ... + # > browser.element('input').clear() + # > expect.blank(browser.element('input')) #->❤️ + # TODO: at least, we have to document – the #->❤️-marked recipes... def _test(self, entity: E) -> None: # currently refactored to be alias to __call__ to be in more compliance # with some subclasses implementations, that override __call__ @@ -1360,6 +1393,8 @@ def __init__x(self, actual: Lambda[E, R], by: Predicate[R]): ... # TODO: do we really need such complicated impl in order # to allow passing actual and by as positional arguments? + # also, take into account that currently the _describe_actual_result + # is not counted in the impl below def __init__x(self, *args, **kwargs): """ Valid signatures in usage: @@ -1443,6 +1478,7 @@ def __init__( actual: Lambda[E, R], *, by: Predicate[R], + _describe_actual_result: Lambda[R, str] | None = None, _inverted=False, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): ... @@ -1463,6 +1499,7 @@ def __init__( *, actual: Lambda[E, R], by: Predicate[R], + _describe_actual_result: Lambda[R, str] | None = None, _inverted=False, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): ... @@ -1484,11 +1521,12 @@ def __init__( actual: Lambda[E, R] | None = None, *, by: Predicate[E] | Predicate[R], + _describe_actual_result: Lambda[R, str] | None = None, _inverted=False, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): """ - The only valid signatures in usage: + The only valid and stable signatures in usage: ```python Match(by=lambda x: x > 0) @@ -1497,6 +1535,12 @@ def __init__( Match('has positive decrement', actual=lambda x: x - 1, by=lambda x: x > 0) Match(actual=lambda x: x - 1, by=lambda x: x > 0) ``` + + In addition to the examples above you can optionally add named + `_describe_actual_result` argument whenever you pass the `actual` argument. + You also can optionally provide _inverted and _falsy_exceptions arguments. + But keep in mind that they are marked with `_` prefix to indicate their + private and potentially "experimental" use, that can change in future versions. """ if not description and not (by_description := Query.full_description_for(by)): raise ValueError( @@ -1515,6 +1559,7 @@ def __init__( description=description, actual=actual, # type: ignore by=by, + _describe_actual_result=_describe_actual_result, _inverted=_inverted, _falsy_exceptions=_falsy_exceptions, ) diff --git a/selene/core/entity.py b/selene/core/entity.py index 9a47adff..3ddab493 100644 --- a/selene/core/entity.py +++ b/selene/core/entity.py @@ -628,6 +628,9 @@ def ss(self, css_or_xpath_or_by: Union[str, Tuple[str, str]]) -> Collection: return self.all(css_or_xpath_or_by) +# TODO: consider renaming or at list aliased to AllElements +# for better consistency with browser.all(selector) +# and maybe even aliased by All for nicer POM support via descriptors class Collection(WaitingEntity['Collection'], Iterable[Element]): def __init__(self, locator: Locator[typing.Sequence[WebElement]], config: Config): self._locator = locator diff --git a/selene/core/exceptions.py b/selene/core/exceptions.py index f8c24f24..2a157db4 100644 --- a/selene/core/exceptions.py +++ b/selene/core/exceptions.py @@ -101,7 +101,7 @@ def decremented(x) -> int: )(2) # ... ``` - """ + """ # todo: document _describe_actual_result examples def __init__(self, message='condition not matched'): super().__init__(message) @@ -123,6 +123,7 @@ def _to_raise_if_not( by: Callable[[R], bool], actual: Callable[[E], R] | None = None, *, + _describe_actual_result: Callable[[E | R], str] | None = None, _inverted: bool = False, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): ... @@ -134,6 +135,12 @@ def _to_raise_if_not( by: Callable[[E | R], bool], actual: Optional[Callable[[E], E | R]] = None, *, + # we can't name it: + # - as `describe` because it will not be clear what are we going to describe + # is it a description of the result of the whole comparison? or what? + # - as `describe_actual` because it will not be clear: + # do we describe actual as a query? or as a result of the query being called? + _describe_actual_result: Callable[[E | R], str] | None = None, _inverted: Optional[bool] = False, # todo: should we rename it to _exceptions_as_truthy_on_inverted? # or just document this in docstring? @@ -144,11 +151,19 @@ def _to_raise_if_not( @functools.wraps(by) def wrapped(entity: E) -> None: def describe_not_match(actual_value): - actual_description = ( - f' {name}' if (name := Query.full_description_for(actual)) else '' + describe_actual_result = _describe_actual_result or ( + lambda value: ( + f'actual' + + ( + f' {actual_name}' + if (actual_name := Query.full_description_for(actual)) + else '' + ) + + f': {value}' + ) ) return ( - f'actual{actual_description}: {actual_value}' + describe_actual_result(actual_value) if actual else ( ( @@ -238,10 +253,16 @@ def _to_raise_if( cls, by: Callable[[E | R], bool], actual: Callable[[E], R] | None = None, + *, + _describe_actual_result: Callable[[R], str] | None = None, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): return cls._to_raise_if_not( - by, actual, _inverted=True, _falsy_exceptions=_falsy_exceptions + by, + actual, + _describe_actual_result=_describe_actual_result, + _inverted=True, + _falsy_exceptions=_falsy_exceptions, ) @classmethod @@ -249,17 +270,28 @@ def _to_raise_if_not_actual( cls, query: Callable[[E], R], by: Callable[[R], bool], + *, + _describe_actual_result: Callable[[R], str] | None = None, ): - return cls._to_raise_if_not(by, query) + return cls._to_raise_if_not( + by, query, _describe_actual_result=_describe_actual_result + ) @classmethod def _to_raise_if_actual( cls, query: Callable[[E], R], by: Callable[[R], bool], + *, + _describe_actual_result: Callable[[R], str] | None = None, _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), ): - return cls._to_raise_if(by, query, _falsy_exceptions=_falsy_exceptions) + return cls._to_raise_if( + by, + query, + _describe_actual_result=_describe_actual_result, + _falsy_exceptions=_falsy_exceptions, + ) class ConditionNotMatchedError(ConditionMismatch): diff --git a/selene/core/match.py b/selene/core/match.py index 22aa3738..7f20bd82 100644 --- a/selene/core/match.py +++ b/selene/core/match.py @@ -25,28 +25,24 @@ import warnings from functools import reduce -from selenium.common import WebDriverException, NoSuchElementException +from selenium.common import NoSuchElementException +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement from typing_extensions import ( List, Any, Union, Iterable, Tuple, - Unpack, - TypedDict, - overload, - NotRequired, - cast, Literal, Dict, override, Callable, - AnyStr, - TypeVar, Self, + Type, ) -from selene.common import predicate, helpers +from selene.common import predicate, helpers, appium_tools from selene.core import query from selene.core.condition import Condition, Match from selene.core.conditions import ( @@ -58,13 +54,134 @@ from selene.core._browser import Browser +# GENERAL CONDITION BUILDERS ------------------------------------------------- # + + +class _ElementHasSomethingSupportingIgnoreCase(Match[Element]): + def __init__( + self, + description, + /, + expected, + actual, + by, + _ignore_case=False, + _inverted=False, + _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), + ): + self.__description = description + self.__actual = actual + self.__expected = expected + self.__by = by + self.__ignore_case = _ignore_case + self.__inverted = _inverted + self.__falsy_exceptions = _falsy_exceptions + + super().__init__( + description=( + f'{description}{" ignoring case:" if _ignore_case else ""} \'{expected}\'' + # todo: refactor to and change tests correspondingly: + # f'{" ignoring case:" if _ignore_case else ":"} «{expected}»' + ), + actual=actual, + by=lambda actual: ( + by(str(expected).lower())(str(actual).lower()) + if _ignore_case + else by(str(expected))(str(actual)) + ), + _inverted=_inverted, + _falsy_exceptions=_falsy_exceptions, + ) + + @property + def ignore_case(self) -> Condition[Element]: + return self.__class__( + self.__description, + self.__expected, + self.__actual, + self.__by, + _ignore_case=True, + _inverted=self.__inverted, + _falsy_exceptions=self.__falsy_exceptions, + ) + + +class _CollectionHasSomethingSupportingIgnoreCase(Match[Collection]): + def __init__( + self, + description, + /, + *expected: str | int | float | Iterable[str], + actual, + by, + _ignore_case=False, + _inverted=False, + _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,), + ): + self.__description = description + self.__actual = actual + self.__expected = expected + self.__by = by + self.__ignore_case = _ignore_case + self.__inverted = _inverted + self.__falsy_exceptions = _falsy_exceptions + + # TODO: should we store flattened version in self? + # how should we render nested expected in error? + # should we transform actual to same un-flattened structure as expected? + # (when rendering, of course) + + def compare(actual: Iterable) -> bool: + expected_flattened = helpers.flatten(expected) + str_lower = lambda some: str(some).lower() + return ( + by(map(str_lower, expected_flattened))(map(str_lower, actual)) + if _ignore_case + else by(map(str, expected_flattened))(map(str, actual)) + ) + + super().__init__( + description=( + f'{description}{" ignoring case:" if _ignore_case else ""}' + f' {list(expected)}' + # todo: refactor to and change tests correspondingly: + # f'{" ignoring case:" if _ignore_case else ":"} {expected}' + ), + actual=actual, + by=compare, + _inverted=_inverted, + _falsy_exceptions=_falsy_exceptions, + ) + + @property + def ignore_case(self) -> Condition[Collection]: + return self.__class__( + self.__description, + *self.__expected, + actual=self.__actual, + by=self.__by, + _ignore_case=True, + _inverted=self.__inverted, + _falsy_exceptions=self.__falsy_exceptions, + ) + + +# CONDITIONS ----------------------------------------------------------------- # + present_in_dom: Condition[Element] = Match( 'is present in DOM', actual=lambda element: element.locate(), by=lambda webelement: webelement is not None, + _describe_actual_result=lambda webelement: ( + f'actual html element: {webelement.get_attribute("outerHTML")}' + if not appium_tools._is_mobile_element(webelement) + else str(webelement) # todo: find out what best to log for mobile + ), _falsy_exceptions=(NoSuchElementException,), ) +# todo: consider refactoring so it would be similar to present_in_dom +# in context of details in error message, by utilizing _describe_actual_result absent_in_dom: Condition[Element] = Condition.as_not(present_in_dom, 'is absent in DOM') @@ -104,36 +221,28 @@ def __deprecated_is_existing(element: Element) -> bool: """Deprecated 'is existing' condition. Use present_in_dom instead.""" visible: Condition[Element] = Match( - 'is visible', - by=lambda element: element.locate().is_displayed(), - _falsy_exceptions=(NoSuchElementException,), -) - -# todo: remove once decide on the best implementation -__visible_with_actual: Condition[Element] = Match( 'is visible', actual=lambda element: element.locate(), by=lambda actual: actual.is_displayed(), - # TODO: consider adding describe_actual - # describe_actual=lambda actual: (actual and actual.get_attribute('outerHTML')), - # TODO: but implementation should count mobile case... + _describe_actual_result=lambda actual: ( + f'actual html element: {actual.get_attribute("outerHTML")}' + if not appium_tools._is_mobile_element(actual) + else str(actual) # todo: find out what best to log for mobile + ), + _falsy_exceptions=(NoSuchElementException,), ) -"""Alternative and disabled via protection prefix version of visible condition, -that will result in errors with unclear actual, something like: - actual: -But if we add support of describe_actual parameter to Match, we can provide error like: - actual: -""" # todo: remove once decide on the best implementation +# and at least documented in docs this version __visible_with_actual_as_tuple: Condition[Element] = Match( 'is visible', actual=lambda element: ( webelement := element.locate(), webelement.get_attribute('outerHTML'), ), - by=lambda actual: actual[0].is_displayed(), + by=lambda actual_element_and_outer_html: ( + actual_element_and_outer_html[0].is_displayed() + ), ) """Alternative and disabled via protection prefix version of visible condition, that will result in good error like: @@ -178,7 +287,11 @@ class js: ) -class _ElementHasText(Condition[Element]): +# todo: consider removing once moved to docs +class __x_ElementHasText(Condition[Element]): + """Just an example of a custom condition builder + based on inheritance from Condition[Element] + """ def __init__( self, @@ -222,18 +335,40 @@ def ignore_case(self) -> Condition[Element]: ) -def text(expected: str | int | float, _ignore_case=False, _inverted=False): - return _ElementHasText( +def __x_text(expected: str | int | float, _ignore_case=False, _inverted=False): + return __x_ElementHasText( expected, 'has text', predicate.includes, _ignore_case, _inverted=_inverted ) -def exact_text(expected: str | int | float, _ignore_case=False, _inverted=False): - return _ElementHasText( +def __x_exact_text(expected: str | int | float, _ignore_case=False, _inverted=False): + return __x_ElementHasText( expected, 'has exact text', predicate.equals, _ignore_case, _inverted=_inverted ) +def text(expected: str | int | float, _ignore_case=False, _inverted=False): + return _ElementHasSomethingSupportingIgnoreCase( + 'has text', + expected, + actual=query.text, + by=predicate.includes, + _ignore_case=_ignore_case, + _inverted=_inverted, + ) + + +def exact_text(expected: str | int | float, _ignore_case=False, _inverted=False): + return _ElementHasSomethingSupportingIgnoreCase( + 'has exact text', + expected, + actual=query.text, + by=predicate.equals, + _ignore_case=_ignore_case, + _inverted=_inverted, + ) + + class text_pattern(Condition[Element]): def __init__(self, expected: str, _flags=0, _inverted=False): @@ -371,128 +506,211 @@ def values_containing( ) -def element_has_attribute(name: str): - def attribute_value(element: Element): - return element.locate().get_attribute(name) - - def attribute_values(collection: Collection): - return [element.get_attribute(name) for element in collection()] +class attribute(Condition[Element]): - raw_attribute_condition = ElementCondition.raise_if_not_actual( - 'has attribute ' + name, attribute_value, predicate.is_truthy - ) - - # TODO: is it OK to have some collection conditions inside a thing named element_has_attribute ? o_O - class ConditionWithValues(ElementCondition): - def value( - self, expected: str | int | float, ignore_case=False - ) -> Condition[Element]: - if ignore_case: - warnings.warn( - 'ignore_case syntax is experimental and might change in future', - FutureWarning, - ) - return ElementCondition.raise_if_not_actual( - f"has attribute '{name}' with value '{expected}'", - attribute_value, - predicate.str_equals(expected, ignore_case), - ) - - def value_containing( - self, expected: str | int | float, ignore_case=False - ) -> Condition[Element]: - if ignore_case: - warnings.warn( - 'ignore_case syntax is experimental and might change in future', - FutureWarning, - ) - return ElementCondition.raise_if_not_actual( - f"has attribute '{name}' with value containing '{expected}'", - attribute_value, - predicate.str_includes(expected, ignore_case), - ) + def __init__(self, name: str, _inverted=False): + self.__expected = name + # self.__ignore_case = _ignore_case + self.__inverted = _inverted - def values( - self, *expected: str | int | float | Iterable[str] - ) -> Condition[Collection]: - expected_ = helpers.flatten(expected) + super().__init__( + f"has attribute '{name}'", + actual=query.attribute(name), + by=predicate.is_truthy, # todo: should it be more like .is_not_none? + _inverted=_inverted, + ) - return CollectionCondition.raise_if_not_actual( - f"has attribute '{name}' with values '{expected_}'", - attribute_values, - predicate.str_equals_to_list(expected_), - ) + def value(self, expected): + return _ElementHasSomethingSupportingIgnoreCase( + f"has attribute '{self.__expected}' with value", + expected=expected, + actual=query.attribute(self.__expected), + by=predicate.equals, + _inverted=self.__inverted, + ) - def values_containing( - self, *expected: str | int | float | Iterable[str] - ) -> Condition[Collection]: - expected_ = helpers.flatten(expected) + def value_containing(self, expected): + return _ElementHasSomethingSupportingIgnoreCase( + f"has attribute '{self.__expected}' with value containing", + expected=expected, + actual=query.attribute(self.__expected), + by=predicate.includes, + _inverted=self.__inverted, + ) - return CollectionCondition.raise_if_not_actual( - f"has attribute '{name}' with values containing '{expected_}'", - attribute_values, - predicate.str_equals_by_contains_to_list(expected_), - ) + def values(self, *expected: str | int | float | Iterable[str]): + return _CollectionHasSomethingSupportingIgnoreCase( + f"has attribute '{self.__expected}' with values", + *expected, + actual=query.attributes(self.__expected), + by=predicate.str_equals_to_list, + _inverted=self.__inverted, + ) - return ConditionWithValues( - str(raw_attribute_condition), test=raw_attribute_condition.__call__ - ) + def values_containing(self, *expected: str | int | float | Iterable[str]): + return _CollectionHasSomethingSupportingIgnoreCase( + f"has attribute '{self.__expected}' with values containing", + *expected, + actual=query.attributes(self.__expected), + by=predicate.str_equals_by_contains_to_list, + _inverted=self.__inverted, + ) -def element_has_value(expected: str | int | float) -> Condition[Element]: - return element_has_attribute('value').value(expected) +def value(expected: str | int | float, _inverted=False): + return attribute('value', _inverted).value(expected) -def element_has_value_containing(expected: str | int | float) -> Condition[Element]: - return element_has_attribute('value').value_containing(expected) +def value_containing(expected: str | int | float, _inverted=False): + return attribute('value', _inverted).value_containing(expected) -def collection_has_values( - *expected: str | int | float | Iterable[str], -) -> Condition[Collection]: - return element_has_attribute('value').values(*expected) +def values(*expected: str | int | float | Iterable[str], _inverted=False): + return attribute('value', _inverted).values(*expected) -def collection_has_values_containing( - *expected: str | int | float | Iterable[str], -) -> Condition[Collection]: - return element_has_attribute('value').values_containing(*expected) +def values_containing(*expected: str | int | float | Iterable[str], _inverted=False): + return attribute('value', _inverted).values_containing(*expected) -def element_has_css_class(expected: str) -> Condition[Element]: +def css_class(name: str, _inverted=False): def class_attribute_value(element: Element): return element.locate().get_attribute('class') - return ElementCondition.raise_if_not_actual( - f"has css class '{expected}'", - class_attribute_value, - predicate.includes_word(expected), + return _ElementHasSomethingSupportingIgnoreCase( + f"has css class", + expected=name, + actual=class_attribute_value, + by=predicate.includes_word, + _inverted=_inverted, ) -element_is_blank: Condition[Element] = exact_text('').and_(element_has_value('')) +# it can't be implemented as exact_text('').and_(value('')) +# because value of
  • ...
  • is '0'! o_O +blank: Condition[Element] = Match( + 'is blank', + actual=lambda element: ( + ('value', webelement.get_attribute('value')) + if (webelement := element.locate()) and webelement.tag_name == 'input' + else ('text', webelement.text) + ), + by=lambda actual_desc_and_result: not actual_desc_and_result[1], + # todo: document in docs the following: specifically in this case, + # providing _describe_actual_result is not like that important + # because if skipped the actual rendering would be just + # 'actual: (text|value, ...)' instead of 'actual text|value: ...' + # that is not much less informative... + # once documented, I would consider removing this customization... + _describe_actual_result=lambda actual_desc_and_result: ( + f'actual {actual_desc_and_result[0]}: {actual_desc_and_result[1]}' + ), +) +"""Asserts that element is blank, +i.e. has empty value if is an element or empty text otherwise. + +Is similar to the experimental 4-in-1 [_empty][selene.core.match._empty] condition, +works only for singular elements: if they are a value-based elements +(like inputs and textarea) then it checks for empty value, +if they are text-based elements then it checks for empty text content. + +The `_empty` condition works same but if applied to collection +then will assert that it has size 0. And when applied to "form" element, +then will check for all form "value-like" inputs to be empty. +Hence, the `blank` condition is more precise, while `empty` is more general. +Because of its generality, the `empty` condition can be easier to remember, +but can lead to some kind of confusion when applied to both element +and collection in same test. +""" -def element_has_tag( +# probably we don't need such over-complication of creating 2 conditions in 1 +# but let's leave it so far, just as an example of potentially possible impl. +def tag( expected: str, - describing_matched_to='has tag', - compared_by_predicate_to=predicate.equals, + _name='has tag', + _by=predicate.equals, ) -> Condition[Element]: - return ElementCondition.raise_if_not_actual( - f'{describing_matched_to} + {expected}', - query.tag, - compared_by_predicate_to(expected), - ) + return Match(f'{_name} {expected}', actual=query.tag, by=_by(expected)) -def element_has_tag_containing(expected: str) -> Condition[Element]: - return element_has_tag(expected, 'has tag containing', predicate.includes) +def tag_containing(expected: str) -> Condition[Element]: + return tag(expected, _name='has tag containing', _by=predicate.includes) -# TODO: should not we make empty to work on both elements and collections? -# to assert have.size(0) on collections -# to assert have.value('').and(have.exact_text('')) on element -def _is_collection_empty(collection: Collection) -> bool: +# todo: find the best way to type it... as Condition[Locatable] or smth like that... +# maybe even like as Condition[Callable] +def __size_or_value_or_text(entity: Callable): + snapshot = entity() + + if hasattr(snapshot, '__len__'): + return 'size', len(snapshot) + + if isinstance(snapshot, WebElement): + return ( + # TODO: what about input of type=color? it's empty value is '#000000' + ('value', snapshot.get_attribute('value')) + if (tag_name := snapshot.tag_name) == 'input' + else ( + ( + 'values of all form inputs, textareas and selects', + ''.join( + (element_with_value.get_attribute('value') or '') + for element_with_value in snapshot.find_elements( + By.CSS_SELECTOR, + 'textarea,' + 'input' + ':not([type=button])' + ':not([type=submit])' + ':not([type=reset])' + ':not([type=hidden])' + ':not([type=range])' # todo: should we count range as empty on 0 value? + # ':not([type=image])' # todo: value will be allwasy '', but should we count src? + ':not([type=color])' # todo: should we count color as empty on #000000 value? + ':not([type=checkbox]:not(:checked))' + ':not([type=radio]:not(:checked))' + ',' + 'input[type=checkbox]:checked,' + 'input[type=radio]:checked,' + 'select', + # todo: what if some file input will not have input field but path set? + ) + ), + ) + if tag_name == 'form' + # todo: should we count values as texts too? + # cause textarea value will be counted by default, but other inputs - not + # though it will be counted only if predefined between tags... + # todo: another weird: textarea with text in html, even after reset to "" in UI + # will still return text hardcoded in html... should we count this somehow? + else ('text', snapshot.text) + ) + ) + + if hasattr(snapshot, 'value'): + return 'value', snapshot.value + + if hasattr(snapshot, 'text'): + return 'text', snapshot.text + + return 'str', str(snapshot) + + +_empty: Condition[Callable] = Match( + 'is empty', + __size_or_value_or_text, # noqa + by=lambda actual_desc_and_result: not actual_desc_and_result[1], + _describe_actual_result=lambda actual_desc_and_result: ( + f'actual {actual_desc_and_result[0]}: {actual_desc_and_result[1]}' + ), +) +"""Experimental 4-in-1 "is form or input or element or collection empty" condition, +that is an alternative to old and deprecated the [empty][selene.core.match.empty] +collection condition. +""" # todo: document all details of implementation + + +def __is_empty(collection: Collection): warnings.warn( 'match.collection_is_empty or be.empty is deprecated; ' 'use more explicit and obvious have.size(0) instead', @@ -501,9 +719,14 @@ def _is_collection_empty(collection: Collection) -> bool: return len(collection()) == 0 -collection_is_empty: Condition[Collection] = CollectionCondition.raise_if_not( - 'is empty', _is_collection_empty # noqa +empty: Condition[Collection] = Match( + 'is empty', + __is_empty, # noqa + by=predicate.is_truthy, ) +"""Deprecated 'is empty' collection condition. +Use [size(0)][selene.core.match.size] instead. +""" def collection_has_size( @@ -541,6 +764,7 @@ def collection_has_size_less_than(expected: int) -> Condition[Collection]: return collection_has_size(expected, 'has size less than', predicate.is_less_than) +# todo: consider .should(have.size(10).or_less) ;) def collection_has_size_less_than_or_equal( expected: int, ) -> Condition[Collection]: @@ -611,10 +835,11 @@ def ignore_case(self) -> Condition[Collection]: def texts( *expected: str | int | float | Iterable[str], _ignore_case=False, _inverted=False ): - return _CollectionHasTexts( + return _CollectionHasSomethingSupportingIgnoreCase( + f"have texts", *expected, - _describing_matched_to='have texts', - _compared_by_predicate_to=predicate.equals_by_contains_to_list, + actual=query.visible_texts, + by=predicate.equals_by_contains_to_list, _ignore_case=_ignore_case, _inverted=_inverted, ) @@ -623,10 +848,11 @@ def texts( def exact_texts( *expected: str | int | float | Iterable[str], _ignore_case=False, _inverted=False ): - return _CollectionHasTexts( + return _CollectionHasSomethingSupportingIgnoreCase( + f"have exact texts", *expected, - _describing_matched_to='have exact texts', - _compared_by_predicate_to=predicate.equals_to_list, + actual=query.visible_texts, + by=predicate.equals_to_list, _ignore_case=_ignore_case, _inverted=_inverted, ) diff --git a/selene/support/conditions/be.py b/selene/support/conditions/be.py index 6b4e3fcf..b60f6e8d 100644 --- a/selene/support/conditions/be.py +++ b/selene/support/conditions/be.py @@ -38,12 +38,18 @@ clickable = match.clickable -blank = match.element_is_blank +blank = match.blank + + +_empty = match._empty # --- Deprecated --- # -empty = match.collection_is_empty +empty = match.empty +"""Deprecated 'is empty' condition. Use +[size(0)][selene.support.conditions.have.size] instead. +""" present = match.present diff --git a/selene/support/conditions/have.py b/selene/support/conditions/have.py index a37ccd12..305a9cfd 100644 --- a/selene/support/conditions/have.py +++ b/selene/support/conditions/have.py @@ -81,39 +81,39 @@ def attribute(name: str, value: Optional[str] = None): 'passing second argument is deprecated; use have.attribute(foo).value(bar) instead', DeprecationWarning, ) - return match.element_has_attribute(name).value(value) + return match.attribute(name).value(value) - return match.element_has_attribute(name) + return match.attribute(name) def value(text: str | int | float) -> Condition[Element]: - return match.element_has_value(text) + return match.value(text) def values(*texts: str | int | float | Iterable[str]) -> Condition[Collection]: - return match.collection_has_values(*texts) + return match.values(*texts) def value_containing(partial_text: str | int | float) -> Condition[Element]: - return match.element_has_value_containing(partial_text) + return match.value_containing(partial_text) def values_containing( *partial_texts: str | int | float | Iterable[str], ) -> Condition[Collection]: - return match.collection_has_values_containing(*partial_texts) + return match.values_containing(*partial_texts) -def css_class(name) -> Condition[Element]: - return match.element_has_css_class(name) +def css_class(name): + return match.css_class(name) def tag(name: str) -> Condition[Element]: - return match.element_has_tag(name) + return match.tag(name) def tag_containing(name: str) -> Condition[Element]: - return match.element_has_tag_containing(name) + return match.tag_containing(name) # *** SeleneCollection conditions *** diff --git a/selene/support/conditions/not_.py b/selene/support/conditions/not_.py index c51300f9..d7fb960f 100644 --- a/selene/support/conditions/not_.py +++ b/selene/support/conditions/not_.py @@ -52,7 +52,7 @@ # focused: Condition[Element] = _match.focused.not_ -blank: Condition[Element] = _match.element_is_blank.not_ +blank: Condition[Element] = _match.blank.not_ # --- be.not_.* DEPRECATED conditions --- # @@ -88,37 +88,11 @@ def attribute(name: str, *args, **kwargs): 'use have.attribute(foo).value(bar) instead', DeprecationWarning, ) - return ( - _match.element_has_attribute(name) - .value(args[0] if args else kwargs['value']) - .not_ + return _match.attribute(name, _inverted=True).value( + args[0] if args else kwargs['value'] ) - original = _match.element_has_attribute(name) - negated = original.not_ - - def value( - self, expected: str | int | float, ignore_case=False - ) -> Condition[Element]: - return original.value(expected, ignore_case).not_ - - def value_containing( - self, expected: str | int | float, ignore_case=False - ) -> Condition[Element]: - return original.value_containing(expected, ignore_case).not_ - - def values(self, *expected: str | int | float) -> Condition[Collection]: - return original.values(*expected).not_ - - def values_containing(self, *expected: str | int | float) -> Condition[Collection]: - return original.values_containing(*expected).not_ - - negated.value = value - negated.value_containing = value_containing - negated.values = values - negated.values_containing = values_containing - - return negated + return _match.attribute(name, _inverted=True) def js_property(name: str, *args, **kwargs): @@ -202,29 +176,39 @@ def values_containing(self, *expected: str) -> Condition[Collection]: return negated -def value(text: str | int | float) -> Condition[Element]: - return _match.element_has_value(text).not_ +def value(text: str | int | float): + return _match.value(text, _inverted=True) def value_containing(partial_text: str | int | float) -> Condition[Element]: - return _match.element_has_value_containing(partial_text).not_ + return _match.value_containing(partial_text, _inverted=True) -def css_class(name: str) -> Condition[Element]: - return _match.element_has_css_class(name).not_ +def css_class(name: str): + return _match.css_class(name, _inverted=True) def tag(name: str) -> Condition[Element]: - return _match.element_has_tag(name).not_ + return _match.tag(name).not_ def tag_containing(name: str) -> Condition[Element]: - return _match.element_has_tag_containing(name).not_ + return _match.tag_containing(name).not_ # *** SeleneCollection conditions *** +def values(*texts: str | int | float | Iterable[str]) -> Condition[Collection]: + return _match.values(*texts, _inverted=True) + + +def values_containing( + *partial_texts: str | int | float | Iterable[str], +) -> Condition[Collection]: + return _match.values_containing(*partial_texts, _inverted=True) + + def size(number: int) -> Condition[Collection]: return _match.collection_has_size(number).not_ @@ -331,3 +315,13 @@ def js_returned(expected: Any, script: str, *args) -> Condition[Browser]: def script_returned(expected: Any, script: str, *args) -> Condition[Browser]: return _match.browser_has_script_returned(expected, script, *args).not_ + + +_empty = _match._empty.not_ + + +# --- be.* DEPRECATED conditions --- # +empty = _match.empty.not_ +"""Deprecated 'is not empty' condition. Use +[size(0)][selene.support.conditions.have.size] instead. +""" diff --git a/tests/const.py b/tests/const.py index adc72428..d6b0fc1a 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,4 +1,9 @@ # TINYMCE_URL = 'https://www.tiny.cloud/docs/tinymce/latest/cloud-quick-start/' +from pathlib import Path + +import tests + TINYMCE_URL = 'https://autotest.how/demo/tinymce' # SELENOID_HOST = 'selenoid.autotests.cloud' SELENOID_HOST = 'selenoid.autotest.how' +LOGO_PATH = str(Path(tests.__file__).parent.parent / 'docs/assets/images/logo-icon.png') diff --git a/tests/integration/condition__collection__have_exact_texts_test.py b/tests/integration/condition__collection__have_exact_texts_test.py index 42ad0d9f..a240010a 100644 --- a/tests/integration/condition__collection__have_exact_texts_test.py +++ b/tests/integration/condition__collection__have_exact_texts_test.py @@ -95,7 +95,7 @@ def test_should_have_exact_texts_exception(session_browser): with pytest.raises(TimeoutException) as error: browser.all('li').should(have.exact_texts('Alex')) - assert ".have exact texts ('Alex',)" in error.value.msg + assert ".have exact texts ['Alex']" in error.value.msg assert ( "ConditionMismatch: actual visible texts: ['Alex', 'Yakov']" in error.value.msg ) diff --git a/tests/integration/condition__collection__have_texts_test.py b/tests/integration/condition__collection__have_texts_test.py index a5f7ad77..acf5a896 100644 --- a/tests/integration/condition__collection__have_texts_test.py +++ b/tests/integration/condition__collection__have_texts_test.py @@ -58,7 +58,7 @@ def test_should_have_texts_exception(session_browser): pytest.fail('should have failed on texts mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have texts ('Alex',)\n" + "browser.all(('css selector', 'li')).have texts ['Alex']\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['Alex', 'Yakov']\n" ) in str(error) @@ -95,7 +95,7 @@ def test_should_have_no_texts_exception(session_browser): with pytest.raises(TimeoutException) as error: browser.all('li').should(have.no.texts('Alex', 'Yakov')) # TODO: why do we have `has` below, should not it be `have`? - assert "have no (texts ('Alex', 'Yakov'))" in error.value.msg + assert "have no (texts ['Alex', 'Yakov'])" in error.value.msg assert ( "Reason: ConditionMismatch: actual visible texts: ['Alex', 'Yakov']\n" in error.value.msg @@ -144,7 +144,7 @@ def test_should_have_text_exception(session_browser): with pytest.raises(TimeoutException) as error: browser.all('li').should(have.text('Yakov').each) - assert "each has text Yakov" in error.value.msg + assert "each has text 'Yakov'" in error.value.msg assert ( "AssertionError: Not matched elements among all with indexes from 0 to 1:\n" "browser.all(('css selector', 'li')).cached[0]: actual text: Alex\n" @@ -178,7 +178,7 @@ def test_should_have_no_text_exception(session_browser): with pytest.raises(TimeoutException) as error: browser.all('li').should(have.no.text('Alex').each) - assert "each has no (text Alex)" in error.value.msg + assert "each has no (text 'Alex')" in error.value.msg assert ( "AssertionError: Not matched elements among all with indexes from 0 to 1:\n" "browser.all(('css selector', 'li')).cached[0]: actual text: Alex\n" diff --git a/tests/integration/condition__element__enabled__plus_inversions_and_aliases_test.py b/tests/integration/condition__element__enabled__plus_inversions_and_aliases_test.py index 06fbcbf2..af8e7f4f 100644 --- a/tests/integration/condition__element__enabled__plus_inversions_and_aliases_test.py +++ b/tests/integration/condition__element__enabled__plus_inversions_and_aliases_test.py @@ -327,8 +327,8 @@ def test_should_be_clickable__passed_and_failed(session_browser): assert ( "browser.element(('css selector', '#hidden')).is visible and is enabled\n" '\n' - 'Reason: ConditionMismatch: condition not matched\n' - 'Screenshot: ' + 'Reason: ConditionMismatch: actual html element: \n' ) in str(error) # - hidden & disabled fails with mismatch try: @@ -339,7 +339,8 @@ def test_should_be_clickable__passed_and_failed(session_browser): "browser.element(('css selector', '#hidden-disabled')).is visible and is " 'enabled\n' '\n' - 'Reason: ConditionMismatch: condition not matched\n' + 'Reason: ConditionMismatch: actual html element: \n' ) in str(error) # - absent fails with failure try: diff --git a/tests/integration/condition__element__have_css_class_test.py b/tests/integration/condition__element__have_css_class_test.py new file mode 100644 index 00000000..4bd41e4a --- /dev/null +++ b/tests/integration/condition__element__have_css_class_test.py @@ -0,0 +1,307 @@ +# 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: consider breaking it down into separate tests + + +def test_should_have_css_class__passed_and_failed(session_browser): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + ''' + ) + + # THEN + + # have css class? + # - visible and correct expected passes + s('#visible').should(match.css_class('one')) + s('#visible').should(have.css_class('one')) + s('#visible').should(have.css_class('one').not_.not_) + # - visible and incorrect expected fails + try: + s('#visible').should(have.css_class('One')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has css class 'One'\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: one two three\n' + 'Screenshot: ' + ) in str(error) + # - visible & empty and incorrect expected fails + try: + s('#visible-empty').should(have.css_class('one')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible-empty')).has css class 'one'\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: \n' + ) in str(error) + # - visible & empty and empty expected passes # todo: ok, right? + s('#visible-empty').should(have.css_class('')) + # - hidden & empty with always '' expected passes + s('#hidden-empty').should(have.css_class('')) + # - hidden and incorrect expected fails + '''skipped (pw)''' + # - absent and expected '' fails with failure + try: + s('#absent').should(have.css_class('')) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has css class ''\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ' (Session info: ' # 'chrome=126.0.6478.127); For documentation on this error, ' + # 'please visit: ' + # 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n' + # 'Screenshot: ' + ) in str(error) + # - absent and expected '' + double inversion fails with failure + '''skipped (pw)''' + + +def test_should_have_css_class__ignore_case__passed_and_failed(session_browser): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + ''' + ) + + # THEN + + # have css class ignore case? + # - visible and correct expected passes + s('#visible').should(match.css_class('ONE').ignore_case) + s('#visible').should(have.css_class('ONE').ignore_case) + s('#visible').should(have.css_class('ONE').ignore_case.not_.not_) + # - visible and incorrect expected fails + try: + s('#visible').should(have.css_class('o-n-e').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has css class ignoring case: 'o-n-e'\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: one two three\n' + 'Screenshot: ' + ) in str(error) + # - visible & empty and incorrect expected fails + try: + s('#visible-empty').should(have.css_class('o-n-e').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible-empty')).has css class ignoring case: 'o-n-e'\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: \n' + ) in str(error) + # - visible & empty and empty expected passes # todo: ok, right? + s('#visible-empty').should(have.css_class('').ignore_case) + # - hidden & empty with always '' expected passes + s('#hidden-empty').should(have.css_class('').ignore_case) + # - hidden and incorrect expected fails + '''skipped (pw)''' + # - absent and expected '' fails with failure + try: + s('#absent').should(have.css_class('').ignore_case) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has css class ignoring case: ''\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ' (Session info: ' # 'chrome=126.0.6478.127); For documentation on this error, ' + # 'please visit: ' + # 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n' + # 'Screenshot: ' + ) in str(error) + # - absent and expected '' + double inversion fails with failure + '''skipped (pw)''' + + +def test_should_have_no_css_class__passed_and_failed(session_browser): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + ''' + ) + + # THEN + + # have no css class? + # - visible and correct expected fails + try: + s('#visible').should(have.no.css_class('one')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has no (css class 'one')\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: one two three\n' + 'Screenshot: ' + ) in str(error) + # - visible and incorrect expected passes + s('#visible').should(match.css_class('One').not_) + s('#visible').should(have.no.css_class('One')) + s('#visible').should(have.no.css_class('One').not_.not_) + # - visible & empty and incorrect expected passes + s('#visible-empty').should(have.no.css_class('one')) + # - visible & empty and empty expected fails # todo: ok, right? + try: + s('#visible-empty').should(have.no.css_class('')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible-empty')).has no (css class '')\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: \n' + 'Screenshot: ' + ) in str(error) + # - hidden & empty with always '' expected fails + '''skipped (pw)''' + # - hidden and incorrect expected passes + s('#hidden').should(have.no.css_class('One')) + # - absent and expected '' fails with failure + try: + s('#absent').should(have.no.css_class('')) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has no (css class '')\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ' (Session info: ' # 'chrome=126.0.6478.127); For documentation on this error, ' + # 'please visit: ' + # 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n' + # 'Screenshot: ' + ) in str(error) + # - absent and expected '' + double inversion fails with failure + '''skipped (pw)''' + + +def test_should_have_no_css_class__ignore_case__passed_and_failed(session_browser): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + ''' + ) + + # THEN + + # have no css class? + # - visible and correct expected fails + try: + s('#visible').should(have.no.css_class('ONE').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has no (css class ignoring case: 'ONE')\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: one two three\n' + 'Screenshot: ' + ) in str(error) + # - visible and incorrect expected passes + s('#visible').should(match.css_class('o-n-e').ignore_case.not_) + s('#visible').should(have.no.css_class('o-n-e').ignore_case) + s('#visible').should(have.no.css_class('o-n-e').ignore_case.not_.not_) + # - visible & empty and incorrect expected passes + s('#visible-empty').should(have.no.css_class('o-n-e').ignore_case) + # - visible & empty and empty expected fails # todo: ok, right? + try: + s('#visible-empty').should(have.no.css_class('').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible-empty')).has no (css class ignoring case: '')\n" + '\n' + 'Reason: ConditionMismatch: actual class attribute value: \n' + 'Screenshot: ' + ) in str(error) + # - hidden & empty with always '' expected fails + '''skipped (pw)''' + # - hidden and incorrect expected passes + s('#hidden').should(have.no.css_class('o-n-e').ignore_case) + # - absent and expected '' fails with failure + try: + s('#absent').should(have.no.css_class('').ignore_case) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has no (css class ignoring case: '')\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ' (Session info: ' # 'chrome=126.0.6478.127); For documentation on this error, ' + # 'please visit: ' + # 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n' + # 'Screenshot: ' + ) in str(error) + # - absent and expected '' + double inversion fails with failure + '''skipped (pw)''' diff --git a/tests/integration/condition__element__have_exact_text_test.py b/tests/integration/condition__element__have_exact_text_test.py index 8f7d2b14..844c14cd 100644 --- a/tests/integration/condition__element__have_exact_text_test.py +++ b/tests/integration/condition__element__have_exact_text_test.py @@ -26,7 +26,7 @@ from tests.integration.helpers.givenpage import GivenPage -# TODO: consider breaking it down into separate tests +# todo: consider breaking it down into separate tests def test_should_have_exact_text__passed_and_failed__with_text_to_trim(session_browser): @@ -59,7 +59,7 @@ def test_should_have_exact_text__passed_and_failed__with_text_to_trim(session_br pytest.fail('expect mismatch') except AssertionError as error: assert ( - "browser.element(('css selector', '#visible')).has exact text One\n" + "browser.element(('css selector', '#visible')).has exact text 'One'\n" '\n' 'Reason: ConditionMismatch: actual text: One !!!\n' 'Screenshot: ' @@ -81,7 +81,7 @@ def test_should_have_exact_text__passed_and_failed__with_text_to_trim(session_br pytest.fail('expect mismatch') except AssertionError as error: assert ( - "browser.element(('css selector', '#hidden')).has exact text One !!!\n" + "browser.element(('css selector', '#hidden')).has exact text 'One !!!'\n" '\n' 'Reason: ConditionMismatch: actual text: \n' 'Screenshot: ' # ... @@ -92,7 +92,7 @@ def test_should_have_exact_text__passed_and_failed__with_text_to_trim(session_br pytest.fail('expect failure') except AssertionError as error: assert ( - "browser.element(('css selector', '#absent')).has exact text \n" + "browser.element(('css selector', '#absent')).has exact text ''\n" '\n' 'Reason: NoSuchElementException: no such element: Unable to locate element: ' '{"method":"css selector","selector":"#absent"}\n' @@ -107,7 +107,7 @@ def test_should_have_exact_text__passed_and_failed__with_text_to_trim(session_br pytest.fail('expect failure') except AssertionError as error: assert ( - "browser.element(('css selector', '#absent')).has exact text \n" + "browser.element(('css selector', '#absent')).has exact text ''\n" '\n' 'Reason: NoSuchElementException: no such element: Unable to locate element: ' '{"method":"css selector","selector":"#absent"}\n' @@ -153,7 +153,7 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( pytest.fail('expect mismatch') except AssertionError as error: assert ( - "browser.element(('css selector', '#visible')).has no (exact text One !!!)\n" + "browser.element(('css selector', '#visible')).has no (exact text 'One !!!')\n" '\n' 'Reason: ConditionMismatch: actual text: One !!!\n' 'Screenshot: ' # ... @@ -164,7 +164,7 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( pytest.fail('expect mismatch') except AssertionError as error: assert ( - "browser.element(('css selector', '#visible-empty')).has no (exact text )\n" + "browser.element(('css selector', '#visible-empty')).has no (exact text '')\n" '\n' 'Reason: ConditionMismatch: actual text: \n' 'Screenshot: ' # ... @@ -179,7 +179,7 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( pytest.fail('expect mismatch') except AssertionError as error: assert ( - "browser.element(('css selector', '#hidden')).has no (exact text )\n" + "browser.element(('css selector', '#hidden')).has no (exact text '')\n" '\n' 'Reason: ConditionMismatch: actual text: \n' 'Screenshot: ' # ... @@ -190,7 +190,7 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( pytest.fail('expect mismatch') except AssertionError as error: assert ( - "browser.element(('css selector', '#hidden-empty')).has no (exact text )\n" + "browser.element(('css selector', '#hidden-empty')).has no (exact text '')\n" '\n' 'Reason: ConditionMismatch: actual text: \n' 'Screenshot: ' # ... @@ -205,7 +205,7 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( pytest.fail('expect failure') except AssertionError as error: assert ( - "browser.element(('css selector', '#absent')).has no (exact text )\n" + "browser.element(('css selector', '#absent')).has no (exact text '')\n" '\n' 'Reason: NoSuchElementException: no such element: Unable to locate element: ' '{"method":"css selector","selector":"#absent"}\n' @@ -220,7 +220,7 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( pytest.fail('expect failure') except AssertionError as error: assert ( - "browser.element(('css selector', '#absent')).has no (exact text )\n" + "browser.element(('css selector', '#absent')).has no (exact text '')\n" '\n' 'Reason: NoSuchElementException: no such element: Unable to locate element: ' '{"method":"css selector","selector":"#absent"}\n' @@ -235,7 +235,7 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( pytest.fail('expect failure') except AssertionError as error: assert ( - "browser.element(('css selector', '#absent')).has no (exact text One !!!)\n" + "browser.element(('css selector', '#absent')).has no (exact text 'One !!!')\n" '\n' 'Reason: NoSuchElementException: no such element: Unable to locate element: ' '{"method":"css selector","selector":"#absent"}\n' diff --git a/tests/integration/condition__element__is_blank_test.py b/tests/integration/condition__element__is_blank_test.py new file mode 100644 index 00000000..0e6f11f9 --- /dev/null +++ b/tests/integration/condition__element__is_blank_test.py @@ -0,0 +1,234 @@ +# 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, be +from selene.core import match +from tests.integration.helpers.givenpage import GivenPage + + +# todo: consider breaking it down into separate tests + + +def test_should_be_blank__passed_and_failed(session_browser): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + + + + + + ''' + ) + + # THEN + + # be blank? + # - visible empty non-input passes + s('li#visible-empty').should(match.blank) + s('li#visible-empty').should(be.blank) + s('li#visible-empty').should(be.blank.not_.not_) + # - visible empty input passes + s('input#visible-empty').should(match.blank) + s('input#visible-empty').should(be.blank) + s('input#visible-empty').should(be.blank.not_.not_) + # - visible non-empty non-input fails + try: + s('li#visible').should(be.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#visible')).is blank\n" + '\n' + 'Reason: ConditionMismatch: actual text: One !!!\n' + 'Screenshot: ' + ) in str(error) + # - visible non-empty input fails + try: + s('input#visible').should(be.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'input#visible')).is blank\n" + '\n' + 'Reason: ConditionMismatch: actual value: One !!!\n' + 'Screenshot: ' + ) in str(error) + # - hidden empty non-input passes + s('li#hidden-empty').should(match.blank) + s('li#hidden-empty').should(be.blank) + s('li#hidden-empty').should(be.blank.not_.not_) + # - hidden empty input passes + s('input#hidden-empty').should(match.blank) + s('input#hidden-empty').should(be.blank) + s('input#hidden-empty').should(be.blank.not_.not_) + # - hidden non-empty non-input passes + # (because checks text that is empty for hidden) + s('li#hidden').should(match.blank) + s('li#hidden').should(be.blank) + s('li#hidden').should(be.blank.not_.not_) + # - hidden non-empty input fails + # (because value can be get from hidden element and is not blank) + try: + s('input#hidden').should(be.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'input#hidden')).is blank\n" + '\n' + 'Reason: ConditionMismatch: actual value: One !!!\n' + 'Screenshot: ' + ) in str(error) + # - absent fails with failure + try: + s('#absent').should(be.blank) + pytest.fail('expect FAILURE') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).is blank\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ' (Session info: ' # 'chrome=126.0.6478.127); For documentation on this error, ' + # 'please visit: ' + # 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n' + ) in str(error) + + +def test_should_be_not_blank__passed_and_failed(session_browser): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + + + + + + ''' + ) + + # THEN + + # be not blank? + # - visible empty non-input fails + try: + # s('li#visible-empty').should(match.blank.not_) + s('li#visible-empty').should(be.not_.blank) + # s('li#visible-empty').should(be.not_.blank.not_.not_) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#visible-empty')).is not (blank)\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + 'Screenshot: ' + ) in str(error) + # - visible empty input fails + try: + # s('input#visible-empty').should(match.blank.not_) + s('input#visible-empty').should(be.not_.blank) + # s('input#visible-empty').should(be.not_.blank.not_.not_) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'input#visible-empty')).is not (blank)\n" + '\n' + 'Reason: ConditionMismatch: actual value: \n' + 'Screenshot: ' + ) in str(error) + # - visible non-empty non-input passes + s('li#visible').should(match.blank.not_) + s('li#visible').should(be.not_.blank) + s('li#visible').should(be.not_.blank.not_.not_) + # - visible non-empty input fails + s('input#visible').should(be.not_.blank) + # - hidden empty non-input fails + try: + s('li#hidden-empty').should(be.not_.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#hidden-empty')).is not (blank)\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + 'Screenshot: ' + ) in str(error) + # - hidden empty input passes + try: + s('input#hidden-empty').should(be.not_.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'input#hidden-empty')).is not (blank)\n" + '\n' + 'Reason: ConditionMismatch: actual value: \n' + 'Screenshot: ' + ) in str(error) + # - hidden non-empty non-input passes + # (because checks text that is empty for hidden) + try: + s('li#hidden').should(be.not_.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#hidden')).is not (blank)\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + 'Screenshot: ' + ) in str(error) + # - hidden non-empty input passes + # (because value can be got from hidden element and is not blank) + s('input#hidden').should(be.not_.blank) + # - absent fails with failure + try: + s('#absent').should(be.not_.blank) + pytest.fail('expect FAILURE') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).is not (blank)\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ' (Session info: ' # 'chrome=126.0.6478.127); For documentation on this error, ' + # 'please visit: ' + # 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n' + ) in str(error) diff --git a/tests/integration/condition__element__present_visible__plus_inversions__compared_test.py b/tests/integration/condition__element__present_visible__plus_inversions__compared_test.py index af7cdb77..f794b60a 100644 --- a/tests/integration/condition__element__present_visible__plus_inversions__compared_test.py +++ b/tests/integration/condition__element__present_visible__plus_inversions__compared_test.py @@ -83,10 +83,9 @@ def test_should_be_hidden__passed_and_failed__compared_to_be_visible(session_bro assert ( "browser.element(('css selector', '#hidden')).is not (present in DOM)\n" '\n' - 'Reason: ConditionMismatch: actual: ' - '\n' + 'Reason: ConditionMismatch: actual html element: \n' + 'Screenshot: ' ) in str(error) # - visible fails try: @@ -96,10 +95,8 @@ def test_should_be_hidden__passed_and_failed__compared_to_be_visible(session_bro assert ( "browser.element(('css selector', '#visible')).is not (present in DOM)\n" '\n' - 'Reason: ConditionMismatch: actual: ' - '\n' + 'Reason: ConditionMismatch: actual html element: \n' ) in str(error) # absent in dom? @@ -174,7 +171,8 @@ def test_should_be_hidden__passed_and_failed__compared_to_be_visible(session_bro "browser.element(('css selector', '#visible')).is present in DOM and is not " '(visible)\n' '\n' - 'Reason: ConditionMismatch: condition not matched' + 'Reason: ConditionMismatch: actual html element: \n' ) in str(error) # not hidden in dom? @@ -305,7 +303,8 @@ def test_visible__passed_and_failed__compared_to_inline_Match( assert ( "browser.element(('css selector', '#hidden')).is visible\n" '\n' - 'Reason: ConditionMismatch: condition not matched\n' + 'Reason: ConditionMismatch: actual html element: \n' ) in str(error) # - absent fails try: @@ -349,7 +348,7 @@ def test_not_visible__passed_and_failed( # - absent passes absent.should(be.not_.visible) absent.should(be.not_.visible.not_.not_) - # - visible fails # todo: CONSIDER: logging webelement info, like outerHTML + # - visible fails try: visible.should(be.not_.visible) pytest.fail('expect mismatch') @@ -357,7 +356,8 @@ def test_not_visible__passed_and_failed( assert ( "browser.element(('css selector', '#visible')).is not (visible)\n" '\n' - 'Reason: ConditionMismatch: condition not matched\n' + 'Reason: ConditionMismatch: actual html element: \n' ) in str(error) diff --git a/tests/integration/condition__elements__have_attribute_and_co_test.py b/tests/integration/condition__elements__have_attribute_and_co_test.py index 9686e16b..946cee1c 100644 --- a/tests/integration/condition__elements__have_attribute_and_co_test.py +++ b/tests/integration/condition__elements__have_attribute_and_co_test.py @@ -22,14 +22,16 @@ import pytest from selene import have +from selene.core import match from tests.integration.helpers.givenpage import GivenPage -# TODO: consider breaking it down into separate tests +# todo: consider breaking it down into separate tests def test_have_attribute__condition_variations(session_browser): browser = session_browser.with_(timeout=0.1) + ss = lambda selector: browser.all(selector) GivenPage(session_browser.driver).opened_with_body( '''
      Hey: @@ -53,27 +55,161 @@ def test_have_attribute__condition_variations(session_browser): names.should(have.attribute('id').values_containing('first', 'last')) exercises.should(have.attribute('value').values(20, 30)) + try: + exercises.should(have.attribute('value').values(2, 3)) + pytest.fail('should fail on values mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.exercise')).has attribute 'value' with values " + "[2, 3]\n" + '\n' + "Reason: ConditionMismatch: actual value attributes: ['20', '30']\n" + 'Screenshot: ' + ) in str(error) + exercises.should(match.attribute('value').values(2, 3).not_) + exercises.should(match.values(2, 3).not_) + exercises.should(have.attribute('value').values(2, 3).not_) + exercises.should(have.values(2, 3).not_) + exercises.should(have.no.attribute('value').values(2, 3)) + exercises.should(have.no.values(2, 3)) names.should(have.attribute('value').values_containing(20, 2)) + try: + exercises.should(have.attribute('value').values_containing(200, 300)) + pytest.fail('should fail on values mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.exercise')).has attribute 'value' with values " + "containing [200, 300]\n" + '\n' + "Reason: ConditionMismatch: actual value attributes: ['20', '30']\n" + 'Screenshot: ' + ) in str(error) names.should(have.attribute('id').values_containing(20, 2).not_) + names.should(have.no.attribute('id').values_containing(20, 2)) try: - names.should(have.attribute('value').values_containing(20, 2).not_) + names.should(have.no.attribute('value').values_containing(20, 2)) pytest.fail('should fail on values mismatch') except AssertionError as error: assert ( - 'Timed out after 0.1s, while waiting for:\n' "browser.all(('css selector', '.name')).has no (attribute 'value' with values " - "containing '(20, 2)')\n" + "containing [20, 2])\n" '\n' - "Reason: ConditionMismatch: actual attribute values: ['John 20th', 'Doe " + "Reason: ConditionMismatch: actual value attributes: ['John 20th', 'Doe " "2nd']\n" ) in str(error) + names.should(match.attribute('id').value('name').each.not_) names.should(have.attribute('id').value('name').each.not_) + names.should(have.no.attribute('id').value('name').each) exercises.first.should(have.attribute('value').value(20)) exercises.first.should(have.attribute('value').value_containing(2)) - # elements.should(have.no.attribute('id').value('name').each) # TODO: fix names.should(have.attribute('id').value_containing('name').each) + try: + names.should(have.attribute('id').value_containing('first').each) + pytest.fail('should fail on values mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.name')). each has attribute 'id' with value " + "containing 'first'\n" + '\n' + 'Reason: AssertionError: Not matched elements among all with indexes from 0 ' + 'to 1:\n' + "browser.all(('css selector', '.name')).cached[1]: actual attribute id: " + 'lastname\n' + 'Screenshot: ' + ) in str(error) + # assuming two inputs for names: #firstname and #lastname + # not each name contain 'first' (one contain 'last' instead of 'first') names.should(have.attribute('id').value_containing('first').each.not_) + # but each can not contain 'first'! ;) + try: + names.should(have.no.attribute('id').value_containing('first').each) + pytest.fail('should fail on values mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.name')). each has no (attribute 'id' with " + "value containing 'first')\n" + '\n' + 'Reason: AssertionError: Not matched elements among all with indexes from 0 ' + 'to 1:\n' + "browser.all(('css selector', '.name')).cached[0]: actual attribute id: " + 'firstname\n' + 'Screenshot: ' + ) in str(error) + + # with .or_ + ss('.name').should( + have.attribute('id') + .value_containing('first') + .or_(have.attribute('id').value_containing('last')) + .each + ) + try: + ss('.name').should( + have.attribute('id') + .value_containing('first') + .or_(have.attribute('id').value_containing('last')) + .each.not_ + ) + pytest.fail('should fail on values mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.name')).not ( each has attribute 'id' with " + "value containing 'first' or has attribute 'id' with value containing " + "'last')\n" + '\n' + 'Reason: ConditionMismatch: condition not matched\n' # todo: improve details + ) in str(error) + ss('.exercise').should( + have.no.attribute('id') + .value_containing('push') + .or_(have.no.attribute('id').value_containing('pull')) + .each + ) + try: + ss('.exercise').should( + have.no.attribute('id') + .value_containing('pu') + .or_(have.no.attribute('id').value_containing('up')) + .each + ) + pytest.fail('should fail on values mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.exercise')). each has no (attribute 'id' with " + "value containing 'pu') or has no (attribute 'id' with value containing " + "'up')\n" + '\n' + 'Reason: AssertionError: Not matched elements among all with indexes from 0 ' + 'to 1:\n' + "browser.all(('css selector', '.exercise')).cached[0]: actual attribute id: " + 'pullup; actual attribute id: pullup\n' # todo: why is it doubled? – fix if needed + "browser.all(('css selector', '.exercise')).cached[1]: actual attribute id: " + 'pushup; actual attribute id: pushup\n' + ) in str(error) + try: + ss('.name,.exercise').should( + have.attribute('id') + .value_containing('first') + .or_(have.attribute('id').value_containing('last')) + .each + ) + pytest.fail('should fail on values mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.name,.exercise')). each has attribute 'id' " + "with value containing 'first' or has attribute 'id' with value containing " + "'last'\n" + '\n' + 'Reason: AssertionError: Not matched elements among all with indexes from 0 ' + 'to 3:\n' + "browser.all(('css selector', '.name,.exercise')).cached[2]: actual attribute " + 'id: pullup; actual attribute id: pullup\n' + "browser.all(('css selector', '.name,.exercise')).cached[3]: actual attribute " + 'id: pushup; actual attribute id: pushup\n' + ) in str(error) + + # todo: add .and_ tests similar to .or_ ones # aliases exercises.first.should(have.value(20)) diff --git a/tests/integration/condition__elements__have_text_and_co_test.py b/tests/integration/condition__elements__have_text_and_co_test.py index 3d80a030..6171f055 100644 --- a/tests/integration/condition__elements__have_text_and_co_test.py +++ b/tests/integration/condition__elements__have_text_and_co_test.py @@ -64,7 +64,7 @@ def test_have_text__condition_variations(session_browser): except AssertionError as error: assert ( 'Timed out after 0.1s, while waiting for:\n' - "browser.all(('css selector', '.name')).have no (texts (20, 2))\n" + "browser.all(('css selector', '.name')).have no (texts [20, 2])\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['John 20th', 'Doe 2nd']\n" ) in str(error) @@ -91,7 +91,7 @@ def test_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has text ignoring case: one.\n" + "browser.all(('css selector', 'li'))[0].has text ignoring case: 'one.'\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -103,7 +103,7 @@ def test_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has no (text ignoring case: one)\n" + "browser.all(('css selector', 'li'))[0].has no (text ignoring case: 'one')\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -115,7 +115,7 @@ def test_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has no (text ignoring case: one)\n" + "browser.all(('css selector', 'li'))[0].has no (text ignoring case: 'one')\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -143,7 +143,7 @@ def test_exact_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has exact text One\n" + "browser.all(('css selector', 'li'))[0].has exact text 'One'\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -152,7 +152,7 @@ def test_exact_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has exact text ignoring case: one\n" + "browser.all(('css selector', 'li'))[0].has exact text ignoring case: 'one'\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -164,7 +164,7 @@ def test_exact_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has no (exact text 1) One!!!)\n" + "browser.all(('css selector', 'li'))[0].has no (exact text '1) One!!!')\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -174,7 +174,7 @@ def test_exact_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has no (exact text ignoring case: 1) one!!!)\n" + "browser.all(('css selector', 'li'))[0].has no (exact text ignoring case: '1) one!!!')\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -185,7 +185,7 @@ def test_exact_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has no (exact text 1) One!!!)\n" + "browser.all(('css selector', 'li'))[0].has no (exact text '1) One!!!')\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -195,7 +195,7 @@ def test_exact_text__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li'))[0].has no (exact text ignoring case: 1) One!!!)\n" + "browser.all(('css selector', 'li'))[0].has no (exact text ignoring case: '1) One!!!')\n" '\n' 'Reason: ConditionMismatch: actual text: 1) One!!!\n' ) in str(error) @@ -222,8 +222,8 @@ def test_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have texts ('one', " - "'two', 'three')\n" + "browser.all(('css selector', 'li')).have texts ['one', " + "'two', 'three']\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -236,8 +236,8 @@ def test_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have no (texts ('One', " - "'Two', 'Three'))\n" + "browser.all(('css selector', 'li')).have no (texts ['One', " + "'Two', 'Three'])\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -250,8 +250,8 @@ def test_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have texts ignoring case: ('one.', " - "'two.', 'three.')\n" + "browser.all(('css selector', 'li')).have texts ignoring case: ['one.', " + "'two.', 'three.']\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -264,8 +264,8 @@ def test_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have no (texts ignoring case: ('one', " - "'two', 'three'))\n" + "browser.all(('css selector', 'li')).have no (texts ignoring case: ['one', " + "'two', 'three'])\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -278,8 +278,8 @@ def test_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have no (texts ignoring case: ('one', " - "'two', 'three'))\n" + "browser.all(('css selector', 'li')).have no (texts ignoring case: ['one', " + "'two', 'three'])\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -307,8 +307,8 @@ def test_exact_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected text mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have exact texts ('One', 'Two', " - "'Three')\n" + "browser.all(('css selector', 'li')).have exact texts ['One', 'Two', " + "'Three']\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -323,8 +323,8 @@ def test_exact_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have no (exact texts ('1) One!!!', '2) " - "Two...', '3) Three???'))\n" + "browser.all(('css selector', 'li')).have no (exact texts ['1) One!!!', '2) " + "Two...', '3) Three???'])\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -341,8 +341,8 @@ def test_exact_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have exact texts ignoring case: ('one.', " - "'two.', 'three.')\n" + "browser.all(('css selector', 'li')).have exact texts ignoring case: ['one.', " + "'two.', 'three.']\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -357,8 +357,8 @@ def test_exact_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have no (exact texts ignoring case: ('1) " - "one!!!', '2) two...', '3) three???'))\n" + "browser.all(('css selector', 'li')).have no (exact texts ignoring case: ['1) " + "one!!!', '2) two...', '3) three???'])\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" @@ -375,8 +375,8 @@ def test_exact_texts__including_ignorecase__passed_compared_to_failed( pytest.fail('expected mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', 'li')).have no (exact texts ignoring case: ('1) " - "one!!!', '2) two...', '3) three???'))\n" + "browser.all(('css selector', 'li')).have no (exact texts ignoring case: ['1) " + "one!!!', '2) two...', '3) three???'])\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['1) One!!!', '2) Two...', '3) " "Three???']\n" diff --git a/tests/integration/condition__mixed_test.py b/tests/integration/condition__mixed_test.py new file mode 100644 index 00000000..9bcf367c --- /dev/null +++ b/tests/integration/condition__mixed_test.py @@ -0,0 +1,465 @@ +# 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, be +from selene.core import match +from tests.integration.helpers.givenpage import GivenPage +from tests import const + + +# todo: consider breaking it down into separate tests + + +def test_should_match_different_things(session_browser): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + ss = lambda selector: session_browser.with_(timeout=0.1).all(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' +
        + + + +
      • +
      • One !!! +
      • +
      + + + + + + +
        Hey: +
      • +
      • +
      +
        Your training today: +
      • +
      • +
      + ''' + ) + + # THEN + + # have tag? + # - visible passes + s('li#visible').should(match.tag('li')) + s('li#visible').should(have.tag('li')) + s('input#visible').should(have.no.tag('input').not_) + s('input#visible').should(have.tag('input').not_.not_) + # - hidden passes + s('li#hidden').should(match.tag('li')) + s('li#hidden').should(have.tag('li')) + s('input#hidden').should(have.no.tag('input').not_) + s('input#hidden').should(have.tag('input').not_.not_) + # have tag containing? + # - visible passes + s('li#visible').should(match.tag_containing('l')) + s('li#visible').should(have.tag_containing('l')) + s('input#visible').should(have.no.tag_containing('in').not_) + s('input#visible').should(have.tag_containing('in').not_.not_) + # - hidden passes + s('li#hidden').should(match.tag_containing('l')) + s('li#hidden').should(have.tag_containing('l')) + s('input#hidden').should(have.no.tag_containing('in').not_) + s('input#hidden').should(have.tag_containing('in').not_.not_) + # absent fails with failure + try: + s('li#absent').should(have.tag('li')) + pytest.fail('expect FAILURE') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#absent')).has tag li\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"li#absent"}\n' + ) in str(error) + # have no tag? + s('li#visible').should(have.no.tag('input')) + s('input#visible').should(have.no.tag('li')) + # have no tag containing? + s('li#visible').should(have.no.tag_containing('in')) + s('input#visible').should(have.no.tag_containing('l')) + + +def test_should_be_emtpy__applied_to_non_form__passed_and_failed__compared( + session_browser, +): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + ss = lambda selector: session_browser.with_(timeout=0.1).all(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' +
        + + + +
      • +
      • One !!! +
      • +
      + +
      +
      + + +
      +
      + + +
      +
      +
      +
      + + + +
      +
      + + + +
      +
      + +
        Hey: +
      • +
      • +
      +
        Your training today: +
      • +
      • +
      + ''' + ) + + # be empty vs have size vs be blank + inverted? + ss('.exercise').should(have.size(2)) + ss('.exercise').should(have.no.size(0)) + ss('.exercise').should(be.not_._empty) + ss('#visible').should(have.size(2)) + ss('#visible').should(have.no.size(0)) + ss('#visible').should(be.not_._empty) + ss('#hidden').should(have.size(2)) + try: + ss('#hidden').should(have.size(0)) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '#hidden')).has size 0\n" + '\n' + 'Reason: ConditionMismatch: actual size: 2\n' + ) in str(error) + try: + ss('#hidden').should(be._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '#hidden')).is empty\n" + '\n' + 'Reason: ConditionMismatch: actual size: 2\n' + ) in str(error) + try: + s('input#visible').should(be.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'input#visible')).is blank\n" + '\n' + 'Reason: ConditionMismatch: actual value: One !!!\n' + ) in str(error) + try: + s('input#visible').should(be._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'input#visible')).is empty\n" + '\n' + 'Reason: ConditionMismatch: actual value: One !!!\n' + ) in str(error) + try: + s('li#visible').should(be.blank) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#visible')).is blank\n" + '\n' + 'Reason: ConditionMismatch: actual text: One !!!\n' + ) in str(error) + try: + s('li#visible').should(be._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#visible')).is empty\n" + '\n' + 'Reason: ConditionMismatch: actual text: One !!!\n' + ) in str(error) + ss('#hidden').should(have.no.size(0)) + ss('#hidden').should(be.not_._empty) + ss('.absent').should(have.size(0)) + try: + ss('.absent').should(have.no.size(0)) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.absent')).has no (size 0)\n" + '\n' + 'Reason: ConditionMismatch: actual size: 0\n' + ) in str(error) + ss('.absent').should(be._empty) + try: + ss('.absent').should(be.not_._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.absent')).is not (empty)\n" + '\n' + 'Reason: ConditionMismatch: actual size: 0\n' + ) in str(error) + s('li#visible-empty').should(be.blank) + s('input#visible-empty').should(be.blank) + s('li#visible').should(be.not_.blank) + s('input#visible').should(be.not_.blank) + s('li#visible-empty').should(be._empty) + s('input#visible-empty').should(be._empty) + s('li#visible').should(be.not_._empty) + s('input#visible').should(be.not_._empty) + + # non-form container elements are considered empty if there is no text inside + s('#form-no-text-with-values div#empty-inputs').should(be._empty) + s('#form-no-text-with-values div#non-empty-inputs').should(be._empty) + s('#form-with-text-with-values div#empty-inputs').should(be.not_._empty) + s('#form-with-text-with-values div#non-empty-inputs').should(be.not_._empty) + + +def test_should_be_emtpy__applied_to_form__passed_and_failed( + session_browser, +): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + ss = lambda selector: session_browser.with_(timeout=0.1).all(selector) + GivenPage(session_browser.driver).opened_with_body( + f''' +
      + + + + + + + + +
      + + +
      + + + + + + + + + + + + + + + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + + +
      + +
      + + + + + + + + +
      + + +
      + + + + + + + + + + + + + + + +
      + ''' + ) + s('#form-no-text-with-values textarea').type('textarea-with-value-no-text;') + + # form element is considered empty if all "text-value" fields are empty + s('#form-no-text-with-values').should(be.not_._empty) + try: + s('#form-no-text-with-values').should(be._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#form-no-text-with-values')).is empty\n" + '\n' + 'Reason: ConditionMismatch: actual values of all form inputs, textareas and ' + 'selects: ' + 'textarea-with-value-no-text;no-type-with-value;type-text-with-value;BikeHTMLvolvo\n' + 'Screenshot: ' + ) in str(error) + s('#form-with-text-with-values').should(be.not_._empty) + try: + s('#form-with-text-with-values').should(be._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#form-with-text-with-values')).is empty\n" + '\n' + 'Reason: ConditionMismatch: actual values of all form inputs, textareas and selects: ' + 'textarea-with-value-with-text;no-type-with-value;type-text-with-value;BikeHTMLvolvo\n' + ) in str(error) + s('#form-no-text-no-values').should(be._empty) + try: + s('#form-no-text-no-values').should(be.not_._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#form-no-text-no-values')).is not (empty)\n" + '\n' + 'Reason: ConditionMismatch: actual values of all form inputs, textareas and ' + 'selects: \n' # todo: make empty string explicit via quotes (here and everywhere) + 'Screenshot: ' + ) in str(error) + s('#form-with-text-no-values').should(be._empty) + try: + s('#form-with-text-no-values').should(be.not_._empty) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#form-with-text-no-values')).is not " + '(empty)\n' + '\n' + 'Reason: ConditionMismatch: actual values of all form inputs, textareas and ' + 'selects: \n' + 'Screenshot: ' + ) in str(error) diff --git a/tests/integration/condition_each_test.py b/tests/integration/condition_each_test.py index 7ca1c895..b8b80560 100644 --- a/tests/integration/condition_each_test.py +++ b/tests/integration/condition_each_test.py @@ -47,7 +47,7 @@ def test_on_collection(session_browser): elements.should(have.text('Ron').each) except TimeoutException as error: assert ( - "browser.all(('css selector', 'li')). each has text Ron\n" + "browser.all(('css selector', 'li')). each has text 'Ron'\n" "\n" "Reason: AssertionError: " "Not matched elements among all with indexes from 0 to 2:\n" @@ -61,7 +61,7 @@ def test_on_collection(session_browser): elements.should(have.text('Ron').not_.each) except TimeoutException as error: assert ( - "browser.all(('css selector', 'li')). each has no (text Ron)\n" + "browser.all(('css selector', 'li')). each has no (text 'Ron')\n" '\n' 'Reason: AssertionError: Not matched elements among all with indexes from 0 ' 'to 2:\n' @@ -72,7 +72,7 @@ def test_on_collection(session_browser): elements.should(have.no.text('Ron').each) except TimeoutException as error: assert ( - "browser.all(('css selector', 'li')). each has no (text Ron)\n" + "browser.all(('css selector', 'li')). each has no (text 'Ron')\n" '\n' 'Reason: AssertionError: Not matched elements among all with indexes from 0 ' 'to 2:\n' @@ -109,7 +109,7 @@ def test_on_collection_with_expected_size(session_browser): 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" + "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' @@ -123,7 +123,7 @@ def test_on_collection_with_expected_size(session_browser): 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" + "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' diff --git a/tests/integration/element__get__query__frame_context__nested__element_test.py b/tests/integration/element__get__query__frame_context__nested__element_test.py index 7e0c8c5b..c23138c9 100644 --- a/tests/integration/element__get__query__frame_context__nested__element_test.py +++ b/tests/integration/element__get__query__frame_context__nested__element_test.py @@ -60,7 +60,7 @@ def test_actions_on_nested_frames_element_via_search_context_via_get( 'Timed out after 0.5s, while waiting for:\n' "browser.element(('css selector', '[name=frame-top]')): element(('css " "selector', '[name=frame-middle]')): element(('css selector', " - "'#content')).has exact text LEFT\n" + "'#content')).has exact text 'LEFT'\n" '\n' 'Reason: ConditionMismatch: actual text: MIDDLE\n' ) in str(error) @@ -110,7 +110,7 @@ def test_actions_on_nested_frames_element_via_search_context__via_direct_applica 'Timed out after 0.5s, while waiting for:\n' "browser.element(('css selector', '[name=frame-top]')): element(('css " "selector', '[name=frame-middle]')): element(('css selector', " - "'#content')).has exact text LEFT\n" + "'#content')).has exact text 'LEFT'\n" '\n' 'Reason: ConditionMismatch: actual text: MIDDLE\n' ) in str(error) diff --git a/tests/integration/element__get__query__frame_context__nested__with_test.py b/tests/integration/element__get__query__frame_context__nested__with_test.py index 9ea8cc74..855b847b 100644 --- a/tests/integration/element__get__query__frame_context__nested__with_test.py +++ b/tests/integration/element__get__query__frame_context__nested__with_test.py @@ -55,7 +55,7 @@ def test_actions_on_nested_frames_element_via_with_statement(session_browser): 'Message: \n' '\n' 'Timed out after 0.5s, while waiting for:\n' - "browser.element(('css selector', '#content')).has exact text LEFT\n" + "browser.element(('css selector', '#content')).has exact text 'LEFT'\n" '\n' 'Reason: ConditionMismatch: actual text: MIDDLE\n' ) in str(error) 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 a39ebefc..bda13c32 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,8 +57,8 @@ 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]')).have exact texts ('My WRONG text', 'My WRONG " - "text')\n" + "selector', '[name=my-text]')).have exact texts ['My WRONG text', 'My WRONG " + "text']\n" '\n' "Reason: ConditionMismatch: actual visible texts: ['My default text', 'My " "default text']\n" diff --git a/tests/integration/element__get__query__js__shadow_root__element_test.py b/tests/integration/element__get__query__js__shadow_root__element_test.py index 5c2508bc..4ee9f7dd 100644 --- a/tests/integration/element__get__query__js__shadow_root__element_test.py +++ b/tests/integration/element__get__query__js__shadow_root__element_test.py @@ -58,7 +58,7 @@ def test_actions_on_shadow_root_element(session_browser): '\n' 'Timed out after 0.5s, while waiting for:\n' "browser.all(('css selector', 'my-paragraph'))[0]: shadow root.element(('css " - "selector', '[name=my-text]')).has exact text My WRONG text\n" + "selector', '[name=my-text]')).has exact text 'My WRONG text'\n" '\n' 'Reason: ConditionMismatch: actual text: My default text\n' ) in str(error) diff --git a/tests/integration/error_messages_test.py b/tests/integration/error_messages_test.py index e0ecf478..ef39cd98 100644 --- a/tests/integration/error_messages_test.py +++ b/tests/integration/error_messages_test.py @@ -97,7 +97,7 @@ def test_element_search_fails_with_message_when_explicitly_waits_for_condition( assert exception_message(ex) == [ 'Timed out after 0.1s, while waiting for:', - "browser.element(('css selector', '#element')).has exact text Hello wor", + "browser.element(('css selector', '#element')).has exact text 'Hello wor'", '', 'Reason: ConditionMismatch: actual text: Hello world!', 'Screenshot: *.png', @@ -260,8 +260,8 @@ def test_element_search_fails_with_message_when_explicitly_waits_for_not_conditi except AssertionError as error: assert ( - "browser.element(('css selector', '#element')).has no (exact text Hello " - 'world!)\n' + "browser.element(('css selector', '#element')).has no (exact text 'Hello " + 'world!\')\n' '\n' 'Reason: ConditionMismatch: actual text: Hello world!\n' ) in str(error) diff --git a/tests/integration/shared_browser/browser__config__wait_decorator_test.py b/tests/integration/shared_browser/browser__config__wait_decorator_test.py index 4db5918b..3e27aea8 100644 --- a/tests/integration/shared_browser/browser__config__wait_decorator_test.py +++ b/tests/integration/shared_browser/browser__config__wait_decorator_test.py @@ -88,8 +88,8 @@ def test_logging_via__wait_decorator(quit_shared_browser_afterwards): [SE] - [3] - step: browser.element(('css selector', '#new-todo')) > type: c: ENDED [SE] - [3] - step: browser.element(('css selector', '#new-todo')) > press keys: ('\ue007',): STARTED [SE] - [3] - step: browser.element(('css selector', '#new-todo')) > press keys: ('\ue007',): ENDED -[SE] - [4] - step: browser.all(('css selector', '#todo-list>li')) > have texts ('ab', 'b', 'c', 'd'): STARTED -[SE] - [4] - step: browser.all(('css selector', '#todo-list>li')) > have texts ('ab', 'b', 'c', 'd'): FAILED: Message: +[SE] - [4] - step: browser.all(('css selector', '#todo-list>li')) > have texts ['ab', 'b', 'c', 'd']: STARTED +[SE] - [4] - step: browser.all(('css selector', '#todo-list>li')) > have texts ['ab', 'b', 'c', 'd']: FAILED: Message: '''.strip() in handler.stream ) 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 a938a87d..245db63b 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 @@ -73,8 +73,8 @@ def test_logging_via__wait_decorator(quit_shared_browser_afterwards): ''' element('#new-todo'): should be enabled and be visible: STARTED element('#new-todo'): should be enabled and be visible: PASSED -element('#new-todo'): should have exact text and have attribute 'value' with value '': STARTED -element('#new-todo'): should have exact text and have attribute 'value' with value '': PASSED +element('#new-todo'): should be blank: STARTED +element('#new-todo'): should be blank: PASSED element('#new-todo'): type: a: STARTED element('#new-todo'): type: a: PASSED element('#new-todo'): press keys: ENTER: STARTED @@ -83,22 +83,24 @@ def test_logging_via__wait_decorator(quit_shared_browser_afterwards): element('#new-todo'): type: b: PASSED element('#new-todo'): press keys: TAB: STARTED element('#new-todo'): press keys: TAB: PASSED -element('#new-todo'): should have no (exact text and have attribute 'value' with value '') and have attribute 'value' with value 'b': STARTED -element('#new-todo'): should have no (exact text and have attribute 'value' with value '') and have attribute 'value' with value 'b': PASSED +element('#new-todo'): should be not (blank) ''' + '''and have attribute 'value' with value 'b': STARTED +element('#new-todo'): should be not (blank) ''' + '''and have attribute 'value' with value 'b': PASSED element('#new-todo'): press keys: BACKSPACE: STARTED element('#new-todo'): press keys: BACKSPACE: PASSED element('#new-todo'): type: c: STARTED element('#new-todo'): type: c: PASSED element('#new-todo'): press keys: ENTER: STARTED element('#new-todo'): press keys: ENTER: PASSED -all('#todo-list>li'): should have texts ('a', 'b', 'c'): STARTED -all('#todo-list>li'): should have texts ('a', 'b', 'c'): FAILED: +all('#todo-list>li'): should have texts ['a', 'b', 'c']: STARTED +all('#todo-list>li'): should have texts ['a', 'b', 'c']: FAILED: Message:\u0020 Timed out after 0.3s, while waiting for: -browser.all(('css selector', '#todo-list>li')).have texts ('a', 'b', 'c') +browser.all(('css selector', '#todo-list>li')).have texts ['a', 'b', 'c'] Reason: ConditionMismatch: actual visible texts: ['a', 'c']\n '''.strip()