diff --git a/.run/lint_pylint.sh b/.run/lint_pylint.sh
index d56523cf..bf646c63 100755
--- a/.run/lint_pylint.sh
+++ b/.run/lint_pylint.sh
@@ -1,5 +1,5 @@
#!/bin/bash
touch __init__.py
-pylint $(pwd) --disable="$(cat .pylint-disabled-rules)" --ignore-patterns=.venv
+pylint $(pwd) --disable="$(cat .pylint-disabled-rules)" --ignore-patterns=.venv -r n
rm __init__.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ec80f0c..aa9b0430 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -110,11 +110,13 @@ TODOs:
### TODO: document subclass based custom conditions
-## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024)
+### TODO: should we help users do not shoot their legs when using browser.all(selector) in for loops? #534
### TODO: not_ as callable object?
-### TODO: should we help users do not shoot their legs when using browser.all(selector) in for loops? #534
+## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024)
+
+### TODO: finalize pylint update to 3.2.2. (clean all warnings, especially for pyproject.toml)
### TODO: rename all conditions inside match.py so match can be fully used instead be + have #530
@@ -122,6 +124,10 @@ TODOs:
...
+#### TODO: finalize error messages tests for present, visible, hidden
+
+#### TODO: decide on present vs present_in_dom (same for absent)
+
### TODO: ENSURE composed conditions work as expected (or/and, etc.)
...
diff --git a/selene/common/_typing_functions.py b/selene/common/_typing_functions.py
index 74948c66..5ef0edfa 100644
--- a/selene/common/_typing_functions.py
+++ b/selene/common/_typing_functions.py
@@ -21,11 +21,18 @@
# SOFTWARE.
from __future__ import annotations
-import functools
import inspect
import re
-from typing_extensions import TypeVar, Callable, Generic, Optional, overload
+from typing_extensions import (
+ TypeVar,
+ Callable,
+ Generic,
+ Optional,
+ overload,
+ Type,
+ Iterable,
+)
from selene.common.fp import thread_last
@@ -108,26 +115,41 @@ def full_description_for(callable_: Optional[Callable]) -> str | None:
@staticmethod
@overload
- def _inverted(predicate: Predicate[E]) -> Predicate[E]: ...
+ def _inverted(
+ predicate: Predicate[E],
+ _truthy_exceptions: Iterable[Type[Exception]] = (),
+ ) -> Predicate[E]: ...
@staticmethod
@overload
- def _inverted(predicate: Query[E, bool]) -> Query[E, bool]: ...
+ def _inverted(
+ predicate: Query[E, bool],
+ _truthy_exceptions: Iterable[Type[Exception]] = (),
+ ) -> Query[E, bool]: ...
@staticmethod
def _inverted(
- predicate: Predicate[E] | Query[E, bool]
+ predicate: Predicate[E] | Query[E, bool],
+ _truthy_exceptions: Iterable[Type[Exception]] = (),
) -> Predicate[E] | Query[E, bool]:
# TODO: ensure it works correctly:) e.g. unit test it
+
+ def not_predicate(entity: E) -> bool:
+ try:
+ return not predicate(entity)
+ except Exception as reason:
+ if any(
+ isinstance(reason, exception) for exception in _truthy_exceptions
+ ):
+ return True
+ raise reason
+
if isinstance(predicate, Query):
return Query(
f'not {predicate}',
- lambda entity: not predicate(entity),
+ not_predicate,
)
- def not_predicate(entity: E) -> bool:
- return not predicate(entity)
-
not_predicate.__module__ = predicate.__module__
not_predicate.__annotations__ = predicate.__annotations__
diff --git a/selene/core/condition.py b/selene/core/condition.py
index 6c591528..8ed51480 100644
--- a/selene/core/condition.py
+++ b/selene/core/condition.py
@@ -431,10 +431,12 @@ def is_more_than(limit):
"""
from __future__ import annotations
+import functools
import sys
import typing
import warnings
+from selenium.common import WebDriverException
from typing_extensions import (
List,
TypeVar,
@@ -726,6 +728,11 @@ def as_not( # TODO: ENSURE ALL USAGES ARE NOW CORRECT
) -> Condition[E]:
# TODO: how will it work composed conditions?
+ # TODO: should we bother? – about "negated inversion via Condition.as_not"
+ # will "swallow" the reason of failure...
+ # because we invert the predicate or test itself, ignoring exceptions
+ # so then when we "recover original exception failure" on negation
+ # we can just recover it to "false" not to "raise reason error"
if description:
return (
cls(
@@ -736,11 +743,23 @@ def as_not( # TODO: ENSURE ALL USAGES ARE NOW CORRECT
# thus, no need to mark condition for further inversion:
_inverted=False,
)
- if not condition.__by
+ if condition.__by is None
else cls(
description,
- actual=condition.__actual,
- by=Query._inverted(condition.__by),
+ # # We have to skip the actual here (re-building it into by below),
+ # # because can't "truthify" its Exceptions when raised on inverted
+ # # TODO: or can we?
+ # actual=condition.__actual,
+ by=Query._inverted(
+ functools.wraps(condition.__by)(
+ lambda entity: condition.__by( # type: ignore
+ condition.__actual(entity)
+ if condition.__actual
+ else entity
+ )
+ ),
+ _truthy_exceptions=(AssertionError, WebDriverException),
+ ),
_inverted=False,
)
)
@@ -826,9 +845,15 @@ def __init__(
ConditionMismatch._to_raise_if_actual(
self.__actual,
self.__by,
+ # TODO: should we DI? – remove this tight coupling to WebDriverException?
+ # here and elsewhere
+ _falsy_exceptions=(AssertionError, WebDriverException),
)
if self.__actual
- else ConditionMismatch._to_raise_if(self.__by)
+ else ConditionMismatch._to_raise_if(
+ self.__by,
+ _falsy_exceptions=(AssertionError, WebDriverException),
+ )
)
return
@@ -854,7 +879,7 @@ def as_inverted(entity: E) -> None:
return
raise ValueError(
- 'either test or by with optional actual should be provided, ' 'not nothing'
+ 'either test or by with optional actual should be provided, not nothing'
)
# TODO: rethink not_ naming...
@@ -913,6 +938,8 @@ def __describe(self) -> str:
def __describe_inverted(self) -> str:
condition_words = self.__describe().split(' ')
is_or_have = condition_words[0]
+ if is_or_have not in ('is', 'has', 'have'):
+ return f'not ({self.__describe()})'
name = ' '.join(condition_words[1:])
no_or_not = 'not' if is_or_have == 'is' else 'no'
return f'{is_or_have} {no_or_not} ({name})'
@@ -1312,11 +1339,8 @@ def __init__x(self, *args, **kwargs):
'or custom __str__ implementation '
'(like lambda wrapped in Query object)'
)
- description = (
- (str(actual_desc) + ' ')
- if (actual_desc := Query.full_description_for(actual)) is not None
- else ''
- ) + str(
+ actual_desc = Query.full_description_for(actual)
+ description = ((str(actual_desc) + ' ') if actual_desc else '') + str(
by_description
) # noqa
super().__init__(description, actual=actual, by=by)
@@ -1324,12 +1348,48 @@ def __init__x(self, *args, **kwargs):
raise ValueError('invalid arguments to Match initializer')
+ @overload
+ def __init__(
+ self,
+ description: str | Callable[[], str],
+ actual: Lambda[E, R],
+ *,
+ by: Predicate[R],
+ _inverted=False,
+ ): ...
+
+ @overload
+ def __init__(
+ self,
+ description: str | Callable[[], str],
+ *,
+ by: Predicate[E],
+ _inverted=False,
+ ): ...
+
+ @overload
+ def __init__(
+ self,
+ *,
+ actual: Lambda[E, R],
+ by: Predicate[R],
+ _inverted=False,
+ ): ...
+
+ @overload
+ def __init__(
+ self,
+ *,
+ by: Predicate[E],
+ _inverted=False,
+ ): ...
+
def __init__(
self,
description: str | Callable[[], str] | None = None,
actual: Lambda[E, R] | None = None,
*,
- by: Predicate[R],
+ by: Predicate[E] | Predicate[R],
_inverted=False,
):
"""
@@ -1345,20 +1405,18 @@ def __init__(
"""
if not description and not (by_description := Query.full_description_for(by)):
raise ValueError(
- 'either provide description or ensure that at least by predicate'
- 'has __qualname__ (defined as regular named function)'
+ 'either provide description or ensure that at least by predicate '
+ 'has __qualname__ (defined as regular named function) '
'or custom __str__ implementation '
'(like lambda wrapped in Query object)'
)
+ actual_desc = Query.full_description_for(actual)
description = description or (
- (
- (str(actual_desc) + ' ')
- if (actual_desc := Query.full_description_for(actual)) is not None
- else ''
- )
+ ((str(actual_desc) + ' ') if actual_desc else '')
+ str(by_description) # noqa
)
- super().__init__(
+ # TODO: fix "cannot infer type of argument 1 of __init__" or ignore
+ super().__init__( # type: ignore
description=description,
actual=actual,
by=by,
diff --git a/selene/core/exceptions.py b/selene/core/exceptions.py
index 20c15be3..95d06543 100644
--- a/selene/core/exceptions.py
+++ b/selene/core/exceptions.py
@@ -113,6 +113,7 @@ def _to_raise_if_not(
by: Callable[[E], bool],
*,
_inverted: bool = False,
+ _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,),
): ...
@classmethod
@@ -123,6 +124,7 @@ def _to_raise_if_not(
actual: Callable[[E], R] | None = None,
*,
_inverted: bool = False,
+ _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,),
): ...
# TODO: should we name test param as predicate?
@@ -133,42 +135,18 @@ def _to_raise_if_not(
actual: Optional[Callable[[E], E | R]] = None,
*,
_inverted: Optional[bool] = False,
- _falsy_exceptions: Iterable[Type[Exception]] = (),
+ # TODO: should we rename it to _exceptions_as_truthy_on_inverted?
+ # or just document this in docstring?
+ _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,),
):
@functools.wraps(by)
def wrapped(entity: E) -> None:
- actual_description = (
- f' {name}' if (name := Query.full_description_for(actual)) else ''
- )
- # TODO: should we catch errors on actual?
- # for e.g. to consider them as False indicator
- actual_to_test = None
- # error_on_actual = None
- try:
- actual_to_test = actual(entity) if actual else entity
- except Exception as reason:
- # error_on_actual = reason
- if _inverted:
- return
- raise reason
-
- answer = None
- error_on_predicate = None
- try:
- answer = by(actual_to_test)
- # TODO: should we move Exception processing out of this helper?
- # should it be somewhere in Condition?
- # cause now it's not a Mismatch anymore, it's a failure
- # – no, we should not, we should keep it here,
- # because this is needed for the inverted case
- except Exception as reason:
- error_on_predicate = reason
- # answer is still None
- pass
-
- def describe_not_match():
+ def describe_not_match(actual_value):
+ actual_description = (
+ f' {name}' if (name := Query.full_description_for(actual)) else ''
+ )
return (
- f'actual{actual_description}: {actual_to_test}'
+ f'actual{actual_description}: {actual_value}'
if actual
else (
(
@@ -180,6 +158,10 @@ def describe_not_match():
or "condition"
)
+ ' not matched'
+ # TODO: should we consider eliminating errors like:
+ # 'Reason: ConditionMismatch: condition not matched\n'
+ # to:
+ # 'Reason: ConditionMismatch\n'
)
# TODO: decide on
# cls(f'{Query.full_name_for(predicate) or "condition"} not matched')
@@ -187,29 +169,59 @@ def describe_not_match():
# else cls('condition not matched')
)
- # TODO: should we raise InvalidCompare on _inverted too?
- # should we make it configurable?
- # if not _inverted and error_on_predicate:
- if error_on_predicate and type(error_on_predicate) not in _falsy_exceptions:
+ def describe_error(error):
# TODO: consider making it customizable
# remove stacktrace if available:
- stacktrace = getattr(error_on_predicate, 'stacktrace', None)
- error_on_predicate_str = (
- str(error_on_predicate)
+ stacktrace = getattr(error, 'stacktrace', None)
+ return (
+ str(error)
if not stacktrace
- else (''.join(str(error_on_predicate).split(stacktrace)))
+ else (
+ ''.join(
+ str(error).split('\n'.join(['Stacktrace:', *stacktrace]))
+ )
+ )
)
+
+ # TODO: should we catch errors on actual?
+ # for e.g. to consider them as False indicator
+ actual_to_test = None
+ try:
+ actual_to_test = actual(entity) if actual else entity
+ except Exception as reason:
+ if _inverted and any(
+ isinstance(reason, exception) for exception in _falsy_exceptions
+ ):
+ return
+ # TODO: do we even need this prefix?
+ # raise cls(f'Unable to get actual to match:\n{describe_error(reason)}')
+ raise cls(describe_error(reason)) from reason
+
+ answer = None
+ try:
+ answer = by(actual_to_test)
+ # TODO: should we move Exception processing out of this helper?
+ # should it be somewhere in Condition?
+ # cause now it's not a Mismatch anymore, it's a failure
+ # – no, we should not, we should keep it here,
+ # because this is needed for the inverted case
+ except Exception as reason:
+ if _inverted and any(
+ isinstance(reason, exception) for exception in _falsy_exceptions
+ ):
+ return
+ # answer is still None
raise cls(
- 'InvalidCompareError: '
- f'{error_on_predicate_str}:\n{describe_not_match()}'
- )
+ f'{describe_error(reason)}:'
+ f'\n{describe_not_match(actual_to_test)}'
+ ) from reason
if answer if _inverted else not answer:
# TODO: should we render expected too? (based on predicate name)
# we want need it for our conditions,
# cause wait.py logs it in the message
# but ... ?
- raise cls(describe_not_match())
+ raise cls(describe_not_match(actual_to_test))
return wrapped
@@ -218,8 +230,11 @@ def _to_raise_if(
cls,
by: Callable[[E | R], bool],
actual: Callable[[E], R] | None = None,
+ _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,),
):
- return cls._to_raise_if_not(by, actual, _inverted=True)
+ return cls._to_raise_if_not(
+ by, actual, _inverted=True, _falsy_exceptions=_falsy_exceptions
+ )
@classmethod
def _to_raise_if_not_actual(
@@ -234,8 +249,9 @@ def _to_raise_if_actual(
cls,
query: Callable[[E], R],
by: Callable[[R], bool],
+ _falsy_exceptions: Iterable[Type[Exception]] = (AssertionError,),
):
- return cls._to_raise_if(by, query)
+ return cls._to_raise_if(by, query, _falsy_exceptions=_falsy_exceptions)
class ConditionNotMatchedError(ConditionMismatch):
diff --git a/selene/core/match.py b/selene/core/match.py
index 531b6c8a..69ea416a 100644
--- a/selene/core/match.py
+++ b/selene/core/match.py
@@ -55,34 +55,36 @@
)
from selene.core.entity import Collection, Element
from selene.core._browser import Browser
-from selene.common._typing_functions import Query
-
-# TODO: consider moving to selene.match.element.is_visible, etc...
-element_is_visible: Condition[Element] = ElementCondition.raise_if_not(
- 'is visible', lambda element: element().is_displayed()
+# TODO: consider renaming to present_in_dom
+present: Condition[Element] = Match(
+ 'is present in DOM',
+ actual=lambda element: element.locate(),
+ by=lambda webelement: webelement is not None,
)
+# TODO: consider renaming to absent_in_dom
+absent: Condition[Element] = Condition.as_not(present, 'is absent in DOM')
-element_is_hidden: Condition[Element] = ElementCondition.as_not(
- element_is_visible, 'is hidden'
+
+visible: Condition[Element] = Match(
+ 'is visible',
+ actual=lambda element: element.locate(),
+ by=lambda actual: actual.is_displayed(),
)
+hidden: Condition[Element] = Condition.as_not(visible, 'is hidden')
+
+hidden_in_dom: Condition[Element] = present.and_(visible.not_)
+
+
element_is_enabled: Condition[Element] = ElementCondition.raise_if_not(
'is enabled', lambda element: element().is_enabled()
)
element_is_disabled: Condition[Element] = ElementCondition.as_not(element_is_enabled)
-element_is_clickable: Condition[Element] = element_is_visible.and_(element_is_enabled)
-
-present: Condition[Element] = Match(
- 'is present in DOM',
- actual=lambda element: element.locate(),
- by=lambda webelement: webelement is not None,
-)
-
-element_is_absent: Condition[Element] = ElementCondition.as_not(present)
+element_is_clickable: Condition[Element] = visible.and_(element_is_enabled)
# TODO: how will it work for mobile?
element_is_focused: Condition[Element] = ElementCondition.raise_if_not(
@@ -166,6 +168,11 @@ def __init__(self, expected: str, _flags=0, _inverted=False):
self.__expected = expected
self.__flags = _flags
self.__inverted = _inverted
+ # TODO: on invalid pattern error will be:
+ # 'Reason: ConditionMismatch: nothing to repeat at position 0'
+ # how to improve it? leaving more hints that this is "regex invalid error"
+ # probably, we can re-raise re.error inside predicate.matches
+ # with additional explanation
super().__init__(
f'has text matching{f" (with flags {_flags}):" if _flags else ""}'
diff --git a/selene/support/conditions/be.py b/selene/support/conditions/be.py
index ae7fc8dc..f8927b62 100644
--- a/selene/support/conditions/be.py
+++ b/selene/support/conditions/be.py
@@ -25,15 +25,16 @@
not_ = _not_
-visible = match.element_is_visible
-hidden = match.element_is_hidden
+visible = match.visible
+hidden = match.hidden
+hidden_in_dom = match.hidden_in_dom
selected = match.element_is_selected
present = match.present
in_dom = match.present # TODO: do we need both present and in_dom?
existing = match.present # TODO: consider deprecating
-absent = match.element_is_absent
+absent = match.absent
enabled = match.element_is_enabled
disabled = match.element_is_disabled
diff --git a/selene/support/conditions/not_.py b/selene/support/conditions/not_.py
index 26e83bd8..ea138d39 100644
--- a/selene/support/conditions/not_.py
+++ b/selene/support/conditions/not_.py
@@ -35,8 +35,9 @@
# TODO: consider refactoring to class for better extendability
# when creating custom conditions
-visible: Condition[Element] = _match.element_is_visible.not_
-hidden: Condition[Element] = _match.element_is_hidden.not_
+visible: Condition[Element] = _match.visible.not_
+hidden: Condition[Element] = _match.hidden.not_
+hidden_in_dom: Condition[Element] = _match.hidden_in_dom.not_
present: Condition[Element] = _match.present.not_
in_dom: Condition[Element] = _match.present.not_
@@ -44,7 +45,7 @@
# TODO: consider deprecating existing
existing: Condition[Element] = _match.present.not_
-absent: Condition[Element] = _match.element_is_absent.not_
+absent: Condition[Element] = _match.absent.not_
enabled: Condition[Element] = _match.element_is_enabled.not_
disabled: Condition[Element] = _match.element_is_disabled.not_
diff --git a/tests/integration/condition__element__have_text_matching__compared_test.py b/tests/integration/condition__element__have_text_matching__compared_test.py
index dafe6382..65009b0d 100644
--- a/tests/integration/condition__element__have_text_matching__compared_test.py
+++ b/tests/integration/condition__element__have_text_matching__compared_test.py
@@ -318,7 +318,7 @@ def test_text_matching__regex_pattern__error__on_invalid_regex__with_ignorecase(
"browser.all(('css selector', 'li'))[0].has text matching (with flags "
're.IGNORECASE): *one*\n'
'\n'
- 'Reason: ConditionMismatch: InvalidCompareError: nothing to repeat at position '
+ 'Reason: ConditionMismatch: nothing to repeat at position '
'0:\n'
'actual text: 1) One!!!\n'
'Screenshot: '
@@ -333,7 +333,7 @@ def test_text_matching__regex_pattern__error__on_invalid_regex__with_ignorecase(
"browser.all(('css selector', 'li'))[0].has no (text matching (with flags "
're.IGNORECASE): *one*)\n'
'\n'
- 'Reason: ConditionMismatch: InvalidCompareError: nothing to repeat at position '
+ 'Reason: ConditionMismatch: nothing to repeat at position '
'0:\n'
'actual text: 1) One!!!\n'
'Screenshot: '
@@ -347,7 +347,7 @@ def test_text_matching__regex_pattern__error__on_invalid_regex__with_ignorecase(
"browser.all(('css selector', 'li'))[0].has no (text matching (with flags "
're.IGNORECASE): *one*)\n'
'\n'
- 'Reason: ConditionMismatch: InvalidCompareError: nothing to repeat at position '
+ 'Reason: ConditionMismatch: nothing to repeat at position '
'0:\n'
'actual text: 1) One!!!\n'
'Screenshot: '
diff --git a/tests/integration/condition__element__present__via_inline_Match_test.py b/tests/integration/condition__element__present__via_inline_Match_test.py
new file mode 100644
index 00000000..3ef1ac21
--- /dev/null
+++ b/tests/integration/condition__element__present__via_inline_Match_test.py
@@ -0,0 +1,235 @@
+# 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 be, have
+from selene.core import match
+from selene.core.condition import Match, Condition
+from selene.core.exceptions import ConditionMismatch
+from tests.integration.helpers.givenpage import GivenPage
+
+
+# TODO: review coverage: consider breaking down into atomic tests
+def test_should_be_present__via_inline_Match__passed_and_failed(session_browser):
+ browser = session_browser.with_(timeout=0.1)
+ GivenPage(session_browser.driver).opened_with_body(
+ '''
+
+
+
+ '''
+ )
+
+ absent = browser.element("#absent")
+ hidden = browser.element("#hidden")
+ # visible = browser.element("#visible")
+
+ # THEN
+ # - with actual failure as True on inversion
+ absent.should(
+ Match(
+ 'present',
+ actual=lambda element: element.locate(),
+ by=lambda actual: actual is not None,
+ ).not_
+ )
+ # - with actual failure as True on inversion via Condition.as_not
+ absent.should(
+ Condition.as_not(
+ Match(
+ 'present',
+ actual=lambda element: element.locate(),
+ by=lambda actual: actual is not None,
+ ),
+ 'absent',
+ )
+ )
+ # - with actual failure
+ try:
+ absent.should(
+ Match(
+ 'present',
+ actual=lambda element: element.locate(),
+ by=lambda actual: actual is not None,
+ )
+ )
+ pytest.fail('expected failure')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#absent')).present\n"
+ '\n'
+ 'Reason: ConditionMismatch: Message: no such element: Unable to locate '
+ 'element: {"method":"css selector","selector":"#absent"}\n'
+ ' (Session info: chrome=125.0.6422.142); For documentation on this error, '
+ 'please visit: '
+ 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n'
+ '\n'
+ ) in str(error)
+ # - with actual failure on negated inversion via Condition.as_not
+ # (with lost reason details)
+ try:
+ absent.should(
+ Condition.as_not(
+ Match(
+ 'present',
+ actual=lambda element: element.locate(),
+ by=lambda actual: actual is not None,
+ ),
+ 'absent',
+ ).not_
+ )
+ pytest.fail('expected failure')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#absent')).not (absent)\n"
+ '\n'
+ 'Reason: ConditionMismatch: condition not matched\n'
+ ) in str(error)
+ # ↪ compared to ↙
+ # - with actual failure but wrapped into test on negated inversion via Condition.as_not
+ # (YET with SAME lost reason details) TODO: should we bother?
+ try:
+ absent.should(
+ Condition.as_not(
+ Condition(
+ 'present',
+ test=ConditionMismatch._to_raise_if_not_actual(
+ query=lambda element: element.locate(),
+ by=lambda actual: actual is not None,
+ ),
+ ),
+ 'absent',
+ ).not_
+ )
+ pytest.fail('expected failure')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#absent')).not (absent)\n"
+ '\n'
+ 'Reason: ConditionMismatch: condition not matched\n'
+ ) in str(error)
+ # ↪ compared to ↙
+ # - with by failure on negated inversion via Condition.as_not
+ # (YET with SAME lost reason details) TODO: should we bother?
+ try:
+ absent.should(
+ Condition.as_not(
+ Match(
+ 'present',
+ by=lambda element: element.locate() is not None,
+ ),
+ 'absent',
+ ).not_
+ )
+ pytest.fail('expected failure')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#absent')).not (absent)\n"
+ '\n'
+ 'Reason: ConditionMismatch: condition not matched\n'
+ ) in str(error)
+ # - with actual mismatch on inversion (without 'is' prefix in name)
+ try:
+ hidden.should(
+ Match(
+ 'present',
+ actual=lambda element: element.locate(),
+ by=lambda actual: actual is not None,
+ ).not_
+ )
+ pytest.fail('expected mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#hidden')).not (present)\n"
+ '\n'
+ 'Reason: ConditionMismatch: actual: '
+ '\n'
+ ) in str(error)
+ # - with actual mismatch on inversion (without 'is' prefix in name)
+ try:
+ hidden.should(
+ Match(
+ 'is present',
+ actual=lambda element: element.locate(),
+ by=lambda actual: actual is not None,
+ ).not_
+ )
+ pytest.fail('expected mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#hidden')).is not (present)\n"
+ '\n'
+ 'Reason: ConditionMismatch: actual: '
+ '\n'
+ ) in str(error)
+ # - with by failure as True on inversion
+ absent.should(
+ Match('present', by=lambda element: element.locate() is not None).not_
+ )
+ # - with by failure
+ try:
+ absent.should(Match('present', by=lambda element: element.locate() is not None))
+ pytest.fail('expected failure')
+ except AssertionError as error:
+ assert (
+ # TODO: one problem with this error... that it tells about Mismatch
+ # but in fact its a Failure
+ # (i.e. and actual exception not comparison mismatch)
+ "browser.element(('css selector', '#absent')).present\n"
+ '\n'
+ 'Reason: ConditionMismatch: Message: no such element: Unable to locate '
+ 'element: {"method":"css selector","selector":"#absent"}\n'
+ ' (Session info: chrome=125.0.6422.142); For documentation on this error, '
+ 'please visit: '
+ 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception\n'
+ ':\n'
+ 'condition not matched\n' # TODO: this ending is not needed...
+ # but should we bother?
+ ) in str(error)
+ # - with by mismatch on inversion (without 'is' prefix in name)
+ try:
+ hidden.should(
+ Match('present', by=lambda element: element.locate() is not None).not_
+ )
+ pytest.fail('expected mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#hidden')).not (present)\n"
+ '\n'
+ 'Reason: ConditionMismatch: condition not matched\n'
+ ) in str(error)
+ # - with by mismatch on inversion (with 'is' prefix in name)
+ try:
+ hidden.should(
+ Match('is present', by=lambda element: element.locate() is not None).not_
+ )
+ pytest.fail('expected mismatch')
+ except AssertionError as error:
+ assert (
+ "browser.element(('css selector', '#hidden')).is not (present)\n"
+ '\n'
+ 'Reason: ConditionMismatch: condition not matched\n'
+ ) in str(error)
diff --git a/tests/integration/condition__element__hidden_test.py b/tests/integration/condition__element__present_visible__plus_inversions__compared_test.py
similarity index 69%
rename from tests/integration/condition__element__hidden_test.py
rename to tests/integration/condition__element__present_visible__plus_inversions__compared_test.py
index 0b1346f2..2edaed6d 100644
--- a/tests/integration/condition__element__hidden_test.py
+++ b/tests/integration/condition__element__present_visible__plus_inversions__compared_test.py
@@ -22,6 +22,7 @@
import pytest
from selene import be, have
+from selene.core import match
from tests.integration.helpers.givenpage import GivenPage
@@ -29,14 +30,47 @@ def test_should_be_hidden__passed_and_failed__compared_to_be_visible(session_bro
browser = session_browser.with_(timeout=0.1)
GivenPage(session_browser.driver).opened_with_body(
'''
+
'''
)
+ absent = browser.element("#absent")
hidden = browser.element("#hidden")
visible = browser.element("#visible")
+ # THEN
+
+ absent.should(match.present.not_)
+ absent.should(match.present.not_.not_.not_)
+ absent.should(be.not_.present)
+
+ absent.should(match.absent)
+ absent.should(match.absent.not_.not_)
+ absent.should(be.absent)
+ hidden.should(match.present)
+ hidden.should(be.present)
+ hidden.should(be.hidden_in_dom) # same ↙️
+ hidden.should(be.present.and_(be.not_.visible))
+ hidden.should(be.not_.visible)
+ hidden.should(be.not_.absent) # TODO: rename to be.not_.absent_in_dom?
+
+ absent.should(match.visible.not_)
+ absent.should(be.not_.visible)
+ absent.should(be.hidden) # TODO: should it fail?
+ absent.should(be.not_.hidden_in_dom)
+ absent.should(match.hidden_in_dom.not_)
+
+ visible.should(match.visible)
+ visible.should(be.visible)
+ visible.should(be.not_.hidden)
+ visible.should(be.not_.hidden_in_dom)
+ visible.should(be.present)
+ visible.should(be.not_.absent)
+
+ # TODO: review and extend/finalize coverage below
+
# THEN
visible.should(be.not_.hidden)
try:
@@ -69,7 +103,10 @@ def test_should_be_hidden__passed_and_failed__compared_to_be_visible(session_bro
assert (
"browser.element(('css selector', '#hidden')).is visible\n"
'\n'
- 'Reason: ConditionMismatch: condition not matched\n'
+ 'Reason: ConditionMismatch: actual: '
+ '\n'
) in str(error)
hidden.should(be.not_.visible)
@@ -80,7 +117,10 @@ def test_should_be_hidden__passed_and_failed__compared_to_be_visible(session_bro
assert (
"browser.element(('css selector', '#hidden')).is visible\n"
'\n'
- 'Reason: ConditionMismatch: condition not matched\n'
+ 'Reason: ConditionMismatch: actual: '
+ '\n'
) in str(error)
hidden.should(be.not_.hidden.not_)
diff --git a/tests/integration/element__get__query__frame_context__element_test.py b/tests/integration/element__get__query__frame_context__element_test.py
index a7877fc5..36f40eb2 100644
--- a/tests/integration/element__get__query__frame_context__element_test.py
+++ b/tests/integration/element__get__query__frame_context__element_test.py
@@ -91,6 +91,7 @@ def test_actions_on_frame_element_with_logging(session_browser):
# WHEN
browser.open('https://www.tiny.cloud/docs/tinymce/latest/cloud-quick-start/')
+ browser.element('#live-demo_tab_run_default').click()
# THEN everything inside frame context
text_area.element('p').should(
diff --git a/tests/integration/element__get__query__frame_context__with_test.py b/tests/integration/element__get__query__frame_context__with_test.py
index a9929ed5..c755e058 100644
--- a/tests/integration/element__get__query__frame_context__with_test.py
+++ b/tests/integration/element__get__query__frame_context__with_test.py
@@ -35,6 +35,7 @@ def test_actions_within_frame_context(session_browser):
# WHEN
browser.open('https://www.tiny.cloud/docs/tinymce/latest/cloud-quick-start/')
+ browser.element('#live-demo_tab_run_default').click()
# AND
with text_area_frame_context:
diff --git a/tests/unit/core/condition_test.py b/tests/unit/core/condition_test.py
index 74e490d2..2a5c2c9b 100644
--- a/tests/unit/core/condition_test.py
+++ b/tests/unit/core/condition_test.py
@@ -291,7 +291,8 @@ def test_as_not_match__of_constructed_via_factory__raise_if_not_actual():
# THEN
pytest.fail('on mismatch')
except AssertionError as error:
- assert 'actual self: 1' == str(error)
+ # assert 'actual self: 1' == str(error) # TODO: can we achieve this?
+ assert 'condition not matched' == str(error)
# TODO: add InvalidCompareError tests (positive and negative)