mirror of
https://git.zavage.net/Zavage-Software/wabot.git
synced 2026-07-21 13:06:08 -06:00
fix: set_checkbox propagates click failure; coerce set_el_value; cover alerts/selects
This commit is contained in:
parent
c99b9509f4
commit
2adb7b2236
@ -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")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user