Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import tempfile
import unittest
import unittest.mock
from types import SimpleNamespace
from unittest.mock import patch

Expand Down Expand Up @@ -1273,5 +1274,79 @@ def test_set_start_minimized_does_not_call_apply_login_startup(self):
self.assertFalse(backend.startMinimized)


@unittest.skipIf(Backend is None, "PySide6 not installed in test environment")
class BackendHandleDpiReadTests(unittest.TestCase):
"""Device-reported DPI must persist to ``config.json`` so a hardware
DPI change taken on the mouse survives the next Mouser restart, and
must clamp into the connected device's range to defend against
bogus reports."""

def setUp(self) -> None:
self._save_mock = unittest.mock.MagicMock()
self._patches = (
patch("ui.backend.save_config", self._save_mock),
patch("ui.backend.supports_login_startup", return_value=False),
)
for p in self._patches:
p.start()
self.addCleanup(self._stop_patches)

def _stop_patches(self) -> None:
for p in self._patches:
p.stop()

def _build(self, *, cfg=None, engine=None):
loaded = copy.deepcopy(cfg or DEFAULT_CONFIG)
with patch("ui.backend.load_config", return_value=loaded):
backend = Backend(engine=engine)
self._save_mock.reset_mock()
return backend

def test_persists_new_device_dpi_to_disk(self):
backend = self._build()
backend._handleDpiRead(2400)
self.assertEqual(backend._cfg["settings"]["dpi"], 2400)
self._save_mock.assert_called_once_with(backend._cfg)

def test_clamps_overrange_dpi_to_device_max(self):
device = SimpleNamespace(dpi_min=200, dpi_max=4000)
engine = _FakeEngine(device_connected=True, connected_device=device)
backend = self._build(engine=engine)
backend._handleDpiRead(99999)
self.assertEqual(backend._cfg["settings"]["dpi"], 4000)
self._save_mock.assert_called_once_with(backend._cfg)

def test_clamps_underrange_dpi_to_device_min(self):
device = SimpleNamespace(dpi_min=400, dpi_max=8000)
engine = _FakeEngine(device_connected=True, connected_device=device)
backend = self._build(engine=engine)
backend._handleDpiRead(50)
self.assertEqual(backend._cfg["settings"]["dpi"], 400)
self._save_mock.assert_called_once_with(backend._cfg)

def test_no_change_skips_save(self):
cfg = copy.deepcopy(DEFAULT_CONFIG)
cfg["settings"]["dpi"] = 1500
backend = self._build(cfg=cfg)
backend._handleDpiRead(1500)
self._save_mock.assert_not_called()

def test_syncs_engine_cached_config(self):
engine = _FakeEngine(device_connected=True)
backend = self._build(engine=engine)
backend._handleDpiRead(1800)
self.assertEqual(engine.cfg["settings"]["dpi"], 1800)

def test_emits_dpi_from_device_with_clamped_value(self):
_ensure_qapp()
device = SimpleNamespace(dpi_min=200, dpi_max=4000)
engine = _FakeEngine(device_connected=True, connected_device=device)
backend = self._build(engine=engine)
seen = []
backend.dpiFromDevice.connect(seen.append)
backend._handleDpiRead(9999)
QCoreApplication.processEvents()
self.assertEqual(seen, [4000])

if __name__ == "__main__":
unittest.main()
27 changes: 24 additions & 3 deletions ui/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1640,10 +1640,31 @@ def _handleProfileSwitch(self, profile_name):

@Slot(int)
def _handleDpiRead(self, dpi):
"""Runs on Qt main thread."""
self._cfg.setdefault("settings", {})["dpi"] = dpi
"""Runs on Qt main thread.

A device-reported DPI is authoritative for "what the hardware is
currently set to" -- the user expects Mouser to keep showing the
same value across restarts rather than reverting to a stale
preference whenever the engine reads the device. Clamp the
incoming value, persist it, and keep the engine's cached config
in sync so subsequent reads do not loop through a stale picture.

Skip the engine push (``set_dpi``) here: this handler is reacting
to a value the device already reports, so echoing it back would
be a redundant HID round-trip.
"""
device = self._resolved_connected_device()
clamped = clamp_dpi(dpi, device)
settings = self._cfg.setdefault("settings", {})
if settings.get("dpi") == clamped:
self.dpiFromDevice.emit(clamped)
return
settings["dpi"] = clamped
save_config(self._cfg)
if self._engine:
self._engine.cfg = self._cfg
self.settingsChanged.emit()
self.dpiFromDevice.emit(dpi)
self.dpiFromDevice.emit(clamped)

@Slot(bool)
def _handleConnectionChange(self, connected):
Expand Down
Loading