diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index ed12c8b..1ecf370 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -3,6 +3,15 @@ Version history We follow `Semantic Versions `_. +0.7.1 +******************************************************************************* + +- Add ability to `Element` to specify simple and relative locators using the + `locator` or `relative_locator` arguments, as in `Component.init_element `_. +- Fix some possible xpath errors depending on empty locators queries and + brackets. + + 0.7.0 ******************************************************************************* diff --git a/pomcorn/descriptors/element.py b/pomcorn/descriptors/element.py index 34db1c4..d27829a 100644 --- a/pomcorn/descriptors/element.py +++ b/pomcorn/descriptors/element.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NoReturn +from typing import TYPE_CHECKING, NoReturn, overload from pomcorn import locators @@ -23,23 +23,39 @@ class MainPage(Page): cache_attribute_name = "cached_elements" + @overload def __init__( self, - locator: locators.XPathLocator, - is_relative_locator: bool = True, + locator: locators.XPathLocator | None = None, + ) -> None: + ... + + @overload + def __init__( + self, + *, + relative_locator: locators.XPathLocator | None = None, + ) -> None: + ... + + def __init__( + self, + locator: locators.XPathLocator | None = None, + *, + relative_locator: locators.XPathLocator | None = None, ) -> None: """Initialize descriptor. - Args: - locator: Instance of a class to locate the element in - the browser. - is_relative_locator: Whether add parent ``base_locator`` to the - current descriptors's `base_locator` or not. If descriptor is - used for ``Page``, the value of this argument will not be used. + Use `relative_locator` if you need to include `base_locator` of + instance, otherwise use `locator`. + + If descriptor is used for instance of ``Page``, then + ``relative_locator`` is not needed, since element will be searched + across the entire page, not within some component. """ - self.is_relative_locator = is_relative_locator self.locator = locator + self.relative_locator = relative_locator def __set_name__(self, _owner: type, name: str) -> None: """Save attribute name for which descriptor is created.""" @@ -69,10 +85,6 @@ def prepare_element(self, instance: WebView) -> XPathElement: ``self.is_relative_locator=True``, element will be found by sum of ``base_locator`` of that component and passed locator of descriptor. - If descriptor is used for instance of ``Page``, then ``base_locator`` - is not needed, since element will be searched across the entire page, - not within some component. - """ if not getattr(instance, self.cache_attribute_name, None): setattr(instance, self.cache_attribute_name, {}) @@ -81,15 +93,49 @@ def prepare_element(self, instance: WebView) -> XPathElement: if cached_element := cache.get(self.attribute_name): return cached_element - from pomcorn import Component - - if self.is_relative_locator and isinstance(instance, Component): - self.locator = instance.base_locator // self.locator - - element = instance.init_element(locator=self.locator) + element = instance.init_element( + locator=self._prepare_locator(instance), + ) cache[self.attribute_name] = element return element + def _prepare_locator(self, instance: WebView) -> locators.XPathLocator: + """Prepare a locator by arguments. + + Check that only one locator argument is passed, or none. + If only `relative_locator` was passed, `base_locator` of instance will + be added to specified in descriptor arguments. If only `locator` was + passed, it will return only specified one. + + Raises: + ValueError: If both arguments were passed or neither or + ``relative_locator`` used not in ``Component``. + + """ + if self.relative_locator and self.locator: + raise ValueError( + "You need to pass only one of the arguments: " + "`locator` or `relative_locator`.", + ) + + if not self.relative_locator: + if not self.locator: + raise ValueError( + "You need to pass one of the arguments: " + "`locator` or `relative_locator`.", + ) + return self.locator + + from pomcorn import Component + + if self.relative_locator and isinstance(instance, Component): + return instance.base_locator // self.relative_locator + + raise ValueError( + f"`relative_locator` should be used only if descriptor used in " + f"component. `{instance}` is not a component.", + ) + def __set__(self, *args, **kwargs) -> NoReturn: raise ValueError("You can't reset an element attribute value!") diff --git a/pomcorn/locators/base_locators.py b/pomcorn/locators/base_locators.py index 2f28b17..952b522 100644 --- a/pomcorn/locators/base_locators.py +++ b/pomcorn/locators/base_locators.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterator -from typing import TypeVar +from typing import Literal, TypeVar from selenium.webdriver.common.by import By @@ -120,16 +120,22 @@ def __init__(self, query: str): super().__init__(by=By.XPATH, query=query) def __truediv__(self, other: XPathLocator) -> XPathLocator: - """Override `/` operator to implement following XPath locators.""" - return XPathLocator( - query=f"//{self.related_query}/{other.related_query}", - ) + """Override `/` operator to implement following XPath locators. + + "/" used to select the nearest children of the current node. + + """ + return self.prepare_relative_locator(other=other, separator="/") def __floordiv__(self, other: XPathLocator) -> XPathLocator: - """Override `//` operator to implement nested XPath locators.""" - return XPathLocator( - query=f"//{self.related_query}//{other.related_query}", - ) + """Override `//` operator to implement nested XPath locators. + + "//" used to select all descendants (children, grandchildren, + great-grandchildren, etc.) of current node, regardless of their level + in hierarchy. + + """ + return self.prepare_relative_locator(other=other, separator="//") def __or__(self, other: XPathLocator) -> XPathLocator: r"""Override `|` operator to implement variant XPath locators. @@ -146,6 +152,10 @@ def __or__(self, other: XPathLocator) -> XPathLocator: """ return XPathLocator(query=f"({self.query} | {other.query})") + def __bool__(self) -> bool: + """Return whether query of current locator is empty or not.""" + return bool(self.related_query) + def extend_query(self, extra_query: str) -> XPathLocator: """Return new XPathLocator with extended query.""" return XPathLocator(query=self.query + extra_query) @@ -165,3 +175,49 @@ def contains(self, text: str, exact: bool = False) -> XPathLocator: partial_query = f"[contains(., '{text}')]" exact_query = f"[./text()='{text}']" return self.extend_query(exact_query if exact else partial_query) + + def prepare_relative_locator( + self, + other: XPathLocator, + separator: Literal["/", "//"] = "/", + ) -> XPathLocator: + """Prepare relative locator base on queries of two locators. + + If one of parent and other locator queries is empty, the method will + return only the filled one. + + Args: + other: Child locator object. + separator: Literal which will placed between locators queries - "/" + used to select nearest children of current node and "//" used + to select all descendants (children, grandchildren, + great-grandchildren, etc.) of current node, regardless of their + level in hierarchy. + + Raises: + ValueError: If parent and child locators queries are empty. + + """ + related_query = self.related_query + if not related_query.startswith("("): + # Parent query can be bracketed, in which case we don't need to use + # `//` + # Example: + # (//li)[3] -> valid + # //(//li)[3] -> invalid + related_query = f"//{self.related_query}" + + locator = XPathLocator( + query=f"{related_query}{separator}{other.related_query}", + ) + + if self and other: + return locator + + if not (self or other): + raise ValueError( + f"Both of locators have empty query. The `{locator.query}` is " + "not a valid locator.", + ) + + return other if not self else self diff --git a/pyproject.toml b/pyproject.toml index e4e6924..d1b724c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pomcorn" -version = "0.7.0" +version = "0.7.1" description = "Base implementation of Page Object Model" authors = [ "Saritasa ",