from types import SimpleNamespace from unittest.mock import MagicMock import pytest from selenium.common.exceptions import WebDriverException from wabot._browser import Browser from wabot.pacing import HumanPacing, NoPacing from wabot.page import Page class Login(Page): elements = {"username": ("el", ("id", "username"))} def do_login(self): return "logged-in" def explode(self): raise WebDriverException("browser gone") class Unverifiable(Page): def verify(self): return False @pytest.fixture def bot(): return Browser(MagicMock(name="driver"), pacing=NoPacing()) class TestConstruction: def test_good_flag_starts_true(self, bot): assert bot.good is True def test_default_pacing_is_human(self): assert isinstance(Browser(MagicMock()).pacing, HumanPacing) class TestSetPage: def test_sets_and_returns_true(self, bot): assert bot.set_page(Login) is True assert isinstance(bot.page, Login) def test_failed_verify_refuses_switch(self, bot): bot.set_page(Login) assert bot.set_page(Unverifiable) is False assert isinstance(bot.page, Login) # unchanged class TestDelegation: def test_getattr_delegates_to_page(self, bot): bot.set_page(Login) assert bot.do_login() == "logged-in" def test_getitem_delegates_to_page(self, bot): bot.set_page(Login) el = bot["username"] assert el.name == "username" def test_getattr_without_page_raises(self, bot): with pytest.raises(AttributeError): _ = bot.do_login def test_direct_delegation_does_not_flip_good(self, bot): # only perform() traps+flips; direct bot.x() is the raw path bot.set_page(Login) with pytest.raises(WebDriverException): bot.explode() assert bot.good is True def test_getitem_without_page_raises_friendly(self, bot): with pytest.raises(RuntimeError, match="no current page"): bot["username"] def test_repr_shows_page_and_good(self, bot): assert "page=None" in repr(bot) and "good=True" in repr(bot) bot.set_page(Login) assert "page=Login" in repr(bot) class TestPerform: def test_returns_method_result(self, bot): bot.set_page(Login) assert bot.perform("do_login") == "logged-in" def test_missing_method_returns_none(self, bot): bot.set_page(Login) assert bot.perform("nope") is None def test_webdriver_exception_flips_good(self, bot): bot.set_page(Login) assert bot.perform("explode") is None assert bot.good is False def test_broken_state_refuses_actions_until_reset(self, bot): bot.set_page(Login) bot.perform("explode") assert bot.perform("do_login") is None # refused assert bot.do_login() is None # __getattr__ path also refused bot.reset() assert bot.perform("do_login") == "logged-in" def test_broken_state_attribute_read_returns_noop_callable(self, bot): # documents the footgun: while broken, an attribute read yields the # no-op refusal callable, not the underlying value bot.set_page(Login) bot.perform("explode") # flips good=False assert callable(bot.anything) class TestQuit: def test_quit_quits_driver_and_removes_session(self): driver, store = MagicMock(), MagicMock() store.get.return_value = SimpleNamespace(service_pid=None) # external: nothing to stop bot = Browser(driver, session_name="s1", store=store) bot.quit() driver.quit.assert_called_once_with() store.remove.assert_called_once_with("s1") def test_quit_without_session_only_quits(self): driver = MagicMock() Browser(driver).quit() driver.quit.assert_called_once_with() def test_quit_stops_managed_service(self, monkeypatch): import wabot._browser as browser_mod stopped = [] monkeypatch.setattr(browser_mod, "stop_service", lambda pid: stopped.append(pid)) driver, store = MagicMock(), MagicMock() store.get.return_value = SimpleNamespace(service_pid=4321) # managed session bot = Browser(driver, session_name="s1", store=store) bot.quit() assert stopped == [4321] store.remove.assert_called_once_with("s1") def test_quit_removes_record_even_if_driver_quit_raises(self, monkeypatch): import wabot._browser as browser_mod monkeypatch.setattr(browser_mod, "stop_service", lambda pid: None) driver, store = MagicMock(), MagicMock() driver.quit.side_effect = WebDriverException("boom") store.get.return_value = SimpleNamespace(service_pid=None) bot = Browser(driver, session_name="s1", store=store) with pytest.raises(WebDriverException): bot.quit() store.remove.assert_called_once_with("s1") # finally block still ran