fix: set_checkbox propagates click failure; coerce set_el_value; cover alerts/selects

This commit is contained in:
Mathew Sir Guest the best 2026-07-07 23:12:11 -06:00
parent c99b9509f4
commit 2adb7b2236
3 changed files with 164 additions and 15 deletions

@ -1815,13 +1815,12 @@ git commit -m "feat: Page element maps with MRO inheritance and typed dispatch"
- [ ] **Step 1: Write the failing tests** - [ ] **Step 1: Write the failing tests**
Append to `tests/unit/test_page.py`: Add `import wabot.page as page_mod` to the top-of-file import block and fold
`HumanPacing` into the existing `from wabot.pacing import NoPacing` (keeping the
imports at the top avoids ruff `E402`), then append these test classes to
`tests/unit/test_page.py`:
```python ```python
import wabot.page as page_mod
from wabot.pacing import HumanPacing
class PlainPage(Page): class PlainPage(Page):
elements = {} elements = {}
@ -1921,6 +1920,79 @@ class TestStaleness:
page = PlainPage(make_browser()) page = PlainPage(make_browser())
assert page.is_element_stale(StaleEl()) is True assert page.is_element_stale(StaleEl()) is True
class TestAlert:
def test_accept_alert_accepts_and_returns_text(self, monkeypatch):
browser = make_browser()
page = PlainPage(browser)
monkeypatch.setattr(page_mod, "WebDriverWait", MagicMock()) # until() won't raise
alert = browser.driver.switch_to.alert
alert.text = "confirm?"
assert page.accept_alert() == "confirm?"
alert.accept.assert_called_once_with()
def test_accept_alert_can_dismiss(self, monkeypatch):
browser = make_browser()
page = PlainPage(browser)
monkeypatch.setattr(page_mod, "WebDriverWait", MagicMock())
alert = browser.driver.switch_to.alert
alert.text = "confirm?"
page.accept_alert(accept=False)
alert.dismiss.assert_called_once_with()
def test_accept_alert_returns_false_when_no_alert(self, monkeypatch):
from selenium.common.exceptions import TimeoutException
page = PlainPage(make_browser())
wait = MagicMock()
wait.return_value.until.side_effect = TimeoutException()
monkeypatch.setattr(page_mod, "WebDriverWait", wait)
assert page.accept_alert(timeout=0) is False
class TestSelectHelpers:
def test_get_select_value(self):
select = MagicMock()
select.first_selected_option.get_attribute.return_value = "CO"
assert PlainPage(make_browser()).get_select_value(select) == "CO"
def test_get_select_value_null_returns_none(self):
assert PlainPage(make_browser()).get_select_value(None) is None
def test_set_select_value_by_value_including_zero(self):
# legacy `if value:` skipped a valid 0/"" — verify the fix
select = MagicMock()
assert PlainPage(make_browser()).set_select_value(select, value=0) is True
select.select_by_value.assert_called_once_with("0")
def test_set_select_value_by_text(self):
select = MagicMock()
assert PlainPage(make_browser()).set_select_value(select, text="Colorado") is True
select.select_by_visible_text.assert_called_once_with("Colorado")
class TestCheckboxAndCoercion:
def test_set_checkbox_returns_false_when_click_fails(self, monkeypatch):
page = PlainPage(make_browser())
el = make_element()
el.is_enabled.return_value = True
el.is_selected.return_value = False # needs toggling
monkeypatch.setattr(page, "click", lambda e: False) # click fails
assert page.set_checkbox(el, True) is False
def test_set_checkbox_disabled_returns_false(self):
page = PlainPage(make_browser())
el = make_element()
el.is_enabled.return_value = False
assert page.set_checkbox(el, True) is False
def test_set_el_value_coerces_non_str(self):
page = PlainPage(make_browser())
el = make_element()
el.get_attribute.return_value = "42"
assert page.set_el_value(el, 42) is True
el.send_keys.assert_called_once_with("42")
``` ```
- [ ] **Step 2: Run tests to verify they fail** - [ ] **Step 2: Run tests to verify they fail**
@ -1965,13 +2037,13 @@ Append these methods to `class Page`:
if not el: if not el:
LOGGER.warning("refusing to click null element") LOGGER.warning("refusing to click null element")
return False return False
if not self._wait_clickable(el, clickable_timeout):
return False
try: try:
size = el.size size = el.size
except StaleElementReferenceException: except StaleElementReferenceException:
LOGGER.error("failed to click element: stale reference") LOGGER.error("failed to click element: stale reference")
return False return False
if not self._wait_clickable(el, clickable_timeout):
return False
offset = self.pacing.click_offset(size["width"], size["height"]) offset = self.pacing.click_offset(size["width"], size["height"])
try: try:
if offset is None: if offset is None:
@ -2067,11 +2139,13 @@ Append these methods to `class Page`:
el.send_keys(char) el.send_keys(char)
time.sleep(self.pacing.delay("text") / 10.0) time.sleep(self.pacing.delay("text") / 10.0)
else: else:
el.send_keys(value) el.send_keys(str(value))
except WebDriverException as ex: except WebDriverException as ex:
LOGGER.error("failed to send keys, element in unknown state: %s", ex) LOGGER.error("failed to send keys, element in unknown state: %s", ex)
return False return False
actual = self.get_el_value(el) actual = self.get_el_value(el)
# verify the field holds exactly what we typed (strict: fields that
# normalize input will read back as a mismatch)
if str(actual) != str(value): if str(actual) != str(value):
LOGGER.error("field value mismatch: wanted %r, field has %r", value, actual) LOGGER.error("field value mismatch: wanted %r, field has %r", value, actual)
return False return False
@ -2106,7 +2180,7 @@ Append these methods to `class Page`:
if not el.is_enabled(): if not el.is_enabled():
return False return False
if el.is_selected() != checked: if el.is_selected() != checked:
self.click(el) return self.click(el) # propagate: a failed click is a failed set
return True return True
def get_checkbox_value(self, el, ignore_disabled: bool = False): def get_checkbox_value(self, el, ignore_disabled: bool = False):
@ -2135,7 +2209,7 @@ Append these methods to `class Page`:
- [ ] **Step 4: Run tests to verify they pass** - [ ] **Step 4: Run tests to verify they pass**
Run: `uv run pytest tests/unit/test_page.py -v` Run: `uv run pytest tests/unit/test_page.py -v`
Expected: 26 passed (15 from Task 8 + 11 new) Expected: 36 passed (15 from Task 8 + 21 new)
- [ ] **Step 5: Commit** - [ ] **Step 5: Commit**
@ -2875,7 +2949,7 @@ Run: `uv run pytest tests/unit/test_api.py -v`
Expected: 11 passed Expected: 11 passed
Run: `uv run pytest` Run: `uv run pytest`
Expected: full unit suite passes (116 tests: pacing 7, sessions 14, hosts 19, reattach 6, fields 17, page 26, screenshot 3, browser 13, api 11) Expected: full unit suite passes (126 tests: pacing 7, sessions 14, hosts 19, reattach 6, fields 17, page 36, screenshot 3, browser 13, api 11)
- [ ] **Step 5: Commit** - [ ] **Step 5: Commit**

