Skip to content

Commit

Permalink
Merge pull request #69 from saritasa-nest/feature/update-base-xpah-lo…
Browse files Browse the repository at this point in the history
…cator

Update base XPathLocator
  • Loading branch information
M1troll authored Jun 11, 2024
2 parents 4fb2460 + e51fa74 commit f89f9ab
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 30 deletions.
9 changes: 9 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ Version history

We follow `Semantic Versions <https://semver.org/>`_.

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 <https://github.com/saritasa-nest/pomcorn/blob/main/pomcorn/component.py>`_.
- Fix some possible xpath errors depending on empty locators queries and
brackets.


0.7.0
*******************************************************************************

Expand Down
86 changes: 66 additions & 20 deletions pomcorn/descriptors/element.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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."""
Expand Down Expand Up @@ -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, {})
Expand All @@ -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!")
74 changes: 65 additions & 9 deletions pomcorn/locators/base_locators.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
Expand Down

0 comments on commit f89f9ab

Please sign in to comment.