from unittest.mock import MagicMock from selenium.common.exceptions import NoSuchElementException import wabot.page as page_mod from wabot.fields import CheckField, NullField, SelectField, TextField from wabot.pacing import HumanPacing, NoPacing from wabot.page import Page class BasePage(Page): elements = { "username": ("el", ("id", "username")), "state": ("select", ("id", "state")), "agree": ("checkbox", ("id", "agree")), "rows": ("els", ("css selector", "tr.row")), } class ChildPage(BasePage): elements = { "username": ("el", ("id", "user_name_v2")), # override "extra": ("el", ("id", "extra")), } def make_browser(): browser = MagicMock(name="browser") browser.pacing = NoPacing() return browser class TestElementResolution: def test_own_elements_found(self): page = BasePage(make_browser()) assert page.find_element_locators("username") == ("el", ("id", "username")) def test_mro_walk_inherits_parent_elements(self): page = ChildPage(make_browser()) assert page.find_element_locators("state") == ("select", ("id", "state")) assert page.find_element_locators("extra") == ("el", ("id", "extra")) def test_child_overrides_parent(self): page = ChildPage(make_browser()) assert page.find_element_locators("username") == ("el", ("id", "user_name_v2")) def test_missing_key_returns_none(self): assert BasePage(make_browser()).find_element_locators("nope") is None def test_empty_override_still_inherits_ancestors(self): class Grandchild(ChildPage): elements = {} # declares nothing; must still resolve ancestor keys page = Grandchild(make_browser()) assert page.find_element_locators("state") == ("select", ("id", "state")) assert page.find_element_locators("extra") == ("el", ("id", "extra")) class TestGetProxy: def test_typed_dispatch(self): page = BasePage(make_browser()) assert isinstance(page["username"], TextField) assert isinstance(page["agree"], CheckField) def test_select_dispatch(self, monkeypatch): import wabot.fields as fields_mod monkeypatch.setattr(fields_mod, "Select", MagicMock()) page = BasePage(make_browser()) assert isinstance(page["state"], SelectField) def test_els_returns_find_elements_result(self): browser = make_browser() page = BasePage(browser) result = page["rows"] browser.driver.find_elements.assert_called_once_with(by="css selector", value="tr.row") assert result is browser.driver.find_elements.return_value def test_unknown_key_returns_falsy_nullfield(self): el = BasePage(make_browser())["nope"] assert isinstance(el, NullField) assert not el def test_element_not_on_page_returns_nullfield(self): browser = make_browser() browser.driver.find_element.side_effect = NoSuchElementException("gone") el = BasePage(browser)["username"] assert isinstance(el, NullField) def test_custom_field_class_dispatch(self): class MyField(TextField): pass class CustomPage(Page): elements = {"thing": (MyField, ("id", "thing"))} assert isinstance(CustomPage(make_browser())["thing"], MyField) def test_malformed_locator_missing_accessors_returns_nullfield(self): class BadPage(Page): elements = {"x": ("el",)} # missing the accessors half el = BadPage(make_browser())["x"] assert isinstance(el, NullField) assert not el def test_malformed_accessors_arity_returns_nullfield(self): class BadPage(Page): elements = {"x": ("els", ("id",))} # accessors not a (by, value) pair assert isinstance(BadPage(make_browser())["x"], NullField) def test_unknown_type_returns_nullfield(self): class BadPage(Page): elements = {"x": ("selct", ("id", "x"))} # typo'd type string assert isinstance(BadPage(make_browser())["x"], NullField) class TestVerify: def test_default_verify_is_true(self): assert BasePage(make_browser()).verify() is True class PlainPage(Page): elements = {} def make_element(width=100, height=30, displayed=True, enabled=True): el = MagicMock(name="element") el.size = {"width": width, "height": height} el.is_displayed.return_value = displayed el.is_enabled.return_value = enabled return el class TestClick: def test_refuses_null_element(self): assert PlainPage(make_browser()).click(None) is False assert PlainPage(make_browser()).click(NullField(name="x")) is False def test_plain_click_when_pacing_gives_no_offset(self): page = PlainPage(make_browser()) # NoPacing -> click_offset None el = make_element() assert page.click(el) is True el.click.assert_called_once_with() def test_offset_click_uses_action_chains(self, monkeypatch): browser = make_browser() browser.pacing = HumanPacing() chains = MagicMock(name="ActionChains") monkeypatch.setattr(page_mod, "ActionChains", chains) page = PlainPage(browser) el = make_element(width=100, height=40) assert page.click(el) is True chains.assert_called_once_with(browser.driver) args = chains.return_value.move_to_element_with_offset.call_args.args assert args[0] is el # offsets are center-relative and must stay inside the element assert -49 <= args[1] <= 49 and -19 <= args[2] <= 19 chains.return_value.move_to_element_with_offset.return_value.click.return_value.perform.assert_called_once() def test_never_clickable_element_fails(self): page = PlainPage(make_browser()) el = make_element(displayed=False) assert page.click(el, clickable_timeout=0.3) is False el.click.assert_not_called() class TestFormHelpers: def test_set_el_value_clears_types_and_verifies(self): page = PlainPage(make_browser()) el = make_element() el.get_attribute.return_value = "mathew" assert page.set_el_value(el, "mathew") is True el.clear.assert_called_once_with() el.send_keys.assert_called_once_with("mathew") def test_set_el_value_returns_false_on_mismatch(self): page = PlainPage(make_browser()) el = make_element() el.get_attribute.return_value = "wrong" assert page.set_el_value(el, "mathew") is False def test_set_el_value_none_just_clears(self): page = PlainPage(make_browser()) el = make_element() assert page.set_el_value(el, None) is True el.clear.assert_called_once_with() el.send_keys.assert_not_called() def test_get_el_value_and_text_handle_null(self): page = PlainPage(make_browser()) assert page.get_el_value(None) is None assert page.get_el_text(None) is None def test_set_checkbox_clicks_only_when_state_differs(self): page = PlainPage(make_browser()) el = make_element() el.is_selected.return_value = False page.set_checkbox(el, True) el.click.assert_called_once() el.click.reset_mock() el.is_selected.return_value = True page.set_checkbox(el, True) el.click.assert_not_called() class TestStaleness: def test_fresh_element_is_not_stale(self): page = PlainPage(make_browser()) assert page.is_element_stale(make_element()) is False def test_stale_element_is_detected(self): from selenium.common.exceptions import StaleElementReferenceException class StaleEl: # NOT a MagicMock: mutating type(mock) would poison every mock @property def tag_name(self): raise StaleElementReferenceException("stale") page = PlainPage(make_browser()) 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")