import asyncio import importlib import sys import types from pathlib import Path import fakes import pytest MODULES = Path(__file__).resolve().parents[1] / "modules" def load(): fakes.install() model = importlib.import_module("modules.network.model") package = sys.modules["modules.network"] for name in ("ErrorCode", "NetworkError", "NetworkSnapshot"): setattr(package, name, getattr(model, name)) package.NetworkController = object package.get_network_controller = lambda: CONTROLLER for name, sub in (("modules.settings", "settings"), ("modules.settings.tabs", "settings/tabs")): stub = types.ModuleType(name) stub.__path__ = [str(MODULES / sub)] sys.modules[name] = stub widgets = types.ModuleType("modules.settings.widgets") widgets.CategoryLabel = lambda title: fakes.Fake(label=title) sys.modules["modules.settings.widgets"] = widgets sys.modules.pop("modules.settings.tabs.network", None) return importlib.import_module("modules.settings.tabs.network") from modules.network.model import ( Capabilities, Connectivity, ErrorCode, LinkDetails, LinkState, NetworkError, NetworkSnapshot, Security, WifiNetwork, WifiState, ) CONTROLLER = None class FakeController: def __init__(self, snapshot): self.snapshot = snapshot self.calls = [] self.handlers = {} self.fail = None def connect(self, signal, callback): assert signal == "changed" self.handlers[len(self.handlers) + 1] = callback return len(self.handlers) def disconnect(self, handler_id): self.handlers.pop(handler_id) def push(self, snapshot): self.snapshot = snapshot for callback in list(self.handlers.values()): callback(self) def __getattr__(self, name): async def action(*args): self.calls.append((name, *args)) if self.fail: raise self.fail return action def net(ssid, strength=60, state=LinkState.IDLE, saved=False, security=Security.PSK): return WifiNetwork(ssid, ssid, security, strength, state=state, saved=saved, auto_connect=True if saved else None) def snap(*networks, enabled=True, link=None): return NetworkSnapshot( "iwd", WifiState(True, enabled, False, LinkState.IDLE, tuple(networks), link), (), Connectivity.FULL, Capabilities(forget=True, auto_connect=True, link_details=True), ) def find(root, predicate): return [w for w in root.walk() if predicate(w)] def titles(tab): return [w.label for w in tab.walk() if "net-row-title" in w._classes] async def settle(): for _ in range(3): await asyncio.sleep(0) def mount(tab): async def scenario(): tab.emit("map") await settle() asyncio.run(scenario()) @pytest.fixture def tab_and_controller(): global CONTROLLER CONTROLLER = FakeController(snap(net("zeta", 90), net("home", 40, saved=True), net("cafe", 80))) module = load() return module.NetworkTab(), CONTROLLER def test_initial_render_and_listener_lifecycle(tab_and_controller): tab, controller = tab_and_controller assert titles(tab) == ["home", "zeta", "cafe"] assert controller.handlers == {} async def scenario(): tab.emit("map") assert len(controller.handlers) == 1 tab.emit("map") assert len(controller.handlers) == 1 await settle() assert ("scan",) in controller.calls tab.emit("unmap") assert controller.handlers == {} asyncio.run(scenario()) def test_rows_are_reused_and_moved(tab_and_controller): tab, controller = tab_and_controller mount(tab) row = next(w for w in tab.walk() if type(w).__name__ == "NetworkRow" and w.network.ssid == "cafe") controller.push(snap(net("zeta", 90), net("home", 40, saved=True), net("cafe", 80, LinkState.CONNECTED), link=LinkDetails(frequency_mhz=5180, rx_mbps=300))) assert titles(tab) == ["cafe", "home", "zeta"] same = next(w for w in tab.walk() if type(w).__name__ == "NetworkRow" and w.network.ssid == "cafe") assert same is row assert "connected" in row._classes assert row._facts_box.get_visible() controller.push(snap(net("zeta", 90))) assert titles(tab) == ["zeta"] assert row.get_parent() is None def test_wifi_off_shows_empty_state(tab_and_controller): tab, controller = tab_and_controller mount(tab) controller.push(snap(enabled=False)) assert titles(tab) == [] texts = [w.label for w in tab.walk() if "net-empty-text" in w._classes] assert texts == ["Turn on Wi-Fi to see networks nearby."] def test_click_saved_network_connects(tab_and_controller): tab, controller = tab_and_controller async def scenario(): row = next(w for w in tab.walk() if type(w).__name__ == "NetworkRow" and w.network.ssid == "home") row._on_primary() await settle() asyncio.run(scenario()) assert ("connect_to", "home", None) in controller.calls def test_password_flow(tab_and_controller): tab, controller = tab_and_controller async def scenario(): row = next(w for w in tab.walk() if type(w).__name__ == "NetworkRow" and w.network.ssid == "cafe") row._on_primary() assert row._revealer.reveal_child and row._password_box.get_visible() row._entry.text = "short" row._submit() assert row._error.label.startswith("WPA passwords") assert not [c for c in controller.calls if c[0] == "connect_to"] controller.fail = NetworkError(ErrorCode.WRONG_PASSWORD) row._entry.text = "correct horse" row._submit() await settle() assert ("connect_to", "cafe", "correct horse") in controller.calls assert row._error.label == "Wrong password. Check it and try again." assert row._entry.text == "" assert row._revealer.reveal_child controller.fail = None row._entry.text = "correct horse" row._submit() await settle() assert not row._revealer.reveal_child assert row._error.label == "" asyncio.run(scenario()) def test_cancelled_connect_is_silent(tab_and_controller): tab, controller = tab_and_controller async def scenario(): controller.fail = NetworkError(ErrorCode.CANCELLED) row = next(w for w in tab.walk() if type(w).__name__ == "NetworkRow" and w.network.ssid == "home") row._on_primary() await settle() assert row._error.label in ("", None) or not row._error.get_visible() asyncio.run(scenario()) def test_wifi_switch_and_error_banner(tab_and_controller): tab, controller = tab_and_controller async def scenario(): status = next(w for w in tab.walk() if type(w).__name__ == "StatusCard") controller.fail = NetworkError(ErrorCode.FAILED) status._on_toggle(None, False) await settle() assert ("set_wifi_enabled", False) in controller.calls assert status._banner.get_visible() status.clear_error() assert not status._banner.get_visible() asyncio.run(scenario()) def test_filter_hides_rows(tab_and_controller): tab, _controller = tab_and_controller wifi = next(w for w in tab.walk() if type(w).__name__ == "NetworkList") wifi._set_query("ze") assert titles(tab) == ["zeta"] wifi._set_query("") assert sorted(titles(tab)) == ["cafe", "home", "zeta"]