Skip to content

Commit

Permalink
[#528] NEW: list globs, text wildcards + regex in texts_like conditions
Browse files Browse the repository at this point in the history
+ texts like conditions now accepts int and floats as text item with a TODO: implement same for other text conditions
+ some commented not needed anymore code was left to temporary log in commit some decisions made
  • Loading branch information
yashaka committed May 23, 2024
1 parent 9d74f00 commit a7da02d
Show file tree
Hide file tree
Showing 14 changed files with 2,425 additions and 39 deletions.
104 changes: 103 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,109 @@ TODOs:
- can we force order of how `selene.*` is rendered on autocomplete? via `__all__`...
- deprecate `have.js_returned` in favour of `have.script_returned`

## 2.0.0rc10 (to be released on DD.05.2024)
## 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

`.have.exact_texts(1, 2.0, '3')` is now possible, and will be treated as `['1', '2.0', '3']`

### list globs, text wildcards and regex support in texts_like conditions

List of conditions added (still marked as experimental with `_` prefix):

- `have._exact_texts_like(*exact_texts_or_list_globs: Union[str, int, float])`
- `have._exact_texts_like(*exact_texts_or_list_globs: Union[str, int, float]).where(**globs_to_override)`
- `have._texts_like(*contained_texts_or_list_globs: Union[str, int, float])`
- `have._texts_like(*contained_texts_or_list_globs: Union[str, int, float]).where(**glob_to_override)`
- `have._texts_like(*regex_patterns_or_list_globs: Union[str, int, float]).with_regex`
- is an alias to `have._text_patterns_like`
- `have._text_patterns(*regex_patterns).with_regex`
- like `have.texts` but with regex patterns as expected, i.e. no list globs support
- `have._texts_like(*texts_with_wildcards_or_list_globs: Union[str, int, float]).with_wildcards`
- `have._texts_like(*texts_with_wildcards_or_list_globs: Union[str, int, float]).where_wildcards(**to_override)`
- corresponding `have.no.*` versions of same conditions

Where:

- default list globs are:
- `[...]` matches **zero or one** item of any text in the list
- `...` matches **exactly one** item of any text in the list
- `(...,)` matches one **or more** items of any text in the list
- `[(...,)]` matches **zero** or more items of any text in the list
- all globs can be mixed in the same list of expected items in any order
- regex patterns can't use `^` (start of text) and `$` (end of text)
because they are implicit, and if added explicitly will break the match
- supported wildcards can be overridden and defaults are:
- `*` matches **zero or more** of any characters in a text item
- `?` matches **exactly one** of any character in a text item

Warning:

- Actual implementation does not compare each list item separately, it merges all expected items into one regex pattern and matches it with merged text of all visible elements collection texts, and so it may be tricky to analyze the error message in case of failure. To keep life simpler, try to reduce the usage of such conditions to the simplest cases, preferring wildcards to regex patterns, trying even to avoid wildcards if possible, in the perfect end, sticking just to `exact_texts_like` or `texts_like` conditions with only one explicitly (for readability) customized list glob, choosing `...` as the simplest glob placeholder, for example: `browser.all('li').should(have._exact_texts_like(1, 2, 'Three', ...).where(one_or_more=...))` to assert actual texts `<li>1</li><li>2</li><li>Three</li><li>4</li><li>5</li>` in the list.

Examples of usage:

```python
from selene import browser, have
...
# GivenPage(browser.driver).opened_with_body(
# '''
# <ul>Hello:
# <li>1) One!!!</li>
# <li>2) Two!!!</li>
# <li>3) Three!!!</li>
# <li>4) Four!!!</li>
# <li>5) Five!!!</li>
# </ul>
# '''
# )

browser.all('li').should(have._exact_texts_like(
'1) One!!!', '2) Two!!!', ..., ..., ... # = exactly one
))
browser.all('li').should(have._texts_like(
'\d\) One!+', '\d.*', ..., ..., ...
).with_regex)
browser.all('li').should(have._texts_like(
'?) One*', '?) Two*', ..., ..., ...
).with_wildcards)
browser.all('li').should(have._texts_like(
'_) One**', '_) Two*', ..., ..., ...
).where_wildcards(zero_or_more='**', exactly_one='_'))
browser.all('li').should(have._texts_like(
'One', 'Two', ..., ..., ... # matches each text by contains
)) # kind of "with implicit * wildcards" in the beginning and the end of each text


browser.all('li').should(have._texts_like(
..., ..., ..., 'Four', 'Five'
))
browser.all('li').should(have._texts_like(
'One', ..., ..., 'Four', 'Five'
))

browser.all('li').should(have._texts_like(
'One', 'Two', (..., ) # = one or more
))
browser.all('li').should(have._texts_like(
[(..., )], 'One', 'Two', [(..., )] # = ZERO or more ;)
))
browser.all('li').should(have._texts_like(
[...,], 'One', 'Two', 'Three', 'Four', [...] # = zero or ONE ;)
))

# If you don't need so much "globs"...
# (here goes, actually, the 💡RECOMMENDED💡 way to use it in most cases...
# to keep things simpler for easier support and more explicit for readability)
# – you can use the simplest glob item with explicitly customized meaning:
browser.all('li').should(have._exact_texts_like(
'One', 'Two', ... # = one OR MORE
).where(one_or_more=...)) # – because the ... meaning was overridden
# Same works for other conditions that end with `_like`
browser.all('li').should(have._exact_texts_like(
'1) One!!!', '2) Two!!!', ...
).where(one_or_more=...))
```

### Shadow DOM support via query.js.shadow_root(s)

Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Changelog = "https://github.com/yashaka/selene/releases"
python = "^3.8"
selenium = ">=4.12.0"
future = "*"
typing-extensions = ">=4.9.0"
typing-extensions = ">=4.11.0"

[tool.poetry.dev-dependencies]
black = "^24.2.0"
Expand Down
4 changes: 4 additions & 0 deletions selene/common/predicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ def is_less_than_or_equal(expected):
return lambda actual: actual <= expected


def matches(pattern):
return lambda actual: re.match(pattern, str(actual))


def includes_ignoring_case(expected):
return lambda actual: str(expected).lower() in str(actual).lower()

Expand Down
12 changes: 10 additions & 2 deletions selene/core/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,15 @@ def fn(entity: E) -> None:

return cls(description, fn)

def __init__(self, description: str, fn: Lambda[E, None]):
# TODO: should we make the description type as Callable[[Condition], str]
# instead of Callable[[], str]...
# to be able to pass condition itself...
# when we pass in child classes we pass self.__str__
# that doesn't need to receive self, it already has it
# but what if we want to pass some crazy lambda for description from outside
# to kind of providing a "description self-based strategy" for condition?
# maybe at least we can define it as varagrs? like Callable[..., str]
def __init__(self, description: str | Callable[[], str], fn: Lambda[E, None]):
self._description = description
self._fn = fn

Expand Down Expand Up @@ -177,7 +185,7 @@ def __str__(self):
# TODO: consider changing has to have on the fly for CollectionConditions
# TODO: or changing in collection locator rendering `all` to `collection`
# TODO: or changing in match.* names from collection_has_* to all_have_*
return self._description
return self._description() if callable(self._description) else self._description

def and_(self, condition: Condition[E]) -> Condition[E]:
return Condition.by_and(self, condition)
Expand Down
Loading

0 comments on commit a7da02d

Please sign in to comment.