@ -113,13 +113,13 @@ class Page:
if not el: if not el:
LOGGER.warning("refusing to click null element") LOGGER.warning("refusing to click null element")
return False return False
if not self._wait_clickable(el, clickable_timeout):
return False
try: try:
size = el.size size = el.size
except StaleElementReferenceException: except StaleElementReferenceException:
LOGGER.error("failed to click element: stale reference") LOGGER.error("failed to click element: stale reference")
return False return False
if not self._wait_clickable(el, clickable_timeout):
return False
offset = self.pacing.click_offset(size["width"], size["height"]) offset = self.pacing.click_offset(size["width"], size["height"])
try: try:
if offset is None: if offset is None:
@ -215,11 +215,13 @@ class Page:
el.send_keys(char) el.send_keys(char)
time.sleep(self.pacing.delay("text") / 10.0) time.sleep(self.pacing.delay("text") / 10.0)
else: else:
el.send_keys(value) el.send_keys(str(value))
except WebDriverException as ex: except WebDriverException as ex:
LOGGER.error("failed to send keys, element in unknown state: %s", ex) LOGGER.error("failed to send keys, element in unknown state: %s", ex)
return False return False
actual = self.get_el_value(el) actual = self.get_el_value(el)
# verify the field holds exactly what we typed (strict: fields that
# normalize input will read back as a mismatch)
if str(actual) != str(value): if str(actual) != str(value):
LOGGER.error("field value mismatch: wanted %r, field has %r", value, actual) LOGGER.error("field value mismatch: wanted %r, field has %r", value, actual)
return False return False
@ -254,7 +256,7 @@ class Page:
if not el.is_enabled(): if not el.is_enabled():
return False return False
if el.is_selected() != checked: if el.is_selected() != checked:
self.click(el) return self.click(el) # propagate: a failed click is a failed set
return True return True
def get_checkbox_value(self, el, ignore_disabled: bool = False): def get_checkbox_value(self, el, ignore_disabled: bool = False):

