mirror of
https://git.zavage.net/Zavage-Software/wabot.git
synced 2026-07-21 13:06:08 -06:00
feat: typed field wrappers with pacing (port of legacy fields.py)
This commit is contained in:
parent
14281a0774
commit
807b91e902
106
src/wabot/fields.py
Normal file
106
src/wabot/fields.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""Typed wrappers around page elements.
|
||||
|
||||
``page[key]`` returns one of these instead of a raw WebElement. Unknown
|
||||
attribute access falls through to the underlying selenium element, so a
|
||||
wrapper can be used anywhere an element can. ``NullField`` is the falsy
|
||||
null-object returned for elements that could not be found, enabling
|
||||
``if el:`` guards instead of exception handling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from selenium.webdriver.support.ui import Select
|
||||
|
||||
LOGGER = logging.getLogger("wabot")
|
||||
|
||||
|
||||
class PageObject:
|
||||
"""Base wrapper; subclasses add typed get/set behavior."""
|
||||
|
||||
def __init__(self, page, accessors=None, name=None):
|
||||
self.page = page
|
||||
self.accessors = accessors
|
||||
self.name = name
|
||||
self.el = self._locate() if accessors else None
|
||||
|
||||
def _locate(self):
|
||||
by, value = self.accessors
|
||||
return self.page.driver.find_element(by=by, value=value)
|
||||
|
||||
@property
|
||||
def pacing(self):
|
||||
return self.page.pacing
|
||||
|
||||
def _pace(self, action: str) -> None:
|
||||
delay = self.pacing.delay(action)
|
||||
if delay:
|
||||
time.sleep(delay)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == "el": # guard: never recurse if __init__ didn't finish
|
||||
raise AttributeError(name)
|
||||
return getattr(self.el, name)
|
||||
|
||||
def click(self):
|
||||
return self.page.click(self.el)
|
||||
|
||||
def click_and_go(self):
|
||||
self._pace("navigate")
|
||||
return self.page.click_and_go(self.el)
|
||||
|
||||
|
||||
class TextField(PageObject):
|
||||
def get_value(self):
|
||||
return self.page.get_el_value(self.el)
|
||||
|
||||
def set_value(self, value):
|
||||
LOGGER.info("[%s] set_text(%r)", self.name, value)
|
||||
self._pace("text")
|
||||
return self.page.set_el_value(self.el, value)
|
||||
|
||||
|
||||
class SelectField(PageObject):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.dropdown = Select(self.el)
|
||||
|
||||
def get_value(self):
|
||||
return self.page.get_select_value(self.dropdown)
|
||||
|
||||
def set_value(self, value=None, text=None):
|
||||
LOGGER.info("[%s] set_select(%r)", self.name, value if value is not None else text)
|
||||
self._pace("select")
|
||||
return self.page.set_select_value(self.dropdown, value=value, text=text)
|
||||
|
||||
def select_by_index(self, index):
|
||||
self._pace("select")
|
||||
return self.dropdown.select_by_index(index)
|
||||
|
||||
|
||||
class CheckField(PageObject):
|
||||
def get_checked(self, ignore_disabled=False):
|
||||
return self.page.get_checkbox_value(self.el, ignore_disabled)
|
||||
|
||||
def set_checked(self, checked):
|
||||
LOGGER.info("[%s] set_checked(%r)", self.name, checked)
|
||||
self._pace("checkbox")
|
||||
return self.page.set_checkbox(self.el, checked)
|
||||
|
||||
|
||||
class NullField:
|
||||
"""Falsy placeholder for an element that was not found."""
|
||||
|
||||
def __init__(self, name=None):
|
||||
# bypass __getattr__ tricks: plain attribute set
|
||||
object.__setattr__(self, "name", name)
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __getattr__(self, attr):
|
||||
raise AttributeError(
|
||||
f"element {self.name!r} was not found on the page (attribute {attr!r})"
|
||||
)
|
||||
87
tests/unit/test_fields.py
Normal file
87
tests/unit/test_fields.py
Normal file
@ -0,0 +1,87 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from wabot.fields import CheckField, NullField, PageObject, SelectField, TextField
|
||||
from wabot.pacing import NoPacing
|
||||
|
||||
ACCESSORS = ("id", "username")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page():
|
||||
page = MagicMock(name="page")
|
||||
page.pacing = NoPacing()
|
||||
return page
|
||||
|
||||
|
||||
class TestPageObject:
|
||||
def test_locates_element_via_page_driver(self, page):
|
||||
obj = PageObject(page, accessors=ACCESSORS, name="username")
|
||||
page.driver.find_element.assert_called_once_with(by="id", value="username")
|
||||
assert obj.el is page.driver.find_element.return_value
|
||||
|
||||
def test_unknown_attributes_fall_through_to_element(self, page):
|
||||
obj = PageObject(page, accessors=ACCESSORS, name="username")
|
||||
assert obj.tag_name is obj.el.tag_name
|
||||
|
||||
def test_click_delegates_to_page(self, page):
|
||||
obj = PageObject(page, accessors=ACCESSORS, name="username")
|
||||
assert obj.click() is page.click.return_value
|
||||
page.click.assert_called_once_with(obj.el)
|
||||
|
||||
|
||||
class TestTextField:
|
||||
def test_set_value_uses_page_helper(self, page):
|
||||
field = TextField(page, accessors=ACCESSORS, name="username")
|
||||
result = field.set_value("mathew")
|
||||
page.set_el_value.assert_called_once_with(field.el, "mathew")
|
||||
assert result is page.set_el_value.return_value
|
||||
|
||||
def test_get_value_uses_page_helper(self, page):
|
||||
field = TextField(page, accessors=ACCESSORS, name="username")
|
||||
assert field.get_value() is page.get_el_value.return_value
|
||||
|
||||
|
||||
class TestSelectField:
|
||||
def test_wraps_element_in_selenium_select(self, page, monkeypatch):
|
||||
import wabot.fields as fields_mod
|
||||
|
||||
fake_select_cls = MagicMock(name="Select")
|
||||
monkeypatch.setattr(fields_mod, "Select", fake_select_cls)
|
||||
field = SelectField(page, accessors=("id", "state"), name="state")
|
||||
fake_select_cls.assert_called_once_with(field.el)
|
||||
assert field.dropdown is fake_select_cls.return_value
|
||||
|
||||
def test_set_value_by_value_and_by_text(self, page, monkeypatch):
|
||||
import wabot.fields as fields_mod
|
||||
|
||||
monkeypatch.setattr(fields_mod, "Select", MagicMock())
|
||||
field = SelectField(page, accessors=("id", "state"), name="state")
|
||||
field.set_value(value="CO")
|
||||
page.set_select_value.assert_called_with(field.dropdown, value="CO", text=None)
|
||||
field.set_value(text="Colorado")
|
||||
page.set_select_value.assert_called_with(field.dropdown, value=None, text="Colorado")
|
||||
|
||||
|
||||
class TestCheckField:
|
||||
def test_set_and_get_checked(self, page):
|
||||
field = CheckField(page, accessors=("id", "agree"), name="agree")
|
||||
field.set_checked(True)
|
||||
page.set_checkbox.assert_called_once_with(field.el, True)
|
||||
field.get_checked()
|
||||
page.get_checkbox_value.assert_called_once_with(field.el, False)
|
||||
|
||||
|
||||
class TestNullField:
|
||||
def test_is_falsy(self):
|
||||
assert not NullField(name="missing")
|
||||
|
||||
def test_attribute_access_raises_with_element_name(self):
|
||||
with pytest.raises(AttributeError, match="missing"):
|
||||
NullField(name="missing").click()
|
||||
|
||||
def test_pattern_if_el_guards_work(self):
|
||||
el = NullField(name="missing")
|
||||
if el:
|
||||
pytest.fail("NullField must be falsy")
|
||||
Loading…
Reference in New Issue
Block a user