From 8cb70981ca7e1fc85e626cc8a89c06bcaa12c27e Mon Sep 17 00:00:00 2001 From: yashaka Date: Wed, 17 Jul 2024 18:13:04 +0300 Subject: [PATCH] [#530] REFACTOR: match.native_property and match.css_property impl --- CHANGELOG.md | 5 +- selene/core/match.py | 181 ++++++------------ selene/core/query.py | 28 +++ selene/support/conditions/have.py | 4 +- selene/support/conditions/not_.py | 70 ++----- ...on__elements__have_property_and_co_test.py | 4 +- tests/integration/condition__mixed_test.py | 23 +++ 7 files changed, 132 insertions(+), 183 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043724e9..44b5f422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,9 @@ Then we have to consider rething condition.__call__ aliases... And corresponding `Test` might be also a good candidate over `Match` ... But `Test` does not correlate in `entity.should(Test(actual=..., by=...)) ### TODO: Ensure type errors on element.should(collection_condition), etc. + +check vscode pylance, mypy, jetbrains qodana... + ### TODO: check if there are no type errors on passing be._empty to should ### TODO: decide on ... vs (...,) as one_or_more @@ -210,8 +213,6 @@ like in `clickable = Match('clickable', by=be.visible.and_(be.enabled))` 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 - ### Deprecated conditions - `be.present` in favor of `be.present_in_dom` diff --git a/selene/core/match.py b/selene/core/match.py index e22848f6..6a9331d8 100644 --- a/selene/core/match.py +++ b/selene/core/match.py @@ -507,176 +507,116 @@ def where_flags(self, flags: re.RegexFlag, /) -> Condition[Element]: ) -def native_property(name: str): - def property_value(element: Element): - return element.locate().get_property(name) - - def property_values(collection: Collection): - return [element.get_property(name) for element in collection()] - - raw_property_condition = ElementCondition.raise_if_not_actual( - 'has native property ' + name, property_value, predicate.is_truthy - ) - - class PropertyWithValues(ElementCondition): - def value(self, expected: str | int | float) -> Condition[Element]: - return ElementCondition.raise_if_not_actual( - f"has native property '{name}' with value '{expected}'", - property_value, - predicate.str_equals(expected), - ) - - def value_containing(self, expected: str | int | float) -> Condition[Element]: - return ElementCondition.raise_if_not_actual( - f"has native property '{name}' with value containing '{expected}'", - property_value, - predicate.str_includes(expected), - ) - - def values( - self, *expected: str | int | float | Iterable[str] - ) -> Condition[Collection]: - expected_ = helpers.flatten(expected) - - return CollectionCondition.raise_if_not_actual( - f"has native property '{name}' with values '{expected_}'", - property_values, - predicate.str_equals_to_list(expected_), - ) - - def values_containing( - self, *expected: str | int | float | Iterable[str] - ) -> Condition[Collection]: - expected_ = helpers.flatten(expected) - - return CollectionCondition.raise_if_not_actual( - f"has native property '{name}' with values containing '{expected_}'", - property_values, - predicate.str_equals_by_contains_to_list(expected_), - ) - - return PropertyWithValues( - str(raw_property_condition), test=raw_property_condition.__call__ - ) - - -def element_has_css_property(name: str): - def property_value(element: Element) -> str: - return element().value_of_css_property(name) - - def property_values(collection: Collection) -> List[str]: - return [element.value_of_css_property(name) for element in collection()] - - raw_property_condition = ElementCondition.raise_if_not_actual( - 'has css property ' + name, property_value, predicate.is_truthy - ) - - class ConditionWithValues(ElementCondition): - def value(self, expected: str) -> Condition[Element]: - return ElementCondition.raise_if_not_actual( - f"has css property '{name}' with value '{expected}'", - property_value, - predicate.equals(expected), - ) - - def value_containing(self, expected: str) -> Condition[Element]: - return ElementCondition.raise_if_not_actual( - f"has css property '{name}' with value containing '{expected}'", - property_value, - predicate.includes(expected), - ) - - def values(self, *expected: Union[str, Iterable[str]]) -> Condition[Collection]: - expected_ = helpers.flatten(expected) - - return CollectionCondition.raise_if_not_actual( - f"has css property '{name}' with values '{expected_}'", - property_values, - predicate.equals_to_list(expected_), - ) - - def values_containing( - self, *expected: Union[str, Iterable[str]] - ) -> Condition[Collection]: - expected_ = helpers.flatten(expected) - - return CollectionCondition.raise_if_not_actual( - f"has css property '{name}' with values containing '{expected_}'", - property_values, - predicate.equals_by_contains_to_list(expected_), - ) - - return ConditionWithValues( - str(raw_property_condition), test=raw_property_condition.__call__ - ) - - -class attribute(Condition[Element]): +class _ElementDescriptor(Condition[Element]): # todo: the raw attribute condition does not support ignore_case, should it? - def __init__(self, name: str, _inverted=False): + def __init__( + self, + name: str, + *, + _inverted=False, + _type_name='attribute', + _type_element_query=query.attribute, + # we could eliminate this param, + # because collection query can be got from element query + # but let's keep the impl a bit simpler for now + _type_collection_query=query.attributes, + ): self.__expected = name # self.__ignore_case = _ignore_case self.__inverted = _inverted + self.__type_name = _type_name + self.__type_element_query = _type_element_query + self.__type_collection_query = _type_collection_query super().__init__( - f"has attribute '{name}'", - actual=query.attribute(name), + f"has {_type_name} '{name}'", + actual=_type_element_query(name), by=predicate.is_truthy, # todo: should it be more like .is_not_none? _inverted=_inverted, ) def value(self, expected): return _EntityHasSomethingSupportingIgnoreCase( - f"has attribute '{self.__expected}' with value", + f"has {self.__type_name} '{self.__expected}' with value", expected=expected, - actual=query.attribute(self.__expected), + actual=self.__type_element_query(self.__expected), by=predicate.equals, _inverted=self.__inverted, ) def value_containing(self, expected): return _EntityHasSomethingSupportingIgnoreCase( - f"has attribute '{self.__expected}' with value containing", + f"has {self.__type_name} '{self.__expected}' with value containing", expected=expected, - actual=query.attribute(self.__expected), + actual=self.__type_element_query(self.__expected), by=predicate.includes, _inverted=self.__inverted, ) def values(self, *expected: str | int | float | Iterable[str]): return _CollectionHasSomeThingsSupportingIgnoreCase( - f"has attribute '{self.__expected}' with values", + f"has {self.__type_name} '{self.__expected}' with values", *expected, - actual=query.attributes(self.__expected), + actual=self.__type_collection_query(self.__expected), by=predicate.str_equals_to_list, _inverted=self.__inverted, ) def values_containing(self, *expected: str | int | float | Iterable[str]): return _CollectionHasSomeThingsSupportingIgnoreCase( - f"has attribute '{self.__expected}' with values containing", + f"has {self.__type_name} '{self.__expected}' with values containing", *expected, - actual=query.attributes(self.__expected), + actual=self.__type_collection_query(self.__expected), by=predicate.str_equals_by_contains_to_list, _inverted=self.__inverted, ) +def attribute(name: str, _inverted=False): + return _ElementDescriptor( + name, + _inverted=_inverted, + _type_name='attribute', + _type_element_query=query.attribute, + _type_collection_query=query.attributes, + ) + + +def native_property(name: str, _inverted=False): + return _ElementDescriptor( + name, + _inverted=_inverted, + _type_name='native property', + _type_element_query=query.native_property, + _type_collection_query=query.native_properties, + ) + + +def css_property(name: str, _inverted=False): + return _ElementDescriptor( + name, + _inverted=_inverted, + _type_name='css property', + _type_element_query=query.css_property, + _type_collection_query=query.css_properties, + ) + + def value(expected: str | int | float, _inverted=False): - return attribute('value', _inverted).value(expected) + return attribute('value', _inverted=_inverted).value(expected) def value_containing(expected: str | int | float, _inverted=False): - return attribute('value', _inverted).value_containing(expected) + return attribute('value', _inverted=_inverted).value_containing(expected) def values(*expected: str | int | float | Iterable[str], _inverted=False): - return attribute('value', _inverted).values(*expected) + return attribute('value', _inverted=_inverted).values(*expected) def values_containing(*expected: str | int | float | Iterable[str], _inverted=False): - return attribute('value', _inverted).values_containing(*expected) + return attribute('value', _inverted=_inverted).values_containing(*expected) def css_class(name: str, _inverted=False): @@ -854,6 +794,7 @@ def __init__( self.__by = _by self.__inverted = _inverted + # todo: should we raise AttributeError if dict as expected is passed to Collection? super().__init__( self.__name, actual=lambda entity: ( diff --git a/selene/core/query.py b/selene/core/query.py index 0e10302f..b06db929 100644 --- a/selene/core/query.py +++ b/selene/core/query.py @@ -354,6 +354,20 @@ def fn(element: Element) -> str: return Query(f'css property {name}', fn) +def css_properties(name: str) -> Query[Collection, List[str]]: + """A query that gets the values of the css properties of all elements + in the collection + + Args: + name (str): name of the attribute + """ + + def fn(collection: Collection): + return [element.value_of_css_property(name) for element in collection.locate()] + + return Query(f'{name} css properties', fn) + + def native_property( name: str, ) -> Query[Element, Union[str, bool, WebElement, dict]]: @@ -363,6 +377,20 @@ def func(element: Element) -> Union[str, bool, WebElement, dict]: return Query(f'native property {name}', func) +def native_properties(name: str) -> Query[Collection, List[str]]: + """A query that gets the values of the native properties of all elements + in the collection + + Args: + name (str): name of the attribute + """ + + def fn(collection: Collection): + return [element.get_property(name) for element in collection.locate()] + + return Query(f'{name} native properties', fn) + + def js_property( name: str, ) -> Query[Element, Union[str, bool, WebElement, dict]]: diff --git a/selene/support/conditions/have.py b/selene/support/conditions/have.py index cfcf13ab..8d7e4275 100644 --- a/selene/support/conditions/have.py +++ b/selene/support/conditions/have.py @@ -78,9 +78,9 @@ def css_property(name: str, value: Optional[str] = None): 'passing second argument is deprecated; use have.css_property(foo).value(bar) instead', DeprecationWarning, ) - return match.element_has_css_property(name).value(value) + return match.css_property(name).value(value) - return match.element_has_css_property(name) + return match.css_property(name) def attribute(name: str, value: Optional[str] = None): diff --git a/selene/support/conditions/not_.py b/selene/support/conditions/not_.py index 210e6069..9fa1b6df 100644 --- a/selene/support/conditions/not_.py +++ b/selene/support/conditions/not_.py @@ -113,31 +113,7 @@ def property_(name: str, *args, **kwargs): .not_ ) - original = _match.native_property(name) - negated = original.not_ - - def value(self, expected: str | int | float) -> Condition[Element]: - return original.value(expected).not_ - - def value_containing(self, expected: str | int | float) -> Condition[Element]: - return original.value_containing(expected).not_ - - def values( - self, *expected: str | int | float | Iterable[str] - ) -> Condition[Collection]: - return original.values(*expected).not_ - - def values_containing( - self, *expected: str | int | float | Iterable[str] - ) -> Condition[Collection]: - return original.values_containing(*expected).not_ - - negated.value = value - negated.value_containing = value_containing - negated.values = values - negated.values_containing = values_containing - - return negated + return _match.native_property(name, _inverted=True) def css_property(name: str, *args, **kwargs): @@ -148,39 +124,17 @@ def css_property(name: str, *args, **kwargs): DeprecationWarning, ) return ( - _match.element_has_css_property(name) - .value(args[0] if args else kwargs['value']) - .not_ + _match.css_property(name).value(args[0] if args else kwargs['value']).not_ ) - original = _match.element_has_css_property(name) - negated = original.not_ - - def value(self, expected: str) -> Condition[Element]: - return original.value(expected).not_ - - def value_containing(self, expected: str) -> Condition[Element]: - return original.value_containing(expected).not_ - - def values(self, *expected: str) -> Condition[Collection]: - return original.values(*expected).not_ - - def values_containing(self, *expected: str) -> Condition[Collection]: - return original.values_containing(*expected).not_ - - negated.value = value - negated.value_containing = value_containing - negated.values = values - negated.values_containing = values_containing - - return negated + return _match.css_property(name, _inverted=True) def value(text: str | int | float): return _match.value(text, _inverted=True) -def value_containing(partial_text: str | int | float) -> Condition[Element]: +def value_containing(partial_text: str | int | float): return _match.value_containing(partial_text, _inverted=True) @@ -199,13 +153,11 @@ def tag_containing(name: str) -> Condition[Element]: # *** SeleneCollection conditions *** -def values(*texts: str | int | float | Iterable[str]) -> Condition[Collection]: +def values(*texts: str | int | float | Iterable[str]): return _match.values(*texts, _inverted=True) -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, _inverted=True) @@ -303,14 +255,18 @@ def tabs_number_greater_than_or_equal(value: int): def js_returned_true(script_to_return_bool: str) -> Condition[Browser]: warnings.warn( - 'might be deprecated; use have.js_returned(True, ...) instead', + 'deprecated; use have.script_returned(True, ...) instead', PendingDeprecationWarning, ) - return _match.browser_has_js_returned(True, script_to_return_bool).not_ + return _match.script_returned(True, script_to_return_bool).not_ def js_returned(expected: Any, script: str, *args) -> Condition[Browser]: - return _match.browser_has_js_returned(expected, script, *args).not_ + warnings.warn( + 'deprecated; use have.script_returned(...) instead', + PendingDeprecationWarning, + ) + return _match.script_returned(expected, script, *args).not_ def script_returned(expected: Any, script: str, *args) -> Condition[Browser]: diff --git a/tests/integration/condition__elements__have_property_and_co_test.py b/tests/integration/condition__elements__have_property_and_co_test.py index e8cf370b..441a7717 100644 --- a/tests/integration/condition__elements__have_property_and_co_test.py +++ b/tests/integration/condition__elements__have_property_and_co_test.py @@ -62,9 +62,9 @@ def test_have_property__condition_variations(session_browser): except AssertionError as error: assert ( "browser.all(('css selector', '.name')).has no (native property 'value' with " - "values containing '(20, 2)')\n" + "values containing [20, 2])\n" '\n' - "Reason: ConditionMismatch: actual property values: ['John 20th', 'Doe 2nd']\n" + "Reason: ConditionMismatch: actual value native properties: ['John 20th', 'Doe 2nd']\n" ) in str(error) exercises.first.should(have.property_('value').value(20)) diff --git a/tests/integration/condition__mixed_test.py b/tests/integration/condition__mixed_test.py index ed0b1ae1..ec4e2e8a 100644 --- a/tests/integration/condition__mixed_test.py +++ b/tests/integration/condition__mixed_test.py @@ -141,6 +141,29 @@ def test_should_match_different_things(session_browser): browser.should(have.title_containing('test').ignore_case) browser.with_(_ignore_case=True).should(have.title_containing('test')) + # have css_property? + s('li#visible').should(have.css_property('display').value('block')) + s('li#visible').should(have.css_property('display').value_containing('blo')) + s('li#hidden').should(have.css_property('display').value('none')) + s('li#hidden').should(have.css_property('display').value_containing('no')) + s('li#hidden').should(have.no.css_property('display').value_containing('NO')) + s('li#hidden').should( + have.css_property('display').value_containing('NO').ignore_case + ) + try: + s('li#hidden').should(have.css_property('display').value_containing('NO')) + pytest.fail('expect mismatch') + except AssertionError as error: + assert ( + "browser.element(('css selector', 'li#hidden')).has css property 'display' " + "with value containing 'NO'\n" + '\n' + 'Reason: ConditionMismatch: actual css property display: none\n' + ) in str(error) + ss('li[style]').should( + have.css_property('display').values('none', 'none', 'block', 'block') + ) + # have script returned? browser.should( have.script_returned(42, 'return arguments[0] * arguments[1]', 21, 2)