@ -223,3 +223,76 @@ class TestStaleness:
page = PlainPage(make_browser()) page = PlainPage(make_browser())
assert page.is_element_stale(StaleEl()) is True assert page.is_element_stale(StaleEl()) is True
class TestAlert:
def test_accept_alert_accepts_and_returns_text(self, monkeypatch):
browser = make_browser()
page = PlainPage(browser)
monkeypatch.setattr(page_mod, "WebDriverWait", MagicMock()) # until() won't raise
alert = browser.driver.switch_to.alert
alert.text = "confirm?"
assert page.accept_alert() == "confirm?"
alert.accept.assert_called_once_with()
def test_accept_alert_can_dismiss(self, monkeypatch):
browser = make_browser()
page = PlainPage(browser)
monkeypatch.setattr(page_mod, "WebDriverWait", MagicMock())
alert = browser.driver.switch_to.alert
alert.text = "confirm?"
page.accept_alert(accept=False)
alert.dismiss.assert_called_once_with()
def test_accept_alert_returns_false_when_no_alert(self, monkeypatch):
from selenium.common.exceptions import TimeoutException
page = PlainPage(make_browser())
wait = MagicMock()
wait.return_value.until.side_effect = TimeoutException()
monkeypatch.setattr(page_mod, "WebDriverWait", wait)
assert page.accept_alert(timeout=0) is False
class TestSelectHelpers:
def test_get_select_value(self):
select = MagicMock()
select.first_selected_option.get_attribute.return_value = "CO"
assert PlainPage(make_browser()).get_select_value(select) == "CO"
def test_get_select_value_null_returns_none(self):
assert PlainPage(make_browser()).get_select_value(None) is None
def test_set_select_value_by_value_including_zero(self):
# legacy `if value:` skipped a valid 0/"" — verify the fix
select = MagicMock()
assert PlainPage(make_browser()).set_select_value(select, value=0) is True
select.select_by_value.assert_called_once_with("0")
def test_set_select_value_by_text(self):
select = MagicMock()
assert PlainPage(make_browser()).set_select_value(select, text="Colorado") is True
select.select_by_visible_text.assert_called_once_with("Colorado")
class TestCheckboxAndCoercion:
def test_set_checkbox_returns_false_when_click_fails(self, monkeypatch):
page = PlainPage(make_browser())
el = make_element()
el.is_enabled.return_value = True
el.is_selected.return_value = False # needs toggling
monkeypatch.setattr(page, "click", lambda e: False) # click fails
assert page.set_checkbox(el, True) is False
def test_set_checkbox_disabled_returns_false(self):
page = PlainPage(make_browser())
el = make_element()
el.is_enabled.return_value = False
assert page.set_checkbox(el, True) is False
def test_set_el_value_coerces_non_str(self):
page = PlainPage(make_browser())
el = make_element()
el.get_attribute.return_value = "42"
assert page.set_el_value(el, 42) is True
el.send_keys.assert_called_once_with("42")