diff --git a/CHANGELOG.md b/CHANGELOG.md index cc21d0d4..a9f5f8b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -210,7 +210,9 @@ should we even refactor out them from Condition and move to Match only? ### TODO: rename all conditions inside match.py so match can be fully used instead be + have #530 -### TODO: should we make ignorecase optionally default for all conditions supporting it? +### TODO: Rename condition__collection__get*_test.py tests + +### TODO: is text + exact_text naming still relevant for match.* case over have.*? ### Deprecated conditions @@ -440,7 +442,17 @@ browser.all('li').first.should(have.text_matching(r'.*one.*').where_flags(re.IGN ``` -Same for attribute related conditions like `have.value` and `have.attribute(name).value*` will be a part of [#537](https://github.com/yashaka/selene/issues/537) +Same can be achieved via `_ignore_case` config option (yet marked as experimental): + +```python +from selene import browser, have +... +browser.all('li').first.with_(_ignore_case=True).should(have.exact_text('1) one!!!')) + +# and so on for other conditions that support ignore_case as property... +... + +``` ### Shadow DOM support via query.js.shadow_root(s) diff --git a/selene/common/_typing_functions.py b/selene/common/_typing_functions.py index 315523fd..4b70fc0b 100644 --- a/selene/common/_typing_functions.py +++ b/selene/common/_typing_functions.py @@ -77,7 +77,7 @@ def _name_for(self, entity: E) -> str: return self._name(entity) if callable(self._name) else self._name @staticmethod - def full_name_for( + def _full_name_for( callable_: Optional[Callable], _entity: E | None = None, ) -> str | None: @@ -116,11 +116,11 @@ def full_name_for( # todo: would not human_readable_name_for be a better name for this helper? @staticmethod - def full_description_for( + def _full_description_for( callable_: Optional[Callable], _entity: E | None = None, ) -> str | None: - full_name = Query.full_name_for(callable_, _entity) + full_name = Query._full_name_for(callable_, _entity) return ( thread_last( full_name, @@ -135,6 +135,21 @@ def full_description_for( else None ) + @staticmethod + def _full_description_or( + alternative: str, + /, + *, + for_: Optional[Callable], + _with_prefix: str = '', + _entity: E | None = None, + ) -> str: + return ( + _with_prefix + desc + if (desc := Query._full_description_for(for_, _entity)) + else alternative + ) + @staticmethod @overload def _inverted( diff --git a/selene/core/_browser.pyi b/selene/core/_browser.pyi index aad3cd3e..a9fa7965 100644 --- a/selene/core/_browser.pyi +++ b/selene/core/_browser.pyi @@ -103,6 +103,7 @@ class Browser(WaitingEntity['Browser']): wait_for_no_overlap_found_by_js: bool = False, _match_only_visible_elements_texts: bool = True, _match_only_visible_elements_size: bool = False, + _ignore_case: bool = False, # Etc. _build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ..., ) -> Browser: ... diff --git a/selene/core/condition.py b/selene/core/condition.py index 5aa525db..b4d8297a 100644 --- a/selene/core/condition.py +++ b/selene/core/condition.py @@ -1016,11 +1016,11 @@ def __str__(self): # - condition.repr_for(entity) # + correlate with __repr__ over __str__; + concise; - weird shortcut # - condition.for(entity) # - looks like a builder # # to remember entity so condition can be .__call__() later without passing entity - def _name_for(self, _entity: E | None = None): + def _name_for(self, entity: E | None = None) -> str: return ( - self.__describe(_entity) + self.__describe(entity) if not self.__inverted - else self.__describe_inverted(_entity) + else self.__describe_inverted(entity) ) # todo: we already have entity.matching for Callable[[E], bool] @@ -1064,6 +1064,19 @@ def _name_for(self, _entity: E | None = None): # > browser.element('input').clear() # > expect.blank(browser.element('input')) #->❤️ # TODO: at least, we have to document – the #->❤️-marked recipes... + # hm... but what if condition has param: + # > expect.value('foo')(browser.element('input')) + # – now it looks less natural + # let's compare other options: + # > expect.value('foo').test(browser.element('input')) + # > expect.value('foo').match(browser.element('input')) + # > assert_.value('foo').test(browser.element('input')) + # > assert_.value('foo').match(browser.element('input')) + # > match.value('foo').match(browser.element('input')) + # > match.value('foo').test(browser.element('input')) + # what if we add on property returning just self? + # or as an method alias to __call__() + # > match.value('foo').on(browser.element('input')) 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__ @@ -1499,14 +1512,14 @@ def __init__x(self, *args, **kwargs): else: actual = None by = args[0] - if not (by_name := Query.full_description_for(by)): + if not (by_name := Query._full_description_for(by)): raise ValueError( 'either provide name 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) + actual_desc = Query._full_description_for(actual) name = ((str(actual_desc) + ' ') if actual_desc else '') + str( by_name ) # noqa @@ -1586,14 +1599,14 @@ def __init__( 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 name and not (by_name := Query.full_description_for(by)): + if not name and not (by_name := Query._full_description_for(by)): raise ValueError( 'either provide a name 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_name = Query.full_description_for(actual) + actual_name = Query._full_description_for(actual) name = name or ( ((str(actual_name) + ' ') if actual_name else '') + str(by_name) # noqa ) diff --git a/selene/core/configuration.py b/selene/core/configuration.py index bf90bc78..98efbe2e 100644 --- a/selene/core/configuration.py +++ b/selene/core/configuration.py @@ -1152,6 +1152,14 @@ def _executor(self): It is set to False by default for backward compatibility reasons. """ # todo: document example + # todo: decide on naming: ignore_case vs match_ignoring_case? + # ignore_case conciser but + # - not consistent with other match_* options + # - does not explicitly tell that relates only to matching + # * but... it should be kind of obvious that can be used only in context of matching + # * and actually it's consistent with have.*.ignore_case conditions + # todo: consider documenting the major list of conditions that are affected by this option + _ignore_case: bool = False # TODO: better name? now technically it's not a decorator but decorator_builder... # or decorator_factory... diff --git a/selene/core/configuration.pyi b/selene/core/configuration.pyi index f788b64d..7f245bc0 100644 --- a/selene/core/configuration.pyi +++ b/selene/core/configuration.pyi @@ -100,6 +100,7 @@ class Config: wait_for_no_overlap_found_by_js: bool = False _match_only_visible_elements_texts: bool = True _match_only_visible_elements_size: bool = False + _ignore_case: bool = False # Etc. _build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ... _executor: _DriverStrategiesExecutor = ... @@ -156,6 +157,7 @@ class Config: wait_for_no_overlap_found_by_js: bool = False, _match_only_visible_elements_texts: bool = True, _match_only_visible_elements_size: bool = False, + _ignore_case: bool = False, # Etc. _build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ..., ): ... @@ -211,6 +213,7 @@ class Config: wait_for_no_overlap_found_by_js: bool = False, _match_only_visible_elements_texts: bool = True, _match_only_visible_elements_size: bool = False, + _ignore_case: bool = False, # Etc. _build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ..., ): ... diff --git a/selene/core/entity.pyi b/selene/core/entity.pyi index fda032f3..a617ab81 100644 --- a/selene/core/entity.pyi +++ b/selene/core/entity.pyi @@ -87,6 +87,7 @@ class Element(WaitingEntity['Element']): wait_for_no_overlap_found_by_js: bool = False, _match_only_visible_elements_texts: bool = True, _match_only_visible_elements_size: bool = False, + _ignore_case: bool = False, # Etc. _build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ..., ) -> Element: ... @@ -151,6 +152,7 @@ class Collection(WaitingEntity['Collection'], Iterable[Element]): wait_for_no_overlap_found_by_js: bool = False, _match_only_visible_elements_texts: bool = True, _match_only_visible_elements_size: bool = False, + _ignore_case: bool = False, # Etc. _build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ..., ) -> Collection: ... diff --git a/selene/core/exceptions.py b/selene/core/exceptions.py index 7e0ef1ce..012aea59 100644 --- a/selene/core/exceptions.py +++ b/selene/core/exceptions.py @@ -153,11 +153,8 @@ def wrapped(entity: E) -> None: def describe_not_match(actual_value): describe_actual_result = _describe_actual_result or ( lambda value: ( - 'actual' - + ( - f' {actual_name}' - if (actual_name := Query.full_description_for(actual)) - else '' + Query._full_description_or( + 'actual', for_=actual, _with_prefix='actual ' ) + f': {value}' ) @@ -169,7 +166,7 @@ def describe_not_match(actual_value): ( ( (f'not ({name})' if _inverted else name) - if (name := Query.full_description_for(by)) + if (name := Query._full_description_for(by)) else '' ) or "condition" diff --git a/selene/core/match.py b/selene/core/match.py index 3c108cf5..bd7353fd 100644 --- a/selene/core/match.py +++ b/selene/core/match.py @@ -42,21 +42,24 @@ Type, cast, Optional, + TypeVar, ) from selene.common import predicate, helpers, appium_tools +from selene.common._typing_functions import Query from selene.core import query -from selene.core.condition import Condition, Match, E +from selene.core.condition import Condition, Match from selene.core.conditions import ( ElementCondition, CollectionCondition, BrowserCondition, ) -from selene.core.entity import Collection, Element +from selene.core.entity import Collection, Element, Configured from selene.core._browser import Browser # GENERAL CONDITION BUILDERS ------------------------------------------------- # +E = TypeVar('E', bound=Configured) class _EntityHasSomethingSupportingIgnoreCase(Match[E]): @@ -80,17 +83,34 @@ def __init__( self.__falsy_exceptions = _falsy_exceptions super().__init__( - ( - f'{name}{" ignoring case:" if _ignore_case else ""} \'{expected}\'' + lambda maybe_entity: ( + name + + ( + ' ignoring case:' + if (maybe_entity is not None and maybe_entity.config._ignore_case) + or _ignore_case + else '' + ) + + f' \'{expected}\'' # todo: refactor to and change tests correspondingly: # f'{" ignoring case:" if _ignore_case else ":"} «{expected}»' ), - actual=actual, - by=lambda actual: ( + actual=lambda entity: (entity, actual(entity)), + by=lambda entity_and_actual: ( by(str(expected).lower())(str(actual).lower()) - if _ignore_case + if ( + entity_and_actual[0], + actual := entity_and_actual[1], + )[0].config._ignore_case + or _ignore_case else by(str(expected))(str(actual)) ), + _describe_actual_result=lambda entity_and_actual: ( + Query._full_description_or( + 'actual', for_=actual, _with_prefix='actual ' + ) + + f': {entity_and_actual[1]}' + ), _inverted=_inverted, _falsy_exceptions=_falsy_exceptions, ) @@ -128,29 +148,40 @@ def __init__( self.__inverted = _inverted self.__falsy_exceptions = _falsy_exceptions - # TODO: should we store flattened version in self? + # 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: + def compare(entity_and_actual: Tuple[Collection, Iterable]) -> bool: + entity, actual = entity_and_actual 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 + if entity.config._ignore_case or _ignore_case else by(map(str, expected_flattened))(map(str, actual)) ) super().__init__( - ( - f'{name}{" ignoring case:" if _ignore_case else ""}' - f' {list(expected)}' - # todo: refactor to and change tests correspondingly: - # f'{" ignoring case:" if _ignore_case else ":"} {expected}' + lambda maybe_entity: ( + name + + ( + ' ignoring case:' + if (maybe_entity is not None and maybe_entity.config._ignore_case) + or _ignore_case + else '' + ) + + f' {list(expected)}' ), - actual=actual, + actual=lambda entity: (entity, actual(entity)), by=compare, + _describe_actual_result=lambda entity_and_actual: ( + Query._full_description_or( + 'actual', for_=actual, _with_prefix='actual ' + ) + + f': {entity_and_actual[1]}' + ), _inverted=_inverted, _falsy_exceptions=_falsy_exceptions, ) @@ -360,6 +391,17 @@ def text(expected: str | int | float, _ignore_case=False, _inverted=False): ) +# TODO: is text + exact_text naming still relevant for match.* case over have.*? +# let's compare examples: +# > element.should(have.exact_text('full of partial!')) # 👍🏻 +# > element.should(have.text('partial')) # 👍🏻 +# > element.should(match.exact_text('full of partial!')) # 👍🏻 +# > element.should(match.text('partial')) # 🤨 +# > element.should(match.text_containing('partial')) # 🤔 +# > element.should(match.partial_text('partial')) # 👍🏻 +# > match.text('partial')(element) # 🤨 +# > match.text_containing('partial')(element) # ?? +# > match.partial_text('partial')(element) # 👍🏻 def exact_text(expected: str | int | float, _ignore_case=False, _inverted=False): return _EntityHasSomethingSupportingIgnoreCase( 'has exact text', @@ -379,10 +421,35 @@ def __init__(self, expected: str, _flags=0, _inverted=False): self.__inverted = _inverted super().__init__( - f'has text matching{f" (with flags {_flags}):" if _flags else ""}' - f' {expected}', - actual=query.text, - by=predicate.matches(expected, _flags), + lambda maybe_entity: ( + f'has text matching' + + ( + f' (with flags {flags}):' + if ( + flags := ( + _flags | re.IGNORECASE + if maybe_entity is not None + and maybe_entity.config._ignore_case + else _flags + ) + ) + else '' + ) + + f' {expected}' + ), + actual=lambda entity: (entity, query.text(entity)), + by=lambda entity_and_actual: predicate.matches( + expected, + ( + _flags | re.IGNORECASE + if entity_and_actual[0] is not None + and entity_and_actual[0].config._ignore_case + else _flags + ), + )(entity_and_actual[1]), + _describe_actual_result=lambda entity_and_actual: ( + f'actual text: {entity_and_actual[1]}' + ), _inverted=_inverted, ) @@ -510,6 +577,7 @@ def values_containing( class attribute(Condition[Element]): + # todo: the raw attribute condition does not support ignore_case, should it? def __init__(self, name: str, _inverted=False): self.__expected = name # self.__ignore_case = _ignore_case @@ -736,8 +804,8 @@ class size(Match[Union[Collection, Browser, Element]]): def __init__( self, expected: int | dict, - _name=lambda entity: ( - 'have size' if isinstance(entity, Collection) else 'has size' + _name=lambda maybe_entity: ( + 'have size' if isinstance(maybe_entity, Collection) else 'has size' ), # todo: should we also tune actual rendering based on # config._match_only_visible_elements_size? @@ -746,7 +814,7 @@ def __init__( _inverted=False, ): self.__expected = expected - self.__name = lambda entity: f'{_name(entity)} {expected}' + self.__name = lambda maybe_entity: f'{_name(maybe_entity)} {expected}' self.__by = _by self.__inverted = _inverted @@ -1117,7 +1185,15 @@ def __call__(self, entity: Collection): regex_invalid_error: re.error | None = None try: - answer = re.match(expected_pattern, actual_to_match, self._flags) + answer = re.match( + expected_pattern, + actual_to_match, + ( + self._flags | re.IGNORECASE + if entity.config._ignore_case + else self._flags + ), + ) except re.error as error: # going to re-raise it below as AssertionError on `not answer` regex_invalid_error = error @@ -1130,12 +1206,18 @@ def describe_not_match(): # for item in self._expected # ] return ( - f'actual visible texts:\n {actual_to_render}\n' - '\n' + f'actual ' + + ( + 'visible ' + if entity.config._match_only_visible_elements_texts + else '' + ) + + f'texts:\n {actual_to_render}\n' + + '\n' # f'Pattern explained:\n {pattern_explained}\n' - f'Pattern used for matching:\n {expected_pattern}\n' + + f'Pattern used for matching:\n {expected_pattern}\n' # TODO: consider renaming to Actual merged text for match - f'Actual text used to match:\n {actual_to_match}' + + f'Actual text used to match:\n {actual_to_match}' ) if regex_invalid_error: @@ -1159,6 +1241,8 @@ def __str__(self): # TODO: after previous, we can implement shortcut "ignoring case" # for " (flags: re.IGNORECASE)" return ( + # todo: consider wrapping negated part into () like in other conditions + # todo: consider square brackets arround list of texts like in other conditions f'{self._name_prefix} {"no " if self._inverted else ""}{self._name}' + (f' (flags: {self._flags})' if self._flags else '') + ':' @@ -1181,6 +1265,13 @@ def __str__(self): ) ) + def _name_for(self, entity: Collection | None = None) -> str: + return ( + self.ignore_case.__str__() + if entity is not None and entity.config._ignore_case + else self.__str__() + ) + # TODO: will other methods like or_, and_ – do work? o_O @@ -1291,7 +1382,8 @@ def __init__( self._globs = () # TODO: consider refactoring so this attribute is not even inherited - def where(self): + @override + def where(self, **kwargs): """Just a placeholder. This attribute is not supported for this condition""" raise AttributeError('.where(**) is not supported on text_patterns condition') diff --git a/selene/core/wait.py b/selene/core/wait.py index 9cf4cc44..d8d1fec1 100644 --- a/selene/core/wait.py +++ b/selene/core/wait.py @@ -119,7 +119,7 @@ def logic(fn: Callable[[E], R]) -> R: # TODO: consider using Query.full_description_for(fn) # TODO: consider customizing what to use on __init__ - fn_name = Query.full_name_for(fn, entity) or str(fn) + fn_name = Query._full_name_for(fn, entity) or str(fn) failure = TimeoutException( f'\n' diff --git a/selene/support/conditions/have.py b/selene/support/conditions/have.py index 45d77031..ae293850 100644 --- a/selene/support/conditions/have.py +++ b/selene/support/conditions/have.py @@ -40,6 +40,14 @@ def text(partial_value: str | int | float): return match.text(partial_value) +# todo: why not exact_text_matching o_O? +# is match.exact_text still ok, when it's match not have? +# let's compare: +# > element.should(have.exact_text('full of partial!')) # 👍🏻 +# > element.should(have.text('partial')) # hm, seems like 👍🏻 +# > element.should(have.text_matching('partial').not_) # hm, seems like 👍🏻 +# > element.should(have.text_matching('.*partial.*')) # 👍🏻 +# – seems like no need in exact_ prefix... def text_matching(regex_pattern: str): return match.text_pattern(regex_pattern) @@ -86,21 +94,19 @@ def attribute(name: str, value: Optional[str] = None): return match.attribute(name) -def value(text: str | int | float) -> Condition[Element]: +def value(text: str | int | float): return match.value(text) -def values(*texts: str | int | float | Iterable[str]) -> Condition[Collection]: +def values(*texts: str | int | float | Iterable[str]): return match.values(*texts) -def value_containing(partial_text: str | int | float) -> Condition[Element]: +def value_containing(partial_text: str | int | float): return match.value_containing(partial_text) -def values_containing( - *partial_texts: str | int | float | Iterable[str], -) -> Condition[Collection]: +def values_containing(*partial_texts: str | int | float | Iterable[str]): return match.values_containing(*partial_texts) @@ -108,11 +114,11 @@ def css_class(name): return match.css_class(name) -def tag(name: str) -> Condition[Element]: +def tag(name: str): return match.tag(name) -def tag_containing(name: str) -> Condition[Element]: +def tag_containing(name: str): return match.tag_containing(name) diff --git a/tests/integration/condition__browser__have_url_test.py b/tests/integration/condition__browser__have_url_test.py index 6f00904f..08cc7f04 100644 --- a/tests/integration/condition__browser__have_url_test.py +++ b/tests/integration/condition__browser__have_url_test.py @@ -43,19 +43,46 @@ def test_have_url_containing__ignore_case(session_browser): browser = session_browser.with_(timeout=0.1) GivenPage(browser.driver).opened_empty() browser.should(have.url_containing('EMPTY.html').ignore_case) + try: + browser.should(have.url_containing('NONEMPTY.html').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.has url containing ignoring case: 'NONEMPTY.html'\n" + '\n' + 'Reason: ConditionMismatch: actual url: ' + ) in str(error) + assert re.findall(r'file:.*empty\.html\n', str(error)) + try: + browser.with_(_ignore_case=True).should(have.url_containing('NONEMPTY.html')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.has url containing ignoring case: 'NONEMPTY.html'\n" + '\n' + 'Reason: ConditionMismatch: actual url: ' + ) in str(error) + assert re.findall(r'file:.*empty\.html\n', str(error)) try: browser.should(have.no.url_containing('EMPTY.html').ignore_case) pytest.fail('expect mismatch') except AssertionError as error: - assert re.findall( - re.escape( - "browser.has no (url containing ignoring case: 'EMPTY.html')\n" - '\n' - 'Reason: ConditionMismatch: actual url: ' - ) - + r'file:.*empty\.html\n', - str(error), - ) + assert ( + "browser.has no (url containing ignoring case: 'EMPTY.html')\n" + '\n' + 'Reason: ConditionMismatch: actual url: ' + ) in str(error) + assert re.findall(r'file:.*empty\.html\n', str(error)) + try: + browser.with_(_ignore_case=True).should(have.no.url_containing('EMPTY.html')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.has no (url containing ignoring case: 'EMPTY.html')\n" + '\n' + 'Reason: ConditionMismatch: actual url: ' + ) in str(error) + assert re.findall(r'file:.*empty\.html\n', str(error)) browser.should(have.no.url_containing('start_page.xhtml')) browser.should(have.no.url_containing('start_page.xhtml').ignore_case) diff --git a/tests/integration/condition__element__have_exact_text_test.py b/tests/integration/condition__element__have_exact_text_test.py index 844c14cd..b56dc9d7 100644 --- a/tests/integration/condition__element__have_exact_text_test.py +++ b/tests/integration/condition__element__have_exact_text_test.py @@ -118,6 +118,121 @@ def test_should_have_exact_text__passed_and_failed__with_text_to_trim(session_br ) in str(error) +def test_should_have_exact_text__passed_and_failed__with_text_to_trim__ignore_case( + session_browser, +): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + ''' + ) + + # THEN + + # have exact text? + # - visible and correct expected (normalized) passes + s('#visible').should(match.exact_text('ONE !!!').ignore_case) + s('#visible').should(have.exact_text('ONE !!!').ignore_case) + s('#visible').should(have.exact_text('ONE !!!').ignore_case.not_.not_) + s('#visible').should(have.no.exact_text('ONE !!!').ignore_case.not_) + s('#visible').with_(_ignore_case=True).should(have.exact_text('ONE !!!')) + # - visible and incorrect expected (partial) fails + try: + s('#visible').should(have.exact_text('ONE').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has exact text ignoring case: " + "'ONE'\n" + '\n' + 'Reason: ConditionMismatch: actual text: One !!!\n' + ) in str(error) + try: + s('#visible').with_(_ignore_case=True).should(have.exact_text('ONE')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has exact text ignoring case: " + "'ONE'\n" + '\n' + 'Reason: ConditionMismatch: actual text: One !!!\n' + ) in str(error) + # - visible & empty and correct expected passes + s('#visible-empty').should(have.exact_text('').ignore_case) + s('#visible-empty').should(have.exact_text('').ignore_case.not_.not_) + + s('#visible-empty').with_(_ignore_case=True).should(have.exact_text('')) + # - visible & non-textable (like input) with always '' expected passes + ''' + # let's just skip it:) + ''' + # - hidden and with always '' expected passes + s('#hidden').should(have.exact_text('').ignore_case) + s('#hidden').with_(_ignore_case=True).should(have.exact_text('')) + # - hidden & empty with always '' expected passes + s('#hidden-empty').should(have.exact_text('').ignore_case) + s('#hidden-empty').with_(_ignore_case=True).should(have.exact_text('')) + # - hidden and incorrect expected of actual exact text fails + ''' + # let's just skip it:) + ''' + # - absent and expected '' fails with failure + try: + s('#absent').should(have.exact_text('').ignore_case) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has exact text ignoring case: " + "''\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ) in str(error) + try: + s('#absent').with_(_ignore_case=True).should(have.exact_text('')) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has exact text ignoring case: " + "''\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ) in str(error) + # - absent and expected '' + double inversion fails with failure + try: + s('#absent').should(have.exact_text('').ignore_case.not_.not_) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has exact text ignoring case: " + "''\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ) in str(error) + try: + s('#absent').with_(_ignore_case=True).should(have.exact_text('').not_.not_) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has exact text ignoring case: " + "''\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ) in str(error) + + def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( session_browser, ): @@ -246,7 +361,163 @@ def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim( ) in str(error) -# todo: consider putting/inserting (or moving from other test suite) here the ignoring_case tests +def test_should_have_no_exact_text__passed_and_failed__with_text_to_trim__ignore_case( + session_browser, +): + s = lambda selector: session_browser.with_(timeout=0.1).element(selector) + GivenPage(session_browser.driver).opened_with_body( + ''' + + ''' + ) + + # THEN + + # have no exact text? + # - visible and incorrect expected (not normalized) passes + s('#visible').should(match.exact_text(' ONE !!!\n').ignore_case.not_) + s('#visible').should(have.no.exact_text(' ONE !!!\n').ignore_case) + s('#visible').should(have.no.exact_text(' ONE !!!\n').ignore_case.not_.not_) + s('#visible').with_(_ignore_case=True).should(match.exact_text(' ONE !!!\n').not_) + s('#visible').with_(_ignore_case=True).should(have.no.exact_text(' ONE !!!\n')) + s('#visible').with_(_ignore_case=True).should( + have.no.exact_text(' ONE !!!\n').not_.not_ + ) + # - visible and incorrect expected (partial) passes + s('#visible').should(match.exact_text('ONE').ignore_case.not_) + s('#visible').should(have.no.exact_text('ONE').ignore_case) + s('#visible').should(have.no.exact_text('ONE').ignore_case.not_.not_) + s('#visible').with_(_ignore_case=True).should(match.exact_text('ONE').not_) + s('#visible').with_(_ignore_case=True).should(have.no.exact_text('ONE')) + s('#visible').with_(_ignore_case=True).should(have.no.exact_text('ONE').not_.not_) + # - visible and correct expected (normalized) fails + try: + s('#visible').should(have.no.exact_text('ONE !!!').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has no (exact text ignoring " + "case: 'ONE !!!')\n" + '\n' + 'Reason: ConditionMismatch: actual text: One !!!\n' + ) in str(error) + try: + s('#visible').with_(_ignore_case=True).should(have.no.exact_text('ONE !!!')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible')).has no (exact text ignoring " + "case: 'ONE !!!')\n" + '\n' + 'Reason: ConditionMismatch: actual text: One !!!\n' + ) in str(error) + # - visible & empty and correct '' expected fails # todo: improve empty text rendering + try: + s('#visible-empty').should(have.no.exact_text('').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible-empty')).has no (exact text " + "ignoring case: '')\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + ) in str(error) + try: + s('#visible-empty').with_(_ignore_case=True).should(have.no.exact_text('')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#visible-empty')).has no (exact text " + "ignoring case: '')\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + ) in str(error) + # - visible & non-textable (like input) with always '' expected fails + ''' + # let's just skip it:) + ''' + # - hidden and with correct always '' expected fails + try: + s('#hidden').should(have.no.exact_text('').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#hidden')).has no (exact text ignoring " + "case: '')\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + ) in str(error) + try: + s('#hidden').with_(_ignore_case=True).should(have.no.exact_text('')) + s('#hidden').with_(ignore_case=True).should(have.no.exact_text('')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#hidden')).has no (exact text ignoring " + "case: '')\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + ) in str(error) + # - hidden & empty with always '' expected fails + try: + s('#hidden-empty').should(have.no.exact_text('').ignore_case) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#hidden-empty')).has no (exact text " + "ignoring case: '')\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + ) in str(error) + try: + s('#hidden-empty').with_(_ignore_case=True).should(have.no.exact_text('')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#hidden-empty')).has no (exact text " + "ignoring case: '')\n" + '\n' + 'Reason: ConditionMismatch: actual text: \n' + ) in str(error) + # - hidden and incorrect expected of actual exact text passes + s('#hidden').should(have.no.exact_text('One !!!').ignore_case) + # - hidden & empty and incorrect expected of actual exact text passes + s('#hidden-empty').should(have.no.exact_text('One !!!').ignore_case) + # - absent and potentially correct expected '' fails with failure + try: + s('#absent').should(have.no.exact_text('').ignore_case) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has no (exact text ignoring " + "case: '')\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ) in str(error) + try: + s('#absent').with_(_ignore_case=True).should(have.no.exact_text('')) + pytest.fail('expect failure') + except AssertionError as error: + assert ( + "browser.element(('css selector', '#absent')).has no (exact text ignoring " + "case: '')\n" + '\n' + 'Reason: NoSuchElementException: no such element: Unable to locate element: ' + '{"method":"css selector","selector":"#absent"}\n' + ) in str(error) + # - absent and potentially correct expected '' + double inversion fails with failure + '''skip it''' + # - absent and incorrect expected STILL fails with failure + '''skip it''' def test_unicode_text_with_trailing_and_leading_spaces(session_browser): 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 051dc5ad..7045e566 100644 --- a/tests/integration/condition__element__have_text_matching__compared_test.py +++ b/tests/integration/condition__element__have_text_matching__compared_test.py @@ -129,7 +129,6 @@ def test_text_matching__regex_pattern__ignore_case__compared(session_browser): browser.all('li').first.should(have.no.text_matching(r'.*one.*')) browser.all('li').first.should(match.exact_text('1) one!!!').not_) # TODO: o_O browser.all('li').first.should(have.no.exact_text('1) one!!!')) - # browser.all('li').first.should(have.no.exact_text('1) one!!!').ignore_case) # TODO: check this fail with error browser.all('li').first.should(match.exact_text('1) one!!!', _ignore_case=True)) browser.all('li').first.should(have.exact_text('1) one!!!').ignore_case) browser.all('li').first.should(match.text('one', _ignore_case=True)) @@ -152,15 +151,10 @@ def test_text_matching__regex_pattern__ignore_case__compared(session_browser): browser.all('li').should( have.texts_matching(r'.*one.*', r'.*two.*', '.*three.*').ignore_case ) - # # TODO: implement - # browser.all('li').should(have.texts('one', 'two', 'three').ignore_case) - # browser.all('li').should( - # have.exact_texts('1) One!!!', '2) Two...', '3) Three???').ignore_case - # ) browser.all('li').should( match._text_patterns_like(r'.*one.*', r'.*two.*', ..., _flags=re.IGNORECASE) ) - # do we even neet a prefix here? like where_? why not just .flage(...) ? + # do we even neet a prefix here? like where_? why not just .flags(...) ? browser.all('li').should( have._text_patterns_like(r'.*one.*', r'.*two.*', ...).where_flags(re.IGNORECASE) ) @@ -296,6 +290,79 @@ def test_text_matching__regex_pattern__ignore_case__compared(session_browser): ) in str(error) +def test_text_matching__regex_pattern__error__on_mismatch__with_ignorecase( + session_browser, +): + browser = session_browser.with_(timeout=0.1) + GivenPage(browser.driver).opened_with_body( + ''' + + ''' + ) + + browser.all('li').first.should(have.text_matching(r'.*one.*').ignore_case) + browser.all('li').first.with_(_ignore_case=True).should( + have.text_matching(r'.*one.*') + ) + browser.all('li').with_(_ignore_case=True).first.should( + have.text_matching(r'.*one.*') + ) + browser.with_(_ignore_case=True).all('li').first.should( + have.text_matching(r'.*one.*') + ) + + try: + browser.all('li').first.should(have.text_matching(r'one').ignore_case) + pytest.fail('expected mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li'))[0].has text matching (with flags " + 're.IGNORECASE): one\n' + '\n' + 'Reason: ConditionMismatch: actual text: 1) One!!!\n' + ) in str(error) + try: + browser.all('li').first.with_(_ignore_case=True).should( + have.text_matching(r'one') + ) + pytest.fail('expected mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li'))[0].has text matching (with flags " + 're.IGNORECASE): one\n' + '\n' + 'Reason: ConditionMismatch: actual text: 1) One!!!\n' + ) in str(error) + + try: + browser.all('li').first.should(have.no.text_matching(r'.*one.*').ignore_case) + pytest.fail('expected mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li'))[0].has no (text matching (with flags " + 're.IGNORECASE): .*one.*)\n' + '\n' + 'Reason: ConditionMismatch: actual text: 1) One!!!\n' + ) in str(error) + + try: + browser.all('li').first.with_(_ignore_case=True).should( + have.no.text_matching(r'.*one.*') + ) + pytest.fail('expected mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li'))[0].has no (text matching (with flags " + 're.IGNORECASE): .*one.*)\n' + '\n' + 'Reason: ConditionMismatch: actual text: 1) One!!!\n' + ) in str(error) + + def test_text_matching__regex_pattern__error__on_invalid_regex__with_ignorecase( session_browser, ): @@ -324,6 +391,22 @@ def test_text_matching__regex_pattern__error__on_invalid_regex__with_ignorecase( 'Screenshot: ' ) in str(error) + try: + browser.all('li').first.with_(_ignore_case=True).should( + have.text_matching(r'*one*') + ) + pytest.fail('expected invalid regex error') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li'))[0].has text matching (with flags " + 're.IGNORECASE): *one*\n' + '\n' + 'Reason: error: nothing to repeat at position ' + '0:\n' + 'actual text: 1) One!!!\n' + 'Screenshot: ' + ) in str(error) + # TODO: decide on expected behavior try: browser.all('li').first.should(have.text_matching(r'*one*').ignore_case.not_) @@ -352,3 +435,19 @@ def test_text_matching__regex_pattern__error__on_invalid_regex__with_ignorecase( 'actual text: 1) One!!!\n' 'Screenshot: ' ) in str(error) + + try: + browser.all('li').first.with_(_ignore_case=True).should( + have.no.text_matching(r'*one*') + ) + pytest.fail('expected invalid regex error') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li'))[0].has no (text matching (with flags " + 're.IGNORECASE): *one*)\n' + '\n' + 'Reason: error: nothing to repeat at position ' + '0:\n' + 'actual text: 1) One!!!\n' + 'Screenshot: ' + ) 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 946cee1c..f61e48ec 100644 --- a/tests/integration/condition__elements__have_attribute_and_co_test.py +++ b/tests/integration/condition__elements__have_attribute_and_co_test.py @@ -216,3 +216,37 @@ def test_have_attribute__condition_variations(session_browser): exercises.first.should(have.value_containing(2)) exercises.should(have.values(20, 30)) exercises.should(have.values_containing(2, 3)) + + # - ignore_case + names.first.should(have.value('john 20TH').ignore_case) + names.first.with_(_ignore_case=True).should(have.value('john 20TH')) + names.first.should(have.value_containing('john').ignore_case) + names.first.with_(_ignore_case=True).should(have.value_containing('john')) + + names.should(have.values('john 20TH', 'doe 2ND').ignore_case) + names.with_(_ignore_case=True).should(have.values('john 20TH', 'doe 2ND')) + names.should(have.values_containing('john', 'doe').ignore_case) + names.with_(_ignore_case=True).should(have.values_containing('john', 'doe')) + + try: + names.first.with_(_ignore_case=True).should(have.no.value('john 20TH')) + pytest.fail('should fail on mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.name'))[0].has no (attribute 'value' with " + "value ignoring case: 'john 20TH')\n" + '\n' + 'Reason: ConditionMismatch: actual attribute value: John 20th\n' + ) in str(error) + + try: + names.with_(_ignore_case=True).should(have.no.values_containing('john', 'doe')) + pytest.fail('should fail on mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', '.name')).has no (attribute 'value' with values " + "containing ignoring case: ['john', 'doe'])\n" + '\n' + "Reason: ConditionMismatch: actual value attributes: ['John 20th', 'Doe " + "2nd']\n" + ) in str(error) 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 e57b2bb2..31355ce5 100644 --- a/tests/integration/condition__elements__have_text_and_co_test.py +++ b/tests/integration/condition__elements__have_text_and_co_test.py @@ -201,7 +201,7 @@ def test_exact_text__including_ignorecase__passed_compared_to_failed( ) in str(error) -def test_texts__including_ignorecase__passed_compared_to_failed( +def test_texts__including_ignorecase__passed_and_failed__compared_to_texts_like( session_browser, ): browser = session_browser.with_(timeout=0.1) @@ -242,9 +242,13 @@ def test_texts__including_ignorecase__passed_compared_to_failed( "Reason: ConditionMismatch: actual: ['1) One!!!', '2) Two...', '3) " "Three???']\n" ) in str(error) - # have.texts (ignore_case) + # have.texts (ignore_case) compared to have.texts_like (ignore_case) browser.all('li').should(match.texts('one', 'two', 'three', _ignore_case=True)) browser.all('li').should(have.texts('one', 'two', 'three').ignore_case) + browser.all('li').with_(_ignore_case=True).should(have.texts('one', 'two', 'three')) + browser.all('li').with_(_ignore_case=True).should( + have._texts_like('one', 'two', ...) + ) try: browser.all('li').should(have.texts('one.', 'two.', 'three.').ignore_case) pytest.fail('expected text mismatch') @@ -256,9 +260,65 @@ def test_texts__including_ignorecase__passed_compared_to_failed( "Reason: ConditionMismatch: actual: ['1) One!!!', '2) Two...', '3) " "Three???']\n" ) in str(error) + try: + browser.all('li').with_(_ignore_case=True).should( + have.texts('one.', 'two.', 'three.') + ) + pytest.fail('expected text mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li')).have texts ignoring case: ['one.', " + "'two.', 'three.']\n" + '\n' + "Reason: ConditionMismatch: actual: ['1) One!!!', '2) Two...', '3) " + "Three???']\n" + ) in str(error) + try: + browser.all('li').with_(_ignore_case=True).should( + have._texts_like('one.', 'two.', ...) + ) + pytest.fail('expected text mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li')).have texts like (flags: re.IGNORECASE):\n" + ' one., two., ...\n' + '\n' + 'Reason: AssertionError: actual visible texts:\n' + ' 1) One!!!, 2) Two..., 3) Three???\n' + '\n' + 'Pattern used for matching:\n' + ' ^.*?one\\..*?‚.*?two\\..*?‚[^‚]+‚$\n' + 'Actual text used to match:\n' + ' 1) One!!!‚2) Two...‚3) Three???‚\n' + ) in str(error) + try: + browser.all('li').with_( + _ignore_case=True, + _match_only_visible_elements_texts=False, + ).should(have._texts_like('one.', 'two.', ...)) + pytest.fail('expected text mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li')).have texts like (flags: re.IGNORECASE):\n" + ' one., two., ...\n' + '\n' + 'Reason: AssertionError: actual texts:\n' + ' 1) One!!!, 2) Two..., 3) Three???\n' + '\n' + 'Pattern used for matching:\n' + ' ^.*?one\\..*?‚.*?two\\..*?‚[^‚]+‚$\n' + 'Actual text used to match:\n' + ' 1) One!!!‚2) Two...‚3) Three???‚\n' + ) in str(error) # - inverted # - - with no before browser.all('li').should(have.no.texts('one.', 'two.', 'three.').ignore_case) + browser.all('li').with_(_ignore_case=True).should( + have.no.texts('one.', 'two.', 'three.') + ) + browser.all('li').with_(_ignore_case=True).should( + have.no._texts_like('one.', 'two.', ...) + ) try: browser.all('li').should(have.no.texts('one', 'two', 'three').ignore_case) pytest.fail('expected mismatch') @@ -270,6 +330,58 @@ def test_texts__including_ignorecase__passed_compared_to_failed( "Reason: ConditionMismatch: actual: ['1) One!!!', '2) Two...', '3) " "Three???']\n" ) in str(error) + try: + browser.all('li').with_(_ignore_case=True).should( + have.no.texts('one', 'two', 'three') + ) + pytest.fail('expected mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li')).have no (texts ignoring case: ['one', " + "'two', 'three'])\n" + '\n' + "Reason: ConditionMismatch: actual: ['1) One!!!', '2) Two...', '3) " + "Three???']\n" + ) in str(error) + try: + browser.all('li').with_(_ignore_case=True).should( + have.no._texts_like('one', 'two', ...) + ) + pytest.fail('expected mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li')).have no texts like (flags: " + 're.IGNORECASE):\n' + ' one, two, ...\n' + '\n' + 'Reason: AssertionError: actual visible texts:\n' + ' 1) One!!!, 2) Two..., 3) Three???\n' + '\n' + 'Pattern used for matching:\n' + ' ^.*?one.*?‚.*?two.*?‚[^‚]+‚$\n' + 'Actual text used to match:\n' + ' 1) One!!!‚2) Two...‚3) Three???‚\n' + ) in str(error) + try: + browser.all('li').with_( + _ignore_case=True, + _match_only_visible_elements_texts=False, + ).should(have.no._texts_like('one', 'two', ...)) + pytest.fail('expected mismatch') + except AssertionError as error: + assert ( + "browser.all(('css selector', 'li')).have no texts like (flags: " + 're.IGNORECASE):\n' + ' one, two, ...\n' + '\n' + 'Reason: AssertionError: actual texts:\n' + ' 1) One!!!, 2) Two..., 3) Three???\n' + '\n' + 'Pattern used for matching:\n' + ' ^.*?one.*?‚.*?two.*?‚[^‚]+‚$\n' + 'Actual text used to match:\n' + ' 1) One!!!‚2) Two...‚3) Three???‚\n' + ) in str(error) # - - with not after, in the end # (in the middle works but without Autocomplete & not recommended) browser.all('li').should(have.texts('one.', 'two.', 'three.').ignore_case.not_)