From 1e7f32a8b07972468564e774ac58b1608f399bcd Mon Sep 17 00:00:00 2001 From: yashaka Date: Mon, 22 Jul 2024 01:23:15 +0300 Subject: [PATCH] [#505] REFACTOR: fix sub-sub-descriptors ... ... where first from "sub-" ones has no name + TODO: should not we set attrs on instance, not lru_cache __get__ result? --- selene/support/_pom.py | 103 +++++++++--------- ...est_material_ui__react_x_data_grid__mit.py | 13 ++- tests/unit/core/_pom_test.py | 21 ++++ tests/unit/support/__init__.py | 0 4 files changed, 81 insertions(+), 56 deletions(-) create mode 100644 tests/unit/core/_pom_test.py create mode 100644 tests/unit/support/__init__.py diff --git a/selene/support/_pom.py b/selene/support/_pom.py index e2213cc1..e8808c61 100644 --- a/selene/support/_pom.py +++ b/selene/support/_pom.py @@ -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? @@ -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) @@ -99,26 +113,34 @@ 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 --- # @@ -126,36 +148,19 @@ def all(self, selector: str | Tuple[str, str]) -> All: 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 @@ -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) @@ -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... diff --git a/tests/examples/pom/test_material_ui__react_x_data_grid__mit.py b/tests/examples/pom/test_material_ui__react_x_data_grid__mit.py index 999b2d82..eb32d321 100644 --- a/tests/examples/pom/test_material_ui__react_x_data_grid__mit.py +++ b/tests/examples/pom/test_material_ui__react_x_data_grid__mit.py @@ -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, /): diff --git a/tests/unit/core/_pom_test.py b/tests/unit/core/_pom_test.py new file mode 100644 index 00000000..6431d200 --- /dev/null +++ b/tests/unit/core/_pom_test.py @@ -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 diff --git a/tests/unit/support/__init__.py b/tests/unit/support/__init__.py new file mode 100644 index 00000000..e69de29b