diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ca84de9..01194164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,10 +101,29 @@ TODOs: ## 2.0.0rc10: «copy&paste, frames, shadow & texts_like» (to be released on DD.05.2024) -### texts like conditions now accepts int and floats as text item +### TODO: have.js_property -> have.property ? + + +### TODO: re.IGNORECASE in have.text_matching and have.texts_matching, etc. + +... + +### 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']` +Full list of conditions updated: + +- `have.texts` +- `have.exact_texts` +- `have.text` +- `have.exact_text` +- `have.value` +- `have.value_containing` +- `have.attribute(name).*` (all `*`) +- `have.js_property(name).*` (all `*`) +- `have.no.*` versions of same conditions + ### regex support for element conditions that assert element text List of element conditions added: diff --git a/selene/common/predicate.py b/selene/common/predicate.py index 54ff3b8d..29279a42 100644 --- a/selene/common/predicate.py +++ b/selene/common/predicate.py @@ -27,13 +27,21 @@ def is_truthy(something): return bool(something) if not something == '' else True -def equals_ignoring_case(expected): +def str_equals_ignoring_case(expected): return lambda actual: str(expected).lower() == str(actual).lower() -def equals(expected, ignore_case=False): +def equals(expected, ignore_case=False): # TODO: remove ignore_case from here return lambda actual: ( - expected == actual if not ignore_case else equals_ignoring_case(expected) + expected == actual if not ignore_case else str_equals_ignoring_case(expected) + ) + + +def str_equals(expected, ignore_case=False): + return lambda actual: ( + str(expected) == str(actual) + if not ignore_case + else str_equals_ignoring_case(expected) ) @@ -57,7 +65,7 @@ def matches(pattern): return lambda actual: re.match(pattern, str(actual)) -def includes_ignoring_case(expected): +def str_includes_ignoring_case(expected): return lambda actual: str(expected).lower() in str(actual).lower() @@ -67,7 +75,21 @@ def fn(actual): return ( expected in actual if not ignore_case - else includes_ignoring_case(expected) + else str_includes_ignoring_case(expected) + ) + except TypeError: + return False + + return fn + + +def str_includes(expected, ignore_case=False): + def fn(actual): + try: + return ( + str(expected) in actual # TODO: should we wrap actual with str()? + if not ignore_case + else str_includes_ignoring_case(expected) ) except TypeError: return False @@ -83,7 +105,7 @@ def includes_word(expected, ignore_case=False): return lambda actual: ( expected in re.split(r'\s+', actual) if not ignore_case - else includes_ignoring_case(expected) + else str_includes_ignoring_case(expected) ) @@ -113,4 +135,6 @@ def includes_word(expected, ignore_case=False): equals_to_list = list_compare_by(equals) +str_equals_to_list = list_compare_by(str_equals) equals_by_contains_to_list = list_compare_by(includes) +str_equals_by_contains_to_list = list_compare_by(str_includes) diff --git a/selene/core/match.py b/selene/core/match.py index a5c272a4..e7163b17 100644 --- a/selene/core/match.py +++ b/selene/core/match.py @@ -90,18 +90,18 @@ def element_has_text( - expected: str, - describing_matched_to='has text', - compared_by_predicate_to=predicate.includes, + expected: str | int | float, + _describing_matched_to='has text', + _compared_by_predicate_to=predicate.includes, ) -> Condition[Element]: return ElementCondition.raise_if_not_actual( - describing_matched_to + ' ' + expected, + _describing_matched_to + ' ' + str(expected), query.text, - compared_by_predicate_to(expected), + _compared_by_predicate_to(str(expected)), ) -def element_has_exact_text(expected: str) -> Condition[Element]: +def element_has_exact_text(expected: str | int | float) -> Condition[Element]: return element_has_text(expected, 'has exact text', predicate.equals) @@ -114,6 +114,9 @@ def text_pattern(expected: str) -> Condition[Element]: def element_has_js_property(name: str): + # TODO: will this even work for mobile? o_O + # if .get_property is valid for mobile + # then we should rename it for sure here... # TODO: should we keep simpler but less obvious name - *_has_property ? def property_value(element: Element): return element.locate().get_property(name) @@ -126,38 +129,40 @@ def property_values(collection: Collection): ) class ConditionWithValues(ElementCondition): - def value(self, expected: str) -> Condition[Element]: + def value(self, expected: str | int | float) -> Condition[Element]: return ElementCondition.raise_if_not_actual( f"has js property '{name}' with value '{expected}'", property_value, - predicate.equals(expected), + predicate.str_equals(expected), ) - def value_containing(self, expected: str) -> Condition[Element]: + def value_containing(self, expected: str | int | float) -> Condition[Element]: return ElementCondition.raise_if_not_actual( f"has js property '{name}' with value containing '{expected}'", property_value, - predicate.includes(expected), + predicate.str_includes(expected), ) - def values(self, *expected: Union[str, Iterable[str]]) -> Condition[Collection]: + def values( + self, *expected: str | int | float | Iterable[str] + ) -> Condition[Collection]: expected_ = helpers.flatten(expected) return CollectionCondition.raise_if_not_actual( f"has js property '{name}' with values '{expected_}'", property_values, - predicate.equals_to_list(expected_), + predicate.str_equals_to_list(expected_), ) def values_containing( - self, *expected: Union[str, Iterable[str]] + self, *expected: str | int | float | Iterable[str] ) -> Condition[Collection]: expected_ = helpers.flatten(expected) return CollectionCondition.raise_if_not_actual( f"has js property '{name}' with values containing '{expected_}'", property_values, - predicate.equals_by_contains_to_list(expected_), + predicate.str_equals_by_contains_to_list(expected_), ) return ConditionWithValues(str(raw_property_condition), raw_property_condition.call) @@ -225,7 +230,9 @@ def attribute_values(collection: Collection): # 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, ignore_case=False) -> Condition[Element]: + 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', @@ -234,11 +241,11 @@ def value(self, expected: str, ignore_case=False) -> Condition[Element]: return ElementCondition.raise_if_not_actual( f"has attribute '{name}' with value '{expected}'", attribute_value, - predicate.equals(expected, ignore_case), + predicate.str_equals(expected, ignore_case), ) def value_containing( - self, expected: str, ignore_case=False + self, expected: str | int | float, ignore_case=False ) -> Condition[Element]: if ignore_case: warnings.warn( @@ -248,27 +255,29 @@ def value_containing( return ElementCondition.raise_if_not_actual( f"has attribute '{name}' with value containing '{expected}'", attribute_value, - predicate.includes(expected, ignore_case), + predicate.str_includes(expected, ignore_case), ) - def values(self, *expected: Union[str, Iterable[str]]) -> Condition[Collection]: + def values( + self, *expected: str | int | float | Iterable[str] + ) -> Condition[Collection]: expected_ = helpers.flatten(expected) return CollectionCondition.raise_if_not_actual( f"has attribute '{name}' with values '{expected_}'", attribute_values, - predicate.equals_to_list(expected_), + predicate.str_equals_to_list(expected_), ) def values_containing( - self, *expected: Union[str, Iterable[str]] + self, *expected: str | int | float | Iterable[str] ) -> Condition[Collection]: expected_ = helpers.flatten(expected) return CollectionCondition.raise_if_not_actual( f"has attribute '{name}' with values containing '{expected_}'", attribute_values, - predicate.equals_by_contains_to_list(expected_), + predicate.str_equals_by_contains_to_list(expected_), ) return ConditionWithValues( @@ -281,22 +290,22 @@ def values_containing( ) -def element_has_value(expected: str) -> Condition[Element]: +def element_has_value(expected: str | int | float) -> Condition[Element]: return element_has_attribute('value').value(expected) -def element_has_value_containing(expected: str) -> Condition[Element]: +def element_has_value_containing(expected: str | int | float) -> Condition[Element]: return element_has_attribute('value').value_containing(expected) def collection_has_values( - *expected: Union[str, Iterable[str]] + *expected: str | int | float | Iterable[str], ) -> Condition[Collection]: return element_has_attribute('value').values(*expected) def collection_has_values_containing( - *expected: Union[str, Iterable[str]] + *expected: str | int | float | Iterable[str], ) -> Condition[Collection]: return element_has_attribute('value').values_containing(*expected) @@ -395,8 +404,10 @@ def collection_has_size_less_than_or_equal( ) -# TODO: make it configurable whether assert only visible texts or ot -def collection_has_texts(*expected: Union[str, Iterable[str]]) -> Condition[Collection]: +# TODO: make it configurable whether assert only visible texts or not +def collection_has_texts( + *expected: str | int | float | Iterable[str], +) -> Condition[Collection]: expected_ = helpers.flatten(expected) def actual_visible_texts(collection: Collection) -> List[str]: @@ -407,14 +418,14 @@ def actual_visible_texts(collection: Collection) -> List[str]: return CollectionCondition.raise_if_not_actual( f'has texts {expected_}', Query('visible texts', actual_visible_texts), - predicate.equals_by_contains_to_list(expected_), + predicate.str_equals_by_contains_to_list(expected_), ) def collection_has_exact_texts( *expected: str | int | float | Iterable[str], ): - if ... in expected: # TODO count other cases + if ... in expected: # TODO: count other cases raise ValueError( '... is not allowed in exact_texts for "globbing"' 'use _exact_texts_like condition instead' @@ -427,13 +438,13 @@ def collection_has_exact_texts( ], ) - # flatten expected values and convert numbers to strings - expected_flattened_stringified = [str(item) for item in helpers.flatten(expected)] + # flatten expected values + expected_ = helpers.flatten(expected) return CollectionCondition.raise_if_not_actual( # TODO: should we use just expected here ↙ ? - f'has exact texts {expected_flattened_stringified}', + f'has exact texts {expected_}', actual_visible_texts, - predicate.equals_to_list(expected_flattened_stringified), + predicate.str_equals_to_list(expected_), ) @@ -1068,7 +1079,7 @@ def browser_has_js_returned(expected: Any, script: str, *args) -> Condition[Brow def browser_has_script_returned( expected: Any, script: str, *args -) -> Condition[Browser]: +) -> Condition[Browser]: # TODO: should it work on element too? on collection? def script_result(browser: Browser): return browser.driver.execute_script(script, *args) diff --git a/selene/support/conditions/have.py b/selene/support/conditions/have.py index 90313a90..b2d401b6 100644 --- a/selene/support/conditions/have.py +++ b/selene/support/conditions/have.py @@ -32,12 +32,12 @@ no = _not_ -def exact_text(value: str) -> Condition[Element]: +def exact_text(value: str | int | float) -> Condition[Element]: return match.element_has_exact_text(value) # TODO: consider accepting int -def text(partial_value: str) -> Condition[Element]: +def text(partial_value: str | int | float) -> Condition[Element]: return match.element_has_text(partial_value) @@ -79,20 +79,20 @@ def attribute(name: str, value: Optional[str] = None): return match.element_has_attribute(name) -def value(text) -> Condition[Element]: +def value(text: str | int | float) -> Condition[Element]: return match.element_has_value(text) -def values(*texts: Union[str, Iterable[str]]) -> Condition[Collection]: +def values(*texts: str | int | float | Iterable[str]) -> Condition[Collection]: return match.collection_has_values(*texts) -def value_containing(partial_text) -> Condition[Element]: +def value_containing(partial_text: str | int | float) -> Condition[Element]: return match.element_has_value_containing(partial_text) def values_containing( - *partial_texts: Union[str, Iterable[str]] + *partial_texts: str | int | float | Iterable[str], ) -> Condition[Collection]: return match.collection_has_values_containing(*partial_texts) @@ -141,7 +141,7 @@ def size_greater_than_or_equal(number: int) -> Condition[Collection]: # TODO: consider accepting ints -def texts(*partial_values: str | Iterable[str]) -> Condition[Collection]: +def texts(*partial_values: str | int | float | Iterable[str]) -> Condition[Collection]: return match.collection_has_texts(*partial_values) diff --git a/selene/support/conditions/not_.py b/selene/support/conditions/not_.py index 8659586f..cfc0d7ca 100644 --- a/selene/support/conditions/not_.py +++ b/selene/support/conditions/not_.py @@ -53,16 +53,15 @@ # --- have.* conditions --- # -def exact_text(value) -> Condition[Element]: +def exact_text(value: str | int | float) -> Condition[Element]: return _match.element_has_exact_text(value).not_ -# TODO: consider accepting int -def text(partial_value) -> Condition[Element]: +def text(partial_value: str | int | float) -> Condition[Element]: return _match.element_has_text(partial_value).not_ -def text_matching(regex_pattern) -> Condition[Element]: +def text_matching(regex_pattern: str) -> Condition[Element]: return _match.text_pattern(regex_pattern).not_ @@ -82,16 +81,20 @@ def attribute(name: str, *args, **kwargs): original = _match.element_has_attribute(name) negated = original.not_ - def value(self, expected: str, ignore_case=False) -> Condition[Element]: + 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, ignore_case=False) -> Condition[Element]: + 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) -> Condition[Collection]: + def values(self, *expected: str | int | float) -> Condition[Collection]: return original.values(*expected).not_ - def values_containing(self, *expected: str) -> Condition[Collection]: + def values_containing(self, *expected: str | int | float) -> Condition[Collection]: return original.values_containing(*expected).not_ negated.value = value @@ -118,16 +121,20 @@ def js_property(name: str, *args, **kwargs): original = _match.element_has_js_property(name) negated = original.not_ - def value(self, expected: str) -> Condition[Element]: + def value(self, expected: str | int | float) -> Condition[Element]: return original.value(expected).not_ - def value_containing(self, expected: str) -> Condition[Element]: + def value_containing(self, expected: str | int | float) -> Condition[Element]: return original.value_containing(expected).not_ - def values(self, *expected: str) -> Condition[Collection]: + def values( + self, *expected: str | int | float | Iterable[str] + ) -> Condition[Collection]: return original.values(*expected).not_ - def values_containing(self, *expected: str) -> Condition[Collection]: + def values_containing( + self, *expected: str | int | float | Iterable[str] + ) -> Condition[Collection]: return original.values_containing(*expected).not_ negated.value = value @@ -174,15 +181,15 @@ def values_containing(self, *expected: str) -> Condition[Collection]: return negated -def value(text) -> Condition[Element]: +def value(text: str | int | float) -> Condition[Element]: return _match.element_has_value(text).not_ -def value_containing(partial_text) -> Condition[Element]: +def value_containing(partial_text: str | int | float) -> Condition[Element]: return _match.element_has_value_containing(partial_text).not_ -def css_class(name) -> Condition[Element]: +def css_class(name: str) -> Condition[Element]: return _match.element_has_css_class(name).not_ @@ -225,8 +232,7 @@ def size_greater_than_or_equal(number: int) -> Condition[Collection]: return _match.collection_has_size_greater_than_or_equal(number).not_ -# TODO: consider accepting ints -def texts(*partial_values: str) -> Condition[Collection]: +def texts(*partial_values: str | int | float | Iterable[str]) -> Condition[Collection]: return _match.collection_has_texts(*partial_values).not_ @@ -298,3 +304,7 @@ def js_returned_true(script_to_return_bool: str) -> Condition[Browser]: def js_returned(expected: Any, script: str, *args) -> Condition[Browser]: return _match.browser_has_js_returned(expected, script, *args).not_ + + +def script_returned(expected: Any, script: str, *args) -> Condition[Browser]: + return _match.browser_has_script_returned(expected, script, *args).not_ diff --git a/tests/integration/condition__element__have_exact_text_test.py b/tests/integration/condition__element__have_exact_text_test.py index a4dd4bb5..06db9fdd 100644 --- a/tests/integration/condition__element__have_exact_text_test.py +++ b/tests/integration/condition__element__have_exact_text_test.py @@ -23,12 +23,18 @@ from tests.integration.helpers.givenpage import GivenPage +# TODO: consider breaking it down into separate tests + + def test_unicode_text_with_trailing_and_leading_spaces(session_browser): GivenPage(session_browser.driver).opened_with_body( ''' ''' ) @@ -36,3 +42,4 @@ def test_unicode_text_with_trailing_and_leading_spaces(session_browser): element = session_browser.element('li') element.should(have.exact_text('Саше')).should(have.text('Са')) + session_browser.all('li')[-2].should(have.exact_text(100)).should(have.text(100)) diff --git a/tests/integration/condition__elements__have_attribute_and_co_test.py b/tests/integration/condition__elements__have_attribute_and_co_test.py new file mode 100644 index 00000000..68a42fa8 --- /dev/null +++ b/tests/integration/condition__elements__have_attribute_and_co_test.py @@ -0,0 +1,81 @@ +# 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. +import pytest + +from selene import have +from tests.integration.helpers.givenpage import GivenPage + + +# TODO: consider breaking it down into separate tests + + +def test_have_attribute__condition_variations(session_browser): + browser = session_browser.with_(timeout=0.1) + GivenPage(session_browser.driver).opened_with_body( + ''' + + + ''' + ) + + names = browser.all('.name') + exercises = browser.all('.exercise') + + # the following passes too, cause js prop exists, are we ok with that? + browser.element('ul').should(have.attribute('id')) + + names.should(have.attribute('id').values('firstname', 'lastname')) + names.should(have.attribute('id').values_containing('first', 'last')) + + exercises.should(have.attribute('value').values(20, 30)) + names.should(have.attribute('value').values_containing(20, 2)) + names.should(have.attribute('id').values_containing(20, 2).not_) + try: + names.should(have.attribute('value').values_containing(20, 2).not_) + 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" + '\n' + 'Reason: ConditionNotMatchedError: condition not matched\n' + ) in str(error) + + names.should(have.attribute('id').value('name').each.not_) + 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) + names.should(have.attribute('id').value_containing('first').each.not_) + + # aliases + exercises.first.should(have.value(20)) + exercises.first.should(have.value_containing(2)) + exercises.should(have.values(20, 30)) + exercises.should(have.values_containing(2, 3)) diff --git a/tests/integration/condition__elements__have_property_and_co_test.py b/tests/integration/condition__elements__have_property_and_co_test.py new file mode 100644 index 00000000..26354ab5 --- /dev/null +++ b/tests/integration/condition__elements__have_property_and_co_test.py @@ -0,0 +1,72 @@ +# 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. +import pytest + +from selene import have +from tests.integration.helpers.givenpage import GivenPage + + +# TODO: consider breaking it down into separate tests + + +def test_have_property__condition_variations(session_browser): + browser = session_browser.with_(timeout=0.1) + GivenPage(session_browser.driver).opened_with_body( + ''' + + + ''' + ) + + names = browser.all('.name') + exercises = browser.all('.exercise') + + names.first.type('John 20th') + names.second.type('Doe 2nd') + exercises.first.type('20') + exercises.second.type('30') + + # the following passes too, cause js prop exists, are we ok with that? + browser.element('ul').should(have.attribute('id')) + + exercises.should(have.js_property('value').values(20, 30)) + names.should(have.js_property('value').values_containing(20, 2)) + try: + names.should(have.js_property('value').values_containing(20, 2).not_) + 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 (js property 'value' with values " + "containing '(20, 2)')\n" + '\n' + 'Reason: ConditionNotMatchedError: condition not matched\n' + ) in str(error) + + exercises.first.should(have.js_property('value').value(20)) + exercises.first.should(have.js_property('value').value_containing(2)) diff --git a/tests/integration/condition__elements__have_text_and_co_test.py b/tests/integration/condition__elements__have_text_and_co_test.py new file mode 100644 index 00000000..479e0e24 --- /dev/null +++ b/tests/integration/condition__elements__have_text_and_co_test.py @@ -0,0 +1,69 @@ +# 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. +import pytest + +from selene import have +from tests.integration.helpers.givenpage import GivenPage + + +# TODO: consider breaking it down into separate tests, +# and remove duplicates with other test suites + + +def test_have_text__condition_variations(session_browser): + browser = session_browser.with_(timeout=0.1) + GivenPage(session_browser.driver).opened_with_body( + ''' + + + ''' + ) + + names = browser.all('.name') + exercises = browser.all('.exercise') + + exercises.should(have.exact_texts(20, 30)) + exercises.should(have.exact_texts('20', '30')) + exercises.should(have.texts(2, 3)) + exercises.should(have.texts('2', '3')) + exercises.should(have.text('0').each) + exercises.should(have.text(0).each) + exercises.second.should(have.no.exact_text(20)) + exercises.should(have.exact_text(20).each.not_) + # exercises.should(have.no.exact_text(20).each) # TODO: fix it + + try: + names.should(have.texts(20, 2).not_) + pytest.fail('should fail on texts mismatch') + except AssertionError as error: + assert ( + 'Timed out after 0.1s, while waiting for:\n' + "browser.all(('css selector', '.name')).has no (texts (20, 2))\n" + '\n' + 'Reason: ConditionNotMatchedError: condition not matched\n' + ) in str(error)