Skip to content

Commit

Permalink
Merge pull request #14 from mreiche/bugfix/exception-message-improvem…
Browse files Browse the repository at this point in the history
…ents

Bugfix/exception message improvements
  • Loading branch information
mreiche authored Aug 27, 2023
2 parents d43feeb + f135fbd commit d419b11
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 46 deletions.
2 changes: 1 addition & 1 deletion paf/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def perform_test():
if additional_subject:
subject += additional_subject()

raise AssertionError(f"Expected {subject} after {e.sequence.count} retries ({round(e.sequence.duration, 2)} seconds)")
raise AssertionError(f"Expected {subject} {e}")
return False

@property
Expand Down
6 changes: 4 additions & 2 deletions paf/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,13 @@ def datetime(self, date: datetime):


class NotFoundException(Exception):
pass
def __init__(self):
super().__init__(f"Element not found")


class NotUniqueException(Exception):
pass
def __init__(self):
super().__init__(f"Element not unique")


def inject_config(binder: inject.Binder):
Expand Down
16 changes: 7 additions & 9 deletions paf/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,11 @@ def count(self):


class RetryException(Exception):
def __init__(self, sequence: Sequence, *args, **kwargs):
super().__init__(args, kwargs)
self._sequence = sequence

@property
def sequence(self):
return self._sequence
def __init__(self, sequence: Sequence, exception: Exception):
prefix = f"{exception}"
if len(prefix) > 0:
prefix += f" "
super().__init__(f"{prefix}after {sequence.count} retries ({round(sequence.duration, 2)} seconds)")


@contextmanager
Expand Down Expand Up @@ -95,5 +93,5 @@ def _run():

sequence.run(_run)

if exception:
raise RetryException(sequence, f"{exception} after {sequence.count} retries ({round(sequence.duration, 2)} seconds)")
if exception is not None:
raise RetryException(sequence, exception)
6 changes: 6 additions & 0 deletions paf/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def action_passed(
action_name: str,
ui_element: "UiElement"
):
if action_name == "highlight":
return

self._highlight_with_color(ui_element, "#ff0")

def action_failed_finally(
Expand All @@ -76,6 +79,9 @@ def action_failed_finally(
ui_element: "UiElement",
exception: Exception
):
if action_name == "highlight":
return

if not isinstance(exception, NotFoundException):
self._highlight_with_color(ui_element, "#f00")

Expand Down
66 changes: 44 additions & 22 deletions paf/uielement.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import math
from abc import abstractmethod, ABC
from datetime import datetime
from pathlib import Path
from typing import Type, TypeVar, List, Generic, Iterable, Iterator
from typing import Type, TypeVar, List, Generic, Iterable, Iterator, Callable

import inject
import math
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By as SeleniumBy
from selenium.webdriver.remote.webdriver import WebDriver
Expand All @@ -18,7 +18,7 @@
from paf.dom import Attribute
from paf.listener import Listener
from paf.locator import By
from paf.types import Mapper, Consumer, R
from paf.types import Mapper, R
from paf.xpath import XPath


Expand Down Expand Up @@ -196,30 +196,36 @@ def _handle(web_elements: List[WebElement]):
count = len(web_elements)

if self._by.is_unique and count != 1:
raise NotUniqueException(f"Not unique")
raise NotUniqueException()
elif count > self._index:
web_element = web_elements[self._index]
return mapper(web_element)
else:
raise NotFoundException(f"Not found")
raise NotFoundException()

return self._find_web_elements(_handle)

def _action_sequence(self, consumer: Consumer[WebElement], action_name: str):
def _action_sequence(self, method: Callable, action_name: str):
listener = inject.instance(Listener)
try:
retry(lambda: self.find_web_element(consumer), lambda e: listener.action_failed(action_name, self, e))
retry(method, lambda e: listener.action_failed(action_name, self, e))
listener.action_passed(action_name, self)
except Exception as exception:
listener.action_failed_finally(action_name, self, exception)
raise Exception(f"{self.name_path}: {exception}")

def click(self):
self._action_sequence(lambda web_element: web_element.click(), __name__)
def _action(web_element: WebElement):
web_element.click()

self._action_sequence(lambda: self.find_web_element(_action), __name__)
return self

def send_keys(self, value: str):
self._action_sequence(lambda web_element: web_element.send_keys(value), __name__)
def _action(web_element: WebElement):
web_element.send_keys(value)

self._action_sequence(lambda: self.find_web_element(_action), __name__)
return self

def take_screenshot(self) -> Path | None:
Expand All @@ -242,36 +248,36 @@ def _action(web_element: WebElement):
web_element.send_keys(value)
assert web_element.get_attribute("value") == value

self._action_sequence(_action, __name__)
self._action_sequence(lambda: self.find_web_element(_action), __name__)
return self

def hover(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions.move_to_element(web_element).perform()

self._action_sequence(_action, __name__)
self._action_sequence(lambda: self.find_web_element(_action), __name__)

def context_click(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions.context_click(web_element).perform()

self._action_sequence(_action, __name__)
self._action_sequence(lambda: self.find_web_element(_action), __name__)

def long_click(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions.click_and_hold(web_element).perform()

self._action_sequence(_action, __name__)
self._action_sequence(lambda: self.find_web_element(_action), __name__)

def double_click(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions.double_click(web_element).perform()

self._action_sequence(_action, __name__)
self._action_sequence(lambda: self.find_web_element(_action), __name__)

def drag_and_drop_to(self, target_ui_element: "UiElement"):
def _action(source: WebElement):
Expand All @@ -280,7 +286,7 @@ def _target_found(target: WebElement):
actions.drag_and_drop(source, target).perform()
target_ui_element.find_web_element(_target_found)

self._action_sequence(_action, __name__)
self._action_sequence(lambda: self.find_web_element(_action), __name__)

@property
def expect(self):
Expand All @@ -291,11 +297,15 @@ def wait_for(self):
return UiElementAssertion(self, raise_exception=False)

def clear(self):
self._action_sequence(lambda web_element: web_element.clear(), __name__)
def _action(web_element: WebElement):
web_element.clear()
self._action_sequence(lambda: self.find_web_element(_action), __name__)
return self

def submit(self):
self._action_sequence(lambda web_element: web_element.submit(), __name__)
def _action(web_element: WebElement):
web_element.submit()
self._action_sequence(lambda: self.find_web_element(_action), __name__)
return self

def __str__(self):
Expand All @@ -309,10 +319,15 @@ def name(self):
return f"UiElement({self._by.__str__()})[{self._index}]"

def scroll_into_view(self, x: int = 0, y: int = 0):
self._action_sequence(lambda web_element: script.scroll_to_center(self._webdriver, web_element, Point(x, y)), __name__)
def _action(web_element: WebElement):
script.scroll_to_center(self._webdriver, web_element, Point(x, y))
self._action_sequence(lambda: self.find_web_element(_action), __name__)

def scroll_to_top(self, x: int = 0, y: int = 0):
self._action_sequence(lambda web_element: script.scroll_to_top(self._webdriver, web_element, Point(x, y)), __name__)
def _action(web_element: WebElement):
script.scroll_to_top(self._webdriver, web_element, Point(x, y))

self._action_sequence(lambda: self.find_web_element(_action), __name__)

def _count_elements(self):
count = 0
Expand All @@ -325,10 +340,10 @@ def _count(web_elements: List[WebElement]):
return count

def highlight(self, color: Color = Color.from_string("#0f0"), seconds: float = 2):
def _handle(web_element: WebElement):
def _action(web_element: WebElement):
script.highlight(self._webdriver, web_element, color, math.floor(seconds * 1000))

self.find_web_element(_handle)
self._action_sequence(lambda: self.find_web_element(_action), __name__)

def __iter__(self):
for i in range(self._count_elements()):
Expand Down Expand Up @@ -360,10 +375,17 @@ def _map_web_element_property(
mapper: Mapper[WebElement, any],
property_name: str
) -> ASSERTION:

def _map_failsafe():
try:
return self._ui_element.find_web_element(mapper)
except Exception:
return None

return assertion_class(
parent=self._ui_element,
actual_supplier=lambda: self._ui_element.find_web_element(mapper),
name_supplier=lambda: f".{property_name} {Format.param(self._ui_element.find_web_element(mapper))}",
name_supplier=lambda: f".{property_name} {Format.param(_map_failsafe())}",
raise_exception=self._raise,
)

Expand Down
2 changes: 1 addition & 1 deletion test/test_demo_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_highlight_not_found_log(finder: FinderPage, listener: Listener, caplog)

assert len(caplog.records) == 1
for record in caplog.records:
assert "Cannot highlight UiElement(By.css selector(#inexistent))[0]: Not found" in record.message
assert "Cannot highlight UiElement(By.css selector(#inexistent))[0]: Element not found" in record.message


def teardown_module():
Expand Down
26 changes: 15 additions & 11 deletions test/test_uielement.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

import inject
import pytest
from selenium.webdriver.support.color import Color
Expand Down Expand Up @@ -81,14 +83,11 @@ def test_assertions(finder: FinderPage):


def test_text_assertion_fails(finder: FinderPage):
with pytest.raises(AssertionError) as e:
with pytest.raises(AssertionError, match=re.escape("Expected UiElement(By.css selector(#para1))[0].attribute(data) *undefined* to be [null] after 3 retries")):
finder.open("https://testpages.herokuapp.com/styled/basic-web-page-test.html")
p = finder.find("#para1")
p.expect.attribute("data").be("null")

assert "Expected UiElement(By.css selector(#para1))[0].attribute(data) *undefined* to be [null] after 3 retries" in \
e.value.args[0]


def test_untested_assertion_raises(finder: FinderPage):
finder.open("https://testpages.herokuapp.com/styled/basic-web-page-test.html")
Expand Down Expand Up @@ -286,20 +285,25 @@ def test_not_unique_fails(finder: FinderPage):
btn.click()

events = finder.find("#events")
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match=re.escape("Expected UiElement(By.css selector(#events))[0] > UiElement(By.tag name(p))[0].text *undefined* to be [click] Element not unique after 3 retries")):
p = events.find(By.tag_name("p").unique)
p.expect.text.be("click")

assert "Not unique" in e.value.args[0]

def test_highlight_nonexistent_fails(finder: FinderPage):
finder.open("https://testpages.herokuapp.com/styled/basic-web-page-test.html")

with pytest.raises(Exception, match=re.escape("UiElement(By.css selector(#unkown))[0]: Element not found after 3 retries")):
unknown = finder.find("#unkown")
unknown.highlight()

def test_not_found_fails(finder: FinderPage):

def test_count_nonexistent_fails(finder: FinderPage):
finder.open("https://testpages.herokuapp.com/styled/basic-web-page-test.html")
with pytest.raises(Exception) as e:
unknown = finder.find("#unkown")
unknown.highlight()

assert "Not found" in e.value.args[0]
with pytest.raises(Exception, match=re.escape("Expected UiElement(By.css selector(#unkown))[0] count [0] to be [1] after 3 retries")):
unknown = finder.find("#unkown")
unknown.expect.count.be(1)


def teardown_module():
Expand Down

0 comments on commit d419b11

Please sign in to comment.