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 ",