Skip to content

Commit

Permalink
[#530] NEW: support ignore_case as config option
Browse files Browse the repository at this point in the history
[#537] TEST: cover value like conditions with ignore_case tests
  • Loading branch information
yashaka committed Jul 14, 2024
1 parent 9a32470 commit 615111a
Show file tree
Hide file tree
Showing 16 changed files with 767 additions and 75 deletions.
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ 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

### TODO: should we make ignorecase optionally default for all conditions supporting it?
### TODO: Rename condition__collection__get*_test.py tests

### TODO: is text + exact_text naming still relevant for match.* case over have.*?

### Deprecated conditions

Expand Down Expand Up @@ -440,7 +442,17 @@ browser.all('li').first.should(have.text_matching(r'.*one.*').where_flags(re.IGN

```

Same for attribute related conditions like `have.value` and `have.attribute(name).value*` will be a part of [#537](https://github.com/yashaka/selene/issues/537)
Same can be achieved via `_ignore_case` config option (yet marked as experimental):

```python
from selene import browser, have
...
browser.all('li').first.with_(_ignore_case=True).should(have.exact_text('1) one!!!'))

# and so on for other conditions that support ignore_case as property...
...

```

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

Expand Down
21 changes: 18 additions & 3 deletions selene/common/_typing_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def _name_for(self, entity: E) -> str:
return self._name(entity) if callable(self._name) else self._name

@staticmethod
def full_name_for(
def _full_name_for(
callable_: Optional[Callable],
_entity: E | None = None,
) -> str | None:
Expand Down Expand Up @@ -116,11 +116,11 @@ def full_name_for(

# todo: would not human_readable_name_for be a better name for this helper?
@staticmethod
def full_description_for(
def _full_description_for(
callable_: Optional[Callable],
_entity: E | None = None,
) -> str | None:
full_name = Query.full_name_for(callable_, _entity)
full_name = Query._full_name_for(callable_, _entity)
return (
thread_last(
full_name,
Expand All @@ -135,6 +135,21 @@ def full_description_for(
else None
)

@staticmethod
def _full_description_or(
alternative: str,
/,
*,
for_: Optional[Callable],
_with_prefix: str = '',
_entity: E | None = None,
) -> str:
return (
_with_prefix + desc
if (desc := Query._full_description_for(for_, _entity))
else alternative
)

@staticmethod
@overload
def _inverted(
Expand Down
1 change: 1 addition & 0 deletions selene/core/_browser.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class Browser(WaitingEntity['Browser']):
wait_for_no_overlap_found_by_js: bool = False,
_match_only_visible_elements_texts: bool = True,
_match_only_visible_elements_size: bool = False,
_ignore_case: bool = False,
# Etc.
_build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ...,
) -> Browser: ...
Expand Down
27 changes: 20 additions & 7 deletions selene/core/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,11 +1016,11 @@ def __str__(self):
# - condition.repr_for(entity) # + correlate with __repr__ over __str__; + concise; - weird shortcut
# - condition.for(entity) # - looks like a builder
# # to remember entity so condition can be .__call__() later without passing entity
def _name_for(self, _entity: E | None = None):
def _name_for(self, entity: E | None = None) -> str:
return (
self.__describe(_entity)
self.__describe(entity)
if not self.__inverted
else self.__describe_inverted(_entity)
else self.__describe_inverted(entity)
)

# todo: we already have entity.matching for Callable[[E], bool]
Expand Down Expand Up @@ -1064,6 +1064,19 @@ def _name_for(self, _entity: E | None = None):
# > browser.element('input').clear()
# > expect.blank(browser.element('input')) #->❤️
# TODO: at least, we have to document – the #->❤️-marked recipes...
# hm... but what if condition has param:
# > expect.value('foo')(browser.element('input'))
# – now it looks less natural
# let's compare other options:
# > expect.value('foo').test(browser.element('input'))
# > expect.value('foo').match(browser.element('input'))
# > assert_.value('foo').test(browser.element('input'))
# > assert_.value('foo').match(browser.element('input'))
# > match.value('foo').match(browser.element('input'))
# > match.value('foo').test(browser.element('input'))
# what if we add on property returning just self?
# or as an method alias to __call__()
# > match.value('foo').on(browser.element('input'))
def _test(self, entity: E) -> None:
# currently refactored to be alias to __call__ to be in more compliance
# with some subclasses implementations, that override __call__
Expand Down Expand Up @@ -1499,14 +1512,14 @@ def __init__x(self, *args, **kwargs):
else:
actual = None
by = args[0]
if not (by_name := Query.full_description_for(by)):
if not (by_name := Query._full_description_for(by)):
raise ValueError(
'either provide name or ensure that at least by predicate'
'has __qualname__ (defined as regular named function)'
'or custom __str__ implementation '
'(like lambda wrapped in Query object)'
)
actual_desc = Query.full_description_for(actual)
actual_desc = Query._full_description_for(actual)
name = ((str(actual_desc) + ' ') if actual_desc else '') + str(
by_name
) # noqa
Expand Down Expand Up @@ -1586,14 +1599,14 @@ def __init__(
But keep in mind that they are marked with `_` prefix to indicate their
private and potentially "experimental" use, that can change in future versions.
"""
if not name and not (by_name := Query.full_description_for(by)):
if not name and not (by_name := Query._full_description_for(by)):
raise ValueError(
'either provide a name or ensure that at least by predicate '
'has __qualname__ (defined as regular named function) '
'or custom __str__ implementation '
'(like lambda wrapped in Query object)'
)
actual_name = Query.full_description_for(actual)
actual_name = Query._full_description_for(actual)
name = name or (
((str(actual_name) + ' ') if actual_name else '') + str(by_name) # noqa
)
Expand Down
8 changes: 8 additions & 0 deletions selene/core/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,14 @@ def _executor(self):
It is set to False by default for backward compatibility reasons.
""" # todo: document example
# todo: decide on naming: ignore_case vs match_ignoring_case?
# ignore_case conciser but
# - not consistent with other match_* options
# - does not explicitly tell that relates only to matching
# * but... it should be kind of obvious that can be used only in context of matching
# * and actually it's consistent with have.*.ignore_case conditions
# todo: consider documenting the major list of conditions that are affected by this option
_ignore_case: bool = False

# TODO: better name? now technically it's not a decorator but decorator_builder...
# or decorator_factory...
Expand Down
3 changes: 3 additions & 0 deletions selene/core/configuration.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class Config:
wait_for_no_overlap_found_by_js: bool = False
_match_only_visible_elements_texts: bool = True
_match_only_visible_elements_size: bool = False
_ignore_case: bool = False
# Etc.
_build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ...
_executor: _DriverStrategiesExecutor = ...
Expand Down Expand Up @@ -156,6 +157,7 @@ class Config:
wait_for_no_overlap_found_by_js: bool = False,
_match_only_visible_elements_texts: bool = True,
_match_only_visible_elements_size: bool = False,
_ignore_case: bool = False,
# Etc.
_build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ...,
): ...
Expand Down Expand Up @@ -211,6 +213,7 @@ class Config:
wait_for_no_overlap_found_by_js: bool = False,
_match_only_visible_elements_texts: bool = True,
_match_only_visible_elements_size: bool = False,
_ignore_case: bool = False,
# Etc.
_build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ...,
): ...
2 changes: 2 additions & 0 deletions selene/core/entity.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class Element(WaitingEntity['Element']):
wait_for_no_overlap_found_by_js: bool = False,
_match_only_visible_elements_texts: bool = True,
_match_only_visible_elements_size: bool = False,
_ignore_case: bool = False,
# Etc.
_build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ...,
) -> Element: ...
Expand Down Expand Up @@ -151,6 +152,7 @@ class Collection(WaitingEntity['Collection'], Iterable[Element]):
wait_for_no_overlap_found_by_js: bool = False,
_match_only_visible_elements_texts: bool = True,
_match_only_visible_elements_size: bool = False,
_ignore_case: bool = False,
# Etc.
_build_wait_strategy: Callable[[Config], Callable[[E], Wait[E]]] = ...,
) -> Collection: ...
Expand Down
9 changes: 3 additions & 6 deletions selene/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,8 @@ def wrapped(entity: E) -> None:
def describe_not_match(actual_value):
describe_actual_result = _describe_actual_result or (
lambda value: (
'actual'
+ (
f' {actual_name}'
if (actual_name := Query.full_description_for(actual))
else ''
Query._full_description_or(
'actual', for_=actual, _with_prefix='actual '
)
+ f': {value}'
)
Expand All @@ -169,7 +166,7 @@ def describe_not_match(actual_value):
(
(
(f'not ({name})' if _inverted else name)
if (name := Query.full_description_for(by))
if (name := Query._full_description_for(by))
else ''
)
or "condition"
Expand Down
Loading

0 comments on commit 615111a

Please sign in to comment.