diff --git a/CHANGELOG.md b/CHANGELOG.md index e41b6c47..8954ab2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -156,6 +156,9 @@ especially relevant for have.texts to turn on/off ignoring invisible elements - `be.not_.present` in favor of `be.not_.present_in_dom` - `be.absent` in favor of `be.absent_in_dom` - `be.not_.absent` in favor of `be.not_.absent_in_dom` +- `have.no.js_property` in favor of `have.no.property_` +- `have.js_property` in favor of `have.property_` or `match.native_property` + - same for `query.js_property` in favor of `query.native_property` ### Added be.hidden_in_dom in addition to be.hidden diff --git a/docs/faq/iframes-howto.md b/docs/faq/iframes-howto.md index ea2c7323..d5459e10 100644 --- a/docs/faq/iframes-howto.md +++ b/docs/faq/iframes-howto.md @@ -7,8 +7,6 @@ You allways can work with iframes same way [as you do in pure Selenium](https:// ```python from selene import browser, command, have - - # GIVEN iframe = browser.element('#editor-iframe') # WHEN @@ -27,7 +25,7 @@ browser.driver.switch_to.frame(iframe_webelement) # AND ... browser.all('strong').should(have.size(1)) browser.element('.textarea').should( - have.js_property('innerHTML').value( + have.property_('innerHTML').value( '
Hello, world!
' ) ) diff --git a/selene/core/match.py b/selene/core/match.py index fd4f136b..22aa3738 100644 --- a/selene/core/match.py +++ b/selene/core/match.py @@ -240,11 +240,6 @@ 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 ""}' @@ -258,10 +253,12 @@ def __init__(self, expected: str, _flags=0, _inverted=False): def ignore_case(self): return self.where_flags(re.IGNORECASE) - # TODO: should we shorten name just to flags? i.e. + # todo: should we shorten name just to flags? (or add alias) i.e. # `.should(have.text_matching(r'.*one.*').flags(re.IGNORECASE))` # over # `.should(have.text_matching(r'.*one.*').where_flags(re.IGNORECASE))` + # currently it's named with where_ prefix for consistency with + # texts_like & co conditions def where_flags(self, flags: re.RegexFlag, /) -> Condition[Element]: return self.__class__( self.__expected, @@ -270,11 +267,7 @@ def where_flags(self, flags: re.RegexFlag, /) -> 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 native_property(name: str): def property_value(element: Element): return element.locate().get_property(name) @@ -282,20 +275,20 @@ def property_values(collection: Collection): return [element.get_property(name) for element in collection()] raw_property_condition = ElementCondition.raise_if_not_actual( - 'has js property ' + name, property_value, predicate.is_truthy + 'has native property ' + name, property_value, predicate.is_truthy ) - class ConditionWithValues(ElementCondition): + class PropertyWithValues(ElementCondition): def value(self, expected: str | int | float) -> Condition[Element]: return ElementCondition.raise_if_not_actual( - f"has js property '{name}' with value '{expected}'", + 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 js property '{name}' with value containing '{expected}'", + f"has native property '{name}' with value containing '{expected}'", property_value, predicate.str_includes(expected), ) @@ -306,7 +299,7 @@ def values( expected_ = helpers.flatten(expected) return CollectionCondition.raise_if_not_actual( - f"has js property '{name}' with values '{expected_}'", + f"has native property '{name}' with values '{expected_}'", property_values, predicate.str_equals_to_list(expected_), ) @@ -317,12 +310,12 @@ def values_containing( expected_ = helpers.flatten(expected) return CollectionCondition.raise_if_not_actual( - f"has js property '{name}' with values containing '{expected_}'", + f"has native property '{name}' with values containing '{expected_}'", property_values, predicate.str_equals_by_contains_to_list(expected_), ) - return ConditionWithValues( + return PropertyWithValues( str(raw_property_condition), test=raw_property_condition.__call__ ) diff --git a/selene/core/query.py b/selene/core/query.py index 6a6ae7a3..0ecb5075 100644 --- a/selene/core/query.py +++ b/selene/core/query.py @@ -188,6 +188,7 @@ def fn(element: Element): import functools import typing +import warnings from typing_extensions import ( List, @@ -343,13 +344,20 @@ def fn(element: Element) -> str: return Query(f'css property {name}', fn) -def js_property( +def native_property( name: str, ) -> Query[Element, Union[str, bool, WebElement, dict]]: def func(element: Element) -> Union[str, bool, WebElement, dict]: return element.locate().get_property(name) - return Query(f'js property {name}', func) + return Query(f'native property {name}', func) + + +def js_property( + name: str, +) -> Query[Element, Union[str, bool, WebElement, dict]]: + warnings.warn('deprecated: use query.native_property instead', DeprecationWarning) + return native_property(name) # --- Pseudo-queries --- # diff --git a/selene/support/conditions/have.py b/selene/support/conditions/have.py index e7a3eb31..a37ccd12 100644 --- a/selene/support/conditions/have.py +++ b/selene/support/conditions/have.py @@ -44,16 +44,24 @@ def text_matching(regex_pattern: str): return match.text_pattern(regex_pattern) -# TODO: should we use here js.property style (and below for js.returned(...)) +# TODO: should we rename it to native_property or simply prop? +# as we did for match.native_property and query.native_property? +def property_(name: str): # named with _ suffix for no conflicts with built in + return match.native_property(name) + + def js_property(name: str, value: Optional[str] = None): + warnings.warn( + 'have.js_property is deprecated; use have.property instead', DeprecationWarning + ) if value: warnings.warn( - 'passing second argument is deprecated; use have.js_property(foo).value(bar) instead', + 'passing second argument is deprecated; use have.property(foo).value(bar) instead', DeprecationWarning, ) - return match.element_has_js_property(name).value(value) + return match.native_property(name).value(value) - return match.element_has_js_property(name) + return match.native_property(name) def css_property(name: str, value: Optional[str] = None): diff --git a/selene/support/conditions/not_.py b/selene/support/conditions/not_.py index 9cf7aef5..c51300f9 100644 --- a/selene/support/conditions/not_.py +++ b/selene/support/conditions/not_.py @@ -122,19 +122,24 @@ def values_containing(self, *expected: str | int | float) -> Condition[Collectio def js_property(name: str, *args, **kwargs): + warnings.warn('deprecated; use have.no.property instead', DeprecationWarning) + return property_(name, *args, **kwargs) + + +def property_(name: str, *args, **kwargs): if args or 'value' in kwargs: warnings.warn( 'passing second argument is deprecated; ' - 'use have.js_property(foo).value(bar) instead', + 'use have.property(foo).value(bar) instead', DeprecationWarning, ) return ( - _match.element_has_js_property(name) + _match.native_property(name) .value(args[0] if args else kwargs['value']) .not_ ) - original = _match.element_has_js_property(name) + original = _match.native_property(name) negated = original.not_ def value(self, expected: str | int | float) -> Condition[Element]: 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 cad24133..55a49a5e 100644 --- a/tests/integration/condition__elements__have_property_and_co_test.py +++ b/tests/integration/condition__elements__have_property_and_co_test.py @@ -51,21 +51,21 @@ def test_have_property__condition_variations(session_browser): exercises.first.type('20') exercises.second.type('30') - # the following passes too, cause js prop exists, are we ok with that? + # todo: 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)) + exercises.should(have.property_('value').values(20, 30)) + names.should(have.property_('value').values_containing(20, 2)) try: - names.should(have.js_property('value').values_containing(20, 2).not_) + names.should(have.property_('value').values_containing(20, 2).not_) pytest.fail('should fail on values mismatch') except AssertionError as error: assert ( - "browser.all(('css selector', '.name')).has no (js property 'value' with " + "browser.all(('css selector', '.name')).has no (property 'value' with " "values containing '(20, 2)')\n" '\n' "Reason: ConditionMismatch: actual property values: ['John 20th', 'Doe 2nd']\n" ) in str(error) - exercises.first.should(have.js_property('value').value(20)) - exercises.first.should(have.js_property('value').value_containing(2)) + exercises.first.should(have.property_('value').value(20)) + exercises.first.should(have.property_('value').value_containing(2)) diff --git a/tests/integration/element__get__query__frame_context__decorator_test.py b/tests/integration/element__get__query__frame_context__decorator_test.py index 0b33cafa..703d9f66 100644 --- a/tests/integration/element__get__query__frame_context__decorator_test.py +++ b/tests/integration/element__get__query__frame_context__decorator_test.py @@ -43,7 +43,7 @@ def set_bold(self): @text_area_frame._within def should_have_text_html(self, text_html): - self.text_area.should(have.js_property('innerHTML').value(text_html)) + self.text_area.should(have.property_('innerHTML').value(text_html)) return self @text_area_frame._within 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 cdf10b58..79f28aeb 100644 --- a/tests/integration/element__get__query__frame_context__element_test.py +++ b/tests/integration/element__get__query__frame_context__element_test.py @@ -95,7 +95,7 @@ def test_actions_on_frame_element_with_logging(session_browser): # THEN everything inside frame context text_area.element('p').should( - have.js_property('innerHTML').value( + have.property_('innerHTML').value( 'Hello, World!', ) ).perform(command.select_all) @@ -105,13 +105,13 @@ def test_actions_on_frame_element_with_logging(session_browser): # AND coming back inside frame context text_area.element('p').should( - have.js_property('innerHTML').value('Hello, World!') + have.property_('innerHTML').value('Hello, World!') ) text_area.perform(command.select_all).type( 'New content', ).element('p').should( - have.js_property('innerHTML').value( + have.property_('innerHTML').value( 'New content', ) ) @@ -124,9 +124,9 @@ def test_actions_on_frame_element_with_logging(session_browser): # THEN everything is logged: assert ( "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): should " - "have js property 'innerHTML' with value 'Hello, World!': STARTED\n" + "have native property 'innerHTML' with value 'Hello, World!': STARTED\n" "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): should " - "have js property 'innerHTML' with value 'Hello, World!': PASSED\n" + "have native property 'innerHTML' with value 'Hello, World!': PASSED\n" "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): send " '«select all» keys shortcut as ctrl+a or cmd+a for mac: STARTED\n' "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): send " @@ -134,9 +134,9 @@ def test_actions_on_frame_element_with_logging(session_browser): "element('.tox-toolbar__primary').element('[aria-label=Bold]'): click: STARTED\n" "element('.tox-toolbar__primary').element('[aria-label=Bold]'): click: PASSED\n" "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): should " - "have js property 'innerHTML' with value 'Hello, World!': STARTED\n" + "have native property 'innerHTML' with value 'Hello, World!': STARTED\n" "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): should " - "have js property 'innerHTML' with value 'Hello, World!': PASSED\n" + "have native property 'innerHTML' with value 'Hello, World!': PASSED\n" "element('.tox-edit-area__iframe'): element('#tinymce'): send «select all» " 'keys shortcut as ctrl+a or cmd+a for mac: STARTED\n' "element('.tox-edit-area__iframe'): element('#tinymce'): send «select all» " @@ -146,10 +146,10 @@ def test_actions_on_frame_element_with_logging(session_browser): "element('.tox-edit-area__iframe'): element('#tinymce'): type: New content: " 'PASSED\n' "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): should " - "have js property 'innerHTML' with value 'New content': " + "have native property 'innerHTML' with value 'New content': " 'STARTED\n' "element('.tox-edit-area__iframe'): element('#tinymce').element('p'): should " - "have js property 'innerHTML' with value 'New content': " + "have native property 'innerHTML' with value 'New content': " 'PASSED\n' "element('.tox-edit-area__iframe'): element('#tinymce').all('p'): should have " 'size 10: STARTED\n' 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 0631ae07..7f503137 100644 --- a/tests/integration/element__get__query__frame_context__with_test.py +++ b/tests/integration/element__get__query__frame_context__with_test.py @@ -42,7 +42,7 @@ def test_actions_within_frame_context(session_browser): # THEN text_area.element('p').should( - have.js_property('innerHTML').value( + have.property_('innerHTML').value( 'Hello, World!', ) ) @@ -60,7 +60,7 @@ def test_actions_within_frame_context(session_browser): # THEN text_area.element('p').should( - have.js_property('innerHTML').value('Hello, World!') + have.property_('innerHTML').value('Hello, World!') ) # WHEN (just one more example) @@ -70,7 +70,7 @@ def test_actions_within_frame_context(session_browser): # THEN text_area.element('p').should( - have.js_property('innerHTML').value( + have.property_('innerHTML').value( 'New content', ) )