Skip to content

Commit

Permalink
[#505] REFACTOR: fix sub-sub-descriptors ...
Browse files Browse the repository at this point in the history
... where first from "sub-" ones has no name

+ TODO: should not we set attrs on instance, not lru_cache __get__ result?
  • Loading branch information
yashaka committed Jul 21, 2024
1 parent 216c009 commit 1e7f32a
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 56 deletions.
103 changes: 53 additions & 50 deletions selene/support/_pom.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
# > The
class Element: # todo: consider implementing LocationContext interface
def __init__(self, selector: str | Tuple[str, str], _context=None):
# todo: consider refactoring to protected over private attributes
# at least for easier debugging
# and easier dynamic checks of attributes presence
self.__selector = selector

# todo: should we wrap lambda below into lru_cache?
Expand All @@ -74,6 +77,17 @@ def __init__(self, selector: str | Tuple[str, str], _context=None):
)
)

def _as_context(self, instance) -> selene.Element:
return (
context_of_self.element(self.__selector)
if isinstance(
context_of_self := self.__context(instance),
(selene.Browser, selene.Element),
)
# => self.__context is a descriptor:
else context_of_self._as_context(instance).element(self.__selector)
)

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

Expand All @@ -99,63 +113,54 @@ def within_browser(self):
)

def element(self, selector: str | Tuple[str, str]) -> Element:
# return Element(selector, _context=self)
return Element(
selector,
_context=lambda instance: ( # todo: should we lru_cache it?
getattr(instance, self.__name), # ← resolving descriptors chain
self, # before returning the actual context :P
# otherwise any descriptor built on top of previously defined
# can be resolved improperly, because previous one
# might be not accessed yet, thus we have to simulate such assess
# on our own by forcing getattr
)[1],
(
getattr(instance, self.__name), # ← resolving descriptors chain –
self, # – before returning the actual context :P
# otherwise any descriptor built on top of previously defined
# can be resolved improperly, because previous one
# might be not accessed yet, thus we have to simulate such assess
# on our own by forcing getattr
)[1]
if hasattr(self, f'_{self.__class__.__name__}__name')
# => self if a "pass-through"-descriptor)
else self._as_context(instance)
),
)

def all(self, selector: str | Tuple[str, str]) -> All:
return All(
selector,
_context=lambda instance: (
getattr(instance, self.__name),
self,
)[1],
(
getattr(instance, self.__name),
self,
)[1]
if hasattr(self, f'_{self.__class__.__name__}__name')
else self._as_context(instance)
),
)

# --- Descriptor --- #

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

# TODO: should not we set attr on instance instead of lru_cache? :D
# set first time, then reuse :D
# current impl looks like cheating :D
@lru_cache
def __get__(self, instance, owner):
return self._as_context(instance)

actual_context = self.__context(instance)

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

return self.__as_context

# --- LocationContext --- #

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

def _selene_all(self, selector: str | Tuple[str, str]) -> selene.Collection:
return self.__as_context.all(selector)
# --- LocationContext ---
# prev impl. was completely wrong, cause store "as_context" snapshot on self
# but had to store it on instance...


class All:

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

Expand All @@ -180,6 +185,17 @@ def __init__(self, selector: str | Tuple[str, str], _context=None):
)
)

def _as_context(self, instance) -> selene.Collection:
return (
context_of_self.all(self.__selector)
if isinstance(
context_of_self := self.__context(instance),
(selene.Browser, selene.Element, selene.Collection),
)
# => self.__context is a descriptor:
else context_of_self._as_context(instance).all(self.__selector)
)

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

Expand Down Expand Up @@ -212,23 +228,10 @@ def __set_name__(self, owner, name):

@lru_cache
def __get__(self, instance, owner):
actual_context = self.__context(instance)

self.__as_context = cast(
selene.Collection,
(
actual_context.all(self.__selector)
if isinstance(actual_context, (selene.Browser, selene.Element))
# self.__context is of type self.__class__ ;)
else actual_context._selene_all(self.__selector)
),
)

return self.__as_context
return self._as_context(instance)

# --- FilteringContext --- #

# TODO: implement...
# todo: do we need it?


# todo: consider aliases...
Expand Down
13 changes: 7 additions & 6 deletions tests/examples/pom/test_material_ui__react_x_data_grid__mit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ class DataGridMIT:
rows = content.all('[role=row]')
_cells_selector = '[role=gridcell]'
cells = content.all(_cells_selector)
editing_cell_input = content.element('.MuiDataGrid-cell--editing input')
editing_cell_input = content.element('.MuiDataGrid-cell--editing').element('input')
'''
# TODO: make the following work...
# it fails because content.element('.MuiDataGrid-cell--editing')
# can't be "resolved", because has no name, was not actually used as a descriptor
# how to fix it? can we?
# DONE: now the following works...
# it failed previously because content.element('.MuiDataGrid-cell--editing')
# couldn't be "resolved", because has no name, was not actually used
# as a descriptor
editing_cell_input = content.element('.MuiDataGrid-cell--editing').element('input')
# this will work, by the way:
editing_cell = content.element('.MuiDataGrid-cell--editing')
editing_cell_input = editing_cell.element('input')
# by the way, check something like (it should work... – can we use it to fix above?):
# the following worked
editing_cell_input = Element('input').within(lambda self: self.content.element('.MuiDataGrid-cell--editing'))
# so we fixed it incorporating this idea into the code of descriptor.element
'''

def cells_of_row(self, number, /):
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/core/_pom_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from selene.support._pom import Element, All


def test__pom__element_is_unique_for_each_object():
class Page:
selector = '.element'
element = Element(selector)
elements = All(selector)

page1 = Page()
page2 = Page()

assert page1.selector is page2.selector
# but
assert page1.element is not page2.element
assert page1.elements is not page2.elements
# while
assert page1.element is page1.element
assert page1.elements is page1.elements
assert page2.element is page2.element
assert page2.elements is page2.elements
Empty file added tests/unit/support/__init__.py
Empty file.

0 comments on commit 1e7f32a

Please sign in to comment.