Skip to content

Commit

Permalink
[#505] REFACTOR: support outer context search in POM...
Browse files Browse the repository at this point in the history
... via descriptor.within(context)

+ add test example
  • Loading branch information
yashaka committed Jul 21, 2024
1 parent 1365a7a commit e5f1acb
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 24 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ check vscode pylance, mypy, jetbrains qodana...

(speaking about all: ConditionMismatch, Condition, Match...)

### TODO: stable Element descriptors

### TODO: consider removing experimantal mark from `ConditionMismatch._to_raise_if_not`

### TODO: consider regex support via .pattern prop (similar to .ignore_case) (#537)
Expand All @@ -160,7 +162,7 @@ check vscode pylance, mypy, jetbrains qodana...

### TODO: Location strategy?

### DOING: basic Element descriptors?
### DOING: draft Element descriptors POC?

### Deprecated conditions

Expand Down
72 changes: 54 additions & 18 deletions selene/support/_pom.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations

from functools import lru_cache

from typing_extensions import Tuple, cast

import selene
from selene.core.entity import Element, Collection


# TODO: should we built these descriptors into Element and Collection classes?
Expand All @@ -46,52 +48,85 @@
# > Inside + InsideAll
# > InnerElement
# > The
class _Element: # todo: consider implementing LocationContext interface
class Element: # todo: consider implementing LocationContext interface
def __init__(self, selector: str | Tuple[str, str], _context=None):
self.__selector = selector
self.__context = _context

def Element(self, selector: str | Tuple[str, str]) -> _Element:
return _Element(selector, _context=self)
def within(self, context, /):
return Element(self.__selector, _context=context)

def All(self, selector: str | Tuple[str, str]) -> _All:
return _All(selector, _context=self)
def Element(self, selector: str | Tuple[str, str]) -> Element:
return Element(selector, _context=self)

def All(self, selector: str | Tuple[str, str]) -> All:
return All(selector, _context=self)

# --- Descriptor --- #

def __set_name__(self, owner, name):
self.__name = name # TODO: use it

# TODO: consider caching
@lru_cache
def __get__(self, instance, owner):
self.__context = self.__context or getattr(instance, 'context', selene.browser)
self.__as_context = cast(Element, self.__context.element(self.__selector))
self.__context = self.__context or getattr(
instance,
'context',
getattr(
instance,
'browser',
selene.browser,
),
)

self.__as_context = cast(
selene.Element,
(
self.__context.element(self.__selector)
if isinstance(self.__context, (selene.Browser, selene.Element))
# self.__context is of type self.__class__ ;)
else self.__context._element(self.__selector)
),
)

return self.__as_context

# --- LocationContext --- #

def element(self, selector: str | Tuple[str, str]):
# currently protected from direct access on purpose to not missclick on it
# when actually the .Element or .All is needed
def _element(self, selector: str | Tuple[str, str]):
return self.__as_context.element(selector)

def all(self, selector: str | Tuple[str, str]) -> Collection:
return self.__as_context.all(selector)
# def _all(self, selector: str | Tuple[str, str]) -> selene.Collection:
# return self.__as_context.all(selector)


class _All:
class All:

def __init__(self, selector: str | Tuple[str, str], _context=None):
self.__selector = selector
self.__context = _context

def within(self, context, /):
return All(self.__selector, _context=context)

# --- Descriptor --- #

def __set_name__(self, owner, name):
self.__name = name # TODO: use it

# TODO: consider caching
def __get__(self, instance, owner) -> Element:
self.__context = self.__context or getattr(instance, 'context', selene.browser)
@lru_cache
def __get__(self, instance, owner) -> selene.Element:
self.__context = self.__context or getattr(
instance,
'context',
getattr(
instance,
'browser',
selene.browser,
),
)
self.__as_context = self.__context.all(self.__selector)

return self.__as_context
Expand All @@ -101,5 +136,6 @@ def __get__(self, instance, owner) -> Element:
# TODO: implement...


S = _Element
SS = _All
# todo: consider aliases...
# S = _Element
# SS = _All
Empty file added tests/examples/pom/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions tests/examples/pom/test_material_ui__react_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import pytest

from selene import browser, have
from selene.support._pom import Element, All


class MUIBasicSelect:
label = Element('label')
selected_text = Element('.MuiSelect-select')
input = Element('input')
items = All('[role=option]').within(browser)

def __init__(self, context):
self.context = context

@staticmethod
def by_id(value):
return MUIBasicSelect(
browser.element(f'#{value}').element(
'./ancestor::*[contains(concat(" ", normalize-space(@class), " "), " '
'MuiFormControl-root'
' ")]'
)
)

def open(self):
self.context.click()
return self

def choose(self, text):
self.items.element_by(have.exact_text(text)).click()
return self

def select(self, text):
self.open().choose(text)


@pytest.mark.parametrize(
'age',
[
MUIBasicSelect(browser.element('#BasicSelect+* .MuiFormControl-root')),
MUIBasicSelect.by_id('demo-simple-select'),
],
)
def test_material_ui__react_select__basic_select(age):
browser.driver.refresh()

# WHEN
browser.open('https://mui.com/material-ui/react-select/#basic-select')

# THEN
age.label.should(have.exact_text('Age'))
age.selected_text.should(have.exact_text(''))
age.input.should(have.value(''))

# WHEN
age.select('Twenty')

# THEN
age.selected_text.should(have.exact_text('Twenty'))
age.input.should(have.value('20'))
10 changes: 5 additions & 5 deletions tests/integration/element__perform__drag_and_drop_to_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import selene
from selene import command, be, have, query
from selene.support._pom import _Element
from selene.support._pom import Element
from tests.integration.helpers.givenpage import GivenPage


Expand Down Expand Up @@ -171,11 +171,11 @@ def open(self):

# Example of the POM-like PageObject pattern
class ReactContinuousSlider:
thumb = _Element('.MuiSlider-thumb')
thumb = Element('.MuiSlider-thumb')
thumb_input = thumb.Element('input')
volume_up = _Element('[data-testid=VolumeUpIcon]')
volume_down = _Element('[data-testid=VolumeDownIcon]')
rail = _Element('.MuiSlider-rail')
volume_up = Element('[data-testid=VolumeUpIcon]')
volume_down = Element('[data-testid=VolumeDownIcon]')
rail = Element('.MuiSlider-rail')

def __init__(self, browser: Optional[selene.Browser]):
self.browser = browser or selene.browser
Expand Down

0 comments on commit e5f1acb

Please sign in to comment.