Skip to content

Commit

Permalink
[#434] NEW: support int and float in text related conditions
Browse files Browse the repository at this point in the history
`.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
  • Loading branch information
yashaka committed May 25, 2024
1 parent 703df6b commit 73a96ae
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 66 deletions.
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 30 additions & 6 deletions selene/common/predicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)


Expand All @@ -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()


Expand All @@ -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
Expand All @@ -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)
)


Expand Down Expand Up @@ -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)
81 changes: 46 additions & 35 deletions selene/core/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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',
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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]:
Expand All @@ -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'
Expand All @@ -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_),
)


Expand Down Expand Up @@ -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)

Expand Down
14 changes: 7 additions & 7 deletions selene/support/conditions/have.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)


Expand Down
Loading

0 comments on commit 73a96ae

Please sign in to comment.