Skip to content

Commit

Permalink
Add quotes escaper method
Browse files Browse the repository at this point in the history
  • Loading branch information
QuirrelForU authored Jan 31, 2025
1 parent 4d21853 commit 06164ed
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 9 deletions.
7 changes: 7 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ Version history

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

0.8.4 (30.01.25)
*******************************************************************************
- Add escaping single and double quotes in the: ``ElementWithTextLocator``,
``InputInLabelLocator``, ``InputByLabelLocator``, ``TextAreaByLabelLocator``.
- Add escaping single and double quotes in the ``get_item_by_text`` method of
the ``ListComponent``

0.8.3 (20.12.24)
*******************************************************************************
- Rename ``__parameters__`` in ``ListComponent`` to ``__generic__parameters``
Expand Down
4 changes: 3 additions & 1 deletion pomcorn/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,9 @@ def is_valid_item_class(cls, item_class: Any) -> bool:
def get_item_by_text(self, text: str) -> ListItemType:
"""Get list item by text."""
locator = self.base_item_locator.extend_query(
extra_query=f"[contains(.,'{text}')]",
extra_query=(
f"[contains(., {self.base_item_locator._escape_quotes(text)})]"
),
)
return self._item_class(page=self.page, base_locator=locator)

Expand Down
45 changes: 43 additions & 2 deletions pomcorn/locators/base_locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,47 @@ def __bool__(self) -> bool:
"""Return whether query of current locator is empty or not."""
return bool(self.related_query)

@classmethod
def _escape_quotes(cls, text: str) -> str:
"""Escape single and double quotes in given text for use in locators. # noqa: D202, E501.
This method is useful when locating elements
with text containing single or double quotes.
For example, the text `He's 6'2"` will be transformed into:
`concat("He", "'", "s 6", "'", "2", '"')`.
The resulting string can be used in XPath expressions
like `text()=...` or `contains(.,...)`.
Returns:
The escaped text wrapped in `concat()` for XPath compatibility,
or the original text in double quotes if no escaping is needed.
"""

if not text or ('"' not in text and "'" not in text):
return f'"{text}"'

escaped_parts = []
buffer = "" # Temporary storage for normal characters

for char in text:
if char not in ('"', "'"):
buffer += char
continue
if buffer:
escaped_parts.append(f'"{buffer}"')
buffer = ""
escaped_parts.append(
"'" + char + "'" if char == '"' else '"' + char + '"',
)

if buffer:
escaped_parts.append(f'"{buffer}"')

return f"concat({', '.join(escaped_parts)})"

def extend_query(self, extra_query: str) -> XPathLocator:
"""Return new XPathLocator with extended query."""
return XPathLocator(query=self.query + extra_query)
Expand All @@ -172,8 +213,8 @@ def contains(self, text: str, exact: bool = False) -> XPathLocator:
By default, the search is based on a partial match.
"""
partial_query = f"[contains(., '{text}')]"
exact_query = f"[./text()='{text}']"
partial_query = f"[contains(., {self._escape_quotes(text)})]"
exact_query = f"[./text()={self._escape_quotes(text)}]"
return self.extend_query(exact_query if exact else partial_query)

def prepare_relative_locator(
Expand Down
16 changes: 11 additions & 5 deletions pomcorn/locators/xpath_locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ def __init__(self, text: str, element: str = "*", exact: bool = False):
partial match of the value.
"""
exact_query = f'//{element}[./text()="{text}"]'
partial_query = f'//{element}[contains(.,"{text}")]'
exact_query = f"//{element}[./text()={self._escape_quotes(text)}]"
partial_query = f"//{element}[contains(.,{self._escape_quotes(text)})]"

super().__init__(query=exact_query if exact else partial_query)

Expand Down Expand Up @@ -219,7 +219,7 @@ class InputInLabelLocator(XPathLocator):
def __init__(self, label: str):
"""Init XPathLocator."""
super().__init__(
query=f'//label[contains(., "{label}")]//input',
query=f"//label[contains(., {self._escape_quotes(label)})]//input",
)


Expand All @@ -243,7 +243,10 @@ class InputByLabelLocator(XPathLocator):
def __init__(self, label: str):
"""Init XPathLocator."""
super().__init__(
query=f'//label[contains(., "{label}")]/following-sibling::input',
query=(
f"//label[contains(., {self._escape_quotes(label)})]"
"/following-sibling::input"
),
)


Expand All @@ -259,5 +262,8 @@ class TextAreaByLabelLocator(XPathLocator):
def __init__(self, label: str):
"""Init XPathLocator."""
super().__init__(
query=f'//*[label[contains(text(), "{label}")]]/textarea',
query=(
"//*[label[contains(text(), "
f"{self._escape_quotes(label)})]]/textarea"
),
)
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.8.3"
version = "0.8.4"
description = "Base implementation of Page Object Model"
authors = [
"Saritasa <[email protected]>",
Expand Down
Empty file added tests/locators/__init__.py
Empty file.
51 changes: 51 additions & 0 deletions tests/locators/test_quote_escaper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from pomcorn.locators.base_locators import XPathLocator


def test_empty_string():
"""Test that an empty string returned as wrapped empty string."""
assert XPathLocator._escape_quotes("") == '""'


def test_no_quotes():
"""Test that a string without quotes returned as wrapped passed text."""
assert XPathLocator._escape_quotes("Hello World") == '"Hello World"'


def test_single_quote():
"""Test escaping a string with a single quote."""
assert (
XPathLocator._escape_quotes("He's tall")
== 'concat("He", "\'", "s tall")'
)


def test_double_quote():
"""Test escaping a string with a double quote."""
assert (
XPathLocator._escape_quotes('She said "Hello"')
== 'concat("She said ", \'"\', "Hello", \'"\')'
)


def test_both_single_and_double_quotes():
"""Test escaping a string with both single and double quotes."""
assert (
XPathLocator._escape_quotes("He's 6'2\" tall")
== 'concat("He", "\'", "s 6", "\'", "2", \'"\', " tall")'
)


def test_string_starts_with_quote():
"""Test escaping a string that starts with a quote."""
assert XPathLocator._escape_quotes('"Start') == 'concat(\'"\', "Start")'


def test_string_ends_with_quote():
"""Test escaping a string that ends with a quote."""
assert XPathLocator._escape_quotes('End"') == 'concat("End", \'"\')'


def test_string_with_only_quotes():
"""Test escaping a string that contains only quotes."""
assert XPathLocator._escape_quotes('""') == "concat('\"', '\"')"
assert XPathLocator._escape_quotes("''") == 'concat("\'", "\'")'

0 comments on commit 06164ed

Please sign in to comment.