Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build-paf-test-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ permissions:
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
jobs:
build-image:
if: false # Disabled
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.11"
python-version: "3.13"
- name: Install dependencies of all modules
run: pip install -r requirements.txt
- name: Test with pytest
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ element.expect.text.be("Search")
- [Components](doc/components.md)
- [Managing WebDrivers](doc/manager.md)
- [Execution controlling](doc/control.md)
- [Inject listener](doc/listener.md)
- [Inject listener](doc/listeners.md)

### Missing features (tdb)

Expand Down
32 changes: 0 additions & 32 deletions doc/listener.md

This file was deleted.

42 changes: 42 additions & 0 deletions doc/listeners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Listeners

*PAF* provides some listener interfaces to intercept internals.

* `ActionListener`: Listens to element actions
* `AssertionListener`: Listens to element assertions
* `WebDriverManagerListener`: Listen to the lifecycle of a *WebDriver*

## HighlightListener

Highlights actions and assertions on the element. Gets automatically injected when `PAF_DEMO_MODE` is enabled.

## Custom listeners

Implement your custom listener the following way

```python
from paf.listener import ActionListener, WebDriverManagerListener
from paf.uielement import UiElement

class MyListener(ActionListener, WebDriverManagerListener):
def action_passed(self, action_name: str, ui_element: UiElement):
pass
def webdriver_closed(self, webdriver: WebDriver):
pass
```
You need to inject your listener at configuration level like:
```python
import inject
from inject import Binder
import paf.config
from paf.listener import ActionListener, WebDriverManagerListener

def _inject(binder: Binder):
binder.install(paf.config.inject)
my_listener = MyListener()
binder.bind(ActionListener, my_listener)
binder.bind(WebDriverManagerListener, my_listener)

inject.configure(_inject)
```
Make sure, that the environment variable is `PAF_DEMO_MODE=0`, to prevent a duplicate injection of *HighlightListener*.
4 changes: 2 additions & 2 deletions examples/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from paf.manager import WebDriverManager
from paf.page import PageFactory
from paf.request import WebDriverRequest
from test import create_webdriver
from test import get_webdriver


@pytest.mark.asyncio
Expand All @@ -26,7 +26,7 @@ async def test_run_in_tasks():

def run_google_search(i: int):
request = WebDriverRequest(f"asyncio{i}")
webdriver = create_webdriver(request)
webdriver = get_webdriver(request)
page_factory = inject.instance(PageFactory)
webdriver_manager = inject.instance(WebDriverManager)

Expand Down
4 changes: 2 additions & 2 deletions examples/test_cloudflare_challenge.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from paf.request import WebDriverRequest
from paf.uielement import UiElement
from paf.xpath import XPath
from test import create_webdriver
from test import get_webdriver


@pytest.fixture
Expand All @@ -24,7 +24,7 @@ def cloudflare():
request = WebDriverRequest("cloudflare")
request.window_position = Point(-1024,0)
request.window_maximize = True
page = page_factory.create_page(CloudflarePage, create_webdriver(request))
page = page_factory.create_page(CloudflarePage, get_webdriver(request))
yield page


Expand Down
4 changes: 2 additions & 2 deletions examples/test_monkeytype.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from paf.control import change
from paf.manager import WebDriverManager
from paf.page import PageFactory, FinderPage
from test import create_webdriver
from test import get_webdriver


@pytest.fixture
def finder():
page_factory = inject.instance(PageFactory)
finder = page_factory.create_page(FinderPage, create_webdriver())
finder = page_factory.create_page(FinderPage, get_webdriver())
yield finder


Expand Down
10 changes: 5 additions & 5 deletions paf/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,23 @@ def _test_sequence(
test: Predicate[ACTUAL_TYPE],
additional_subject: Supplier = None,
) -> bool:
from paf.listener import Listener
listener = inject.instance(Listener)
from paf.listener import AssertionListener
assertion_listener = inject.instance(AssertionListener)

try:
def perform_test():
assert test(self.actual), "Expected"

retry(perform_test, lambda e: listener.assertion_failed(self, self._find_closest_ui_element(), e))
listener.assertion_passed(self, self._find_closest_ui_element())
retry(perform_test, lambda e: assertion_listener.assertion_failed(self, self._find_closest_ui_element(), e))
assertion_listener.assertion_passed(self, self._find_closest_ui_element())
return True

except RetryException as exception:
exception.add_subject(self.name_path)
if additional_subject:
exception.add_subject(additional_subject())
#exception.update_sequence(sequence)
listener.assertion_failed_finally(self, self._find_closest_ui_element(), exception)
assertion_listener.assertion_failed_finally(self, self._find_closest_ui_element(), exception)

if self._raise:
raise AssertionErrorWrapper(exception)
Expand Down
5 changes: 5 additions & 0 deletions paf/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ class Property(Enum):
def env(prop: "Property") -> any:
return os.getenv(prop.name, prop.value)

@staticmethod
def is_true(prop: "Property") -> bool:
val = Property.env(prop)
return val and str.lower(val) in ("1", "on", "true", "enabled")


class Formatter:
def datetime(self, date: datetime):
Expand Down
6 changes: 2 additions & 4 deletions paf/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
import paf.control
import paf.manager
import paf.page
from paf.common import Property
from paf.listener import Listener, HighlightListener
import paf.listener


def inject(binder: Binder):
binder.install(paf.manager.inject_config)
binder.install(paf.page.inject_config)
binder.install(paf.common.inject_config)
if Property.env(Property.PAF_DEMO_MODE) == "1":
binder.bind(Listener, HighlightListener())
binder.install(paf.listener.inject_config)
107 changes: 68 additions & 39 deletions paf/listener.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,83 @@
import logging
from warnings import deprecated

import inject
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.color import Color

from paf import javascript
from paf.assertion import AbstractAssertion
from paf.common import NotFoundException
from paf.common import NotFoundException, Property
from paf.request import WebDriverRequest


class Listener:
class ActionListener:
def action_passed(
self,
action_name: str,
ui_element: "UiElement"
self,
action_name: str,
ui_element: "UiElement"
): # pragma: no cover
pass

def action_failed(
self,
action_name: str,
ui_element: "UiElement",
exception: Exception
self,
action_name: str,
ui_element: "UiElement",
exception: Exception
): # pragma: no cover
pass

def action_failed_finally(
self,
action_name: str,
ui_element: "UiElement",
exception: Exception
self,
action_name: str,
ui_element: "UiElement",
exception: Exception
): # pragma: no cover
pass

class AssertionListener:
def assertion_passed(
self,
assertion: AbstractAssertion,
ui_element: "UiElement"
self,
assertion: AbstractAssertion,
ui_element: "UiElement"
): # pragma: no cover
pass

def assertion_failed(
self,
assertion: AbstractAssertion,
ui_element: "UiElement",
exception: Exception
self,
assertion: AbstractAssertion,
ui_element: "UiElement",
exception: Exception
): # pragma: no cover
pass

def assertion_failed_finally(
self,
assertion: AbstractAssertion,
ui_element: "UiElement",
exception: Exception
self,
assertion: AbstractAssertion,
ui_element: "UiElement",
exception: Exception
): # pragma: no cover
pass

@deprecated("Use specific listener interface")
class Listener(ActionListener, AssertionListener):
pass

class HighlightListener(Listener):

class WebDriverManagerListener:
def webdriver_create(self, request: WebDriverRequest): # pragma: no cover
pass
def webdriver_introduce(self, webdriver: WebDriver):
pass
def webdriver_introduced(self, webdriver: WebDriver): # pragma: no cover
pass
def webdriver_close(self, webdriver: WebDriver): # pragma: no cover
pass
def webdriver_closed(self, webdriver: WebDriver): # pragma: no cover
pass


class HighlightListener(ActionListener, AssertionListener):

def _highlight_with_color(self, ui_element: "UiElement", color: str):
try:
Expand All @@ -66,20 +88,20 @@ def _highlight_with_color(self, ui_element: "UiElement", color: str):
logging.warning(f"Cannot highlight {ui_element.name_path}: {e}")

def action_passed(
self,
action_name: str,
ui_element: "UiElement"
self,
action_name: str,
ui_element: "UiElement"
):
if action_name == "highlight":
return

self._highlight_with_color(ui_element, "#ff0")

def action_failed_finally(
self,
action_name: str,
ui_element: "UiElement",
exception: Exception
self,
action_name: str,
ui_element: "UiElement",
exception: Exception
):
if action_name == "highlight":
return
Expand All @@ -88,18 +110,25 @@ def action_failed_finally(
self._highlight_with_color(ui_element, "#f00")

def assertion_passed(
self,
assertion: AbstractAssertion,
ui_element: "UiElement"
self,
assertion: AbstractAssertion,
ui_element: "UiElement"
):
if assertion.raise_exception:
self._highlight_with_color(ui_element, "#0f0")

def assertion_failed_finally(
self,
assertion: AbstractAssertion,
ui_element: "UiElement",
exception: Exception
self,
assertion: AbstractAssertion,
ui_element: "UiElement",
exception: Exception
):
if not isinstance(exception, NotFoundException) and assertion.raise_exception:
self._highlight_with_color(ui_element, "#f00")


def inject_config(binder: inject.Binder):
if Property.is_true(Property.PAF_DEMO_MODE):
highlight_listener = HighlightListener()
binder.bind(ActionListener, highlight_listener)
binder.bind(AssertionListener, highlight_listener)
Loading