feat: full implementation - switcher, device watcher, hotkeys, tray, settings UI
This commit is contained in:
63
tests/test_config_manager.py
Normal file
63
tests/test_config_manager.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from config.config_manager import ConfigManager, DEFAULT_CONFIG
|
||||
|
||||
|
||||
def test_load_defaults_when_no_file(tmp_path):
|
||||
cm = ConfigManager(tmp_path / "config.json")
|
||||
assert cm.get()["active_profile"] == "pc1_dp"
|
||||
assert len(cm.get()["profiles"]) == 2
|
||||
|
||||
|
||||
def test_saves_default_config_on_first_run(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
ConfigManager(path)
|
||||
assert path.exists()
|
||||
data = json.loads(path.read_text())
|
||||
assert data["active_profile"] == "pc1_dp"
|
||||
|
||||
|
||||
def test_load_existing_config(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
custom = {"active_profile": "custom", "profiles": [], "device_triggers": [], "general": {}}
|
||||
path.write_text(json.dumps(custom))
|
||||
cm = ConfigManager(path)
|
||||
assert cm.get()["active_profile"] == "custom"
|
||||
|
||||
|
||||
def test_fallback_on_corrupt_config(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text("not json {{{")
|
||||
cm = ConfigManager(path)
|
||||
assert cm.get()["active_profile"] == "pc1_dp"
|
||||
|
||||
|
||||
def test_get_profile_returns_profile(tmp_path):
|
||||
cm = ConfigManager(tmp_path / "config.json")
|
||||
p = cm.get_profile("pc1_dp")
|
||||
assert p is not None
|
||||
assert p["name"] == "PC1 \u2014 DP"
|
||||
|
||||
|
||||
def test_get_profile_returns_none_for_missing(tmp_path):
|
||||
cm = ConfigManager(tmp_path / "config.json")
|
||||
assert cm.get_profile("nonexistent") is None
|
||||
|
||||
|
||||
def test_set_active_profile_persists(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
cm = ConfigManager(path)
|
||||
cm.set_active_profile("pc2_hdmi")
|
||||
cm2 = ConfigManager(path)
|
||||
assert cm2.get_active_profile_id() == "pc2_hdmi"
|
||||
|
||||
|
||||
def test_on_change_callback_fires(tmp_path):
|
||||
cm = ConfigManager(tmp_path / "config.json")
|
||||
called_with = []
|
||||
cm.on_change(lambda cfg: called_with.append(cfg["active_profile"]))
|
||||
cfg = cm.get().copy()
|
||||
cfg["active_profile"] = "pc2_hdmi"
|
||||
cm.update(cfg)
|
||||
assert called_with == ["pc2_hdmi"]
|
||||
62
tests/test_device_watcher.py
Normal file
62
tests/test_device_watcher.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
from config.config_manager import ConfigManager
|
||||
from core.device_watcher import DeviceWatcher
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(tmp_path):
|
||||
return ConfigManager(tmp_path / "config.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def switcher():
|
||||
return MagicMock()
|
||||
|
||||
|
||||
def test_handle_event_connect_triggers_correct_profile(config, switcher):
|
||||
watcher = DeviceWatcher(config, switcher)
|
||||
watcher._handle_event(
|
||||
"HID\\{...}_DEV_VID&0203F0_PID&6343_REV&0413_AABBCCDD", "connect"
|
||||
)
|
||||
time.sleep(0.6)
|
||||
switcher.apply_profile.assert_called_once_with("pc1_dp")
|
||||
|
||||
|
||||
def test_handle_event_disconnect_triggers_correct_profile(config, switcher):
|
||||
watcher = DeviceWatcher(config, switcher)
|
||||
watcher._handle_event(
|
||||
"HID\\{...}_DEV_VID&0203F0_PID&6343_REV&0413_AABBCCDD", "disconnect"
|
||||
)
|
||||
time.sleep(0.6)
|
||||
switcher.apply_profile.assert_called_once_with("pc2_hdmi")
|
||||
|
||||
|
||||
def test_handle_event_ignores_unknown_device(config, switcher):
|
||||
watcher = DeviceWatcher(config, switcher)
|
||||
watcher._handle_event("HID\\VID&DEAD_PID&BEEF", "connect")
|
||||
time.sleep(0.6)
|
||||
switcher.apply_profile.assert_not_called()
|
||||
|
||||
|
||||
def test_handle_event_ignores_disabled_trigger(config, switcher):
|
||||
cfg = config.get()
|
||||
cfg["device_triggers"][0]["enabled"] = False
|
||||
config.update(cfg)
|
||||
|
||||
watcher = DeviceWatcher(config, switcher)
|
||||
watcher._handle_event(
|
||||
"HID\\{...}_DEV_VID&0203F0_PID&6343_REV&0413", "connect"
|
||||
)
|
||||
time.sleep(0.6)
|
||||
switcher.apply_profile.assert_not_called()
|
||||
|
||||
|
||||
def test_debounce_collapses_rapid_events(config, switcher):
|
||||
watcher = DeviceWatcher(config, switcher)
|
||||
device_id = "HID\\{...}_DEV_VID&0203F0_PID&6343_REV&0413"
|
||||
for _ in range(5):
|
||||
watcher._handle_event(device_id, "connect")
|
||||
time.sleep(0.8)
|
||||
assert switcher.apply_profile.call_count == 1
|
||||
67
tests/test_hotkey_manager.py
Normal file
67
tests/test_hotkey_manager.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from config.config_manager import ConfigManager
|
||||
from core.hotkey_manager import HotkeyManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(tmp_path):
|
||||
return ConfigManager(tmp_path / "config.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def switcher():
|
||||
return MagicMock()
|
||||
|
||||
|
||||
def test_register_all_registers_profile_hotkeys(config, switcher):
|
||||
with patch("core.hotkey_manager.keyboard") as mock_kb:
|
||||
hm = HotkeyManager(config, switcher)
|
||||
hm.register_all()
|
||||
assert mock_kb.add_hotkey.call_count == 2
|
||||
calls = [c.args[0] for c in mock_kb.add_hotkey.call_args_list]
|
||||
assert "ctrl+alt+1" in calls
|
||||
assert "ctrl+alt+2" in calls
|
||||
|
||||
|
||||
def test_register_all_skips_empty_hotkeys(config, switcher):
|
||||
cfg = config.get()
|
||||
cfg["profiles"][0]["hotkey"] = ""
|
||||
config.update(cfg)
|
||||
|
||||
with patch("core.hotkey_manager.keyboard") as mock_kb:
|
||||
hm = HotkeyManager(config, switcher)
|
||||
hm.register_all()
|
||||
assert mock_kb.add_hotkey.call_count == 1
|
||||
|
||||
|
||||
def test_unregister_all_removes_hotkeys(config, switcher):
|
||||
with patch("core.hotkey_manager.keyboard") as mock_kb:
|
||||
hm = HotkeyManager(config, switcher)
|
||||
hm.register_all()
|
||||
hm.unregister_all()
|
||||
assert mock_kb.remove_hotkey.call_count == 2
|
||||
|
||||
|
||||
def test_register_all_clears_old_before_registering(config, switcher):
|
||||
with patch("core.hotkey_manager.keyboard") as mock_kb:
|
||||
hm = HotkeyManager(config, switcher)
|
||||
hm.register_all()
|
||||
hm.register_all()
|
||||
assert mock_kb.remove_hotkey.call_count == 2
|
||||
|
||||
|
||||
def test_hotkey_calls_apply_profile(config, switcher):
|
||||
captured_callbacks = {}
|
||||
|
||||
def fake_add_hotkey(hotkey, fn, args):
|
||||
captured_callbacks[hotkey] = (fn, args)
|
||||
|
||||
with patch("core.hotkey_manager.keyboard") as mock_kb:
|
||||
mock_kb.add_hotkey.side_effect = fake_add_hotkey
|
||||
hm = HotkeyManager(config, switcher)
|
||||
hm.register_all()
|
||||
|
||||
fn, args = captured_callbacks["ctrl+alt+1"]
|
||||
fn(*args)
|
||||
switcher.apply_profile.assert_called_once_with("pc1_dp")
|
||||
93
tests/test_switcher.py
Normal file
93
tests/test_switcher.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from config.config_manager import ConfigManager
|
||||
from core.switcher import MonitorSwitcher
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(tmp_path):
|
||||
return ConfigManager(tmp_path / "config.json")
|
||||
|
||||
|
||||
def _make_mock_monitor():
|
||||
m = MagicMock()
|
||||
m.__enter__ = MagicMock(return_value=m)
|
||||
m.__exit__ = MagicMock(return_value=False)
|
||||
return m
|
||||
|
||||
|
||||
def test_apply_profile_sends_vcp_to_both_monitors(config):
|
||||
mon0 = _make_mock_monitor()
|
||||
mon1 = _make_mock_monitor()
|
||||
|
||||
with patch("core.switcher.get_monitors", return_value=[mon0, mon1]):
|
||||
sw = MonitorSwitcher(config)
|
||||
sw.apply_profile("pc1_dp")
|
||||
|
||||
mon0.set_vcp_feature.assert_called_once_with(0x60, 15)
|
||||
mon1.set_vcp_feature.assert_called_once_with(0x60, 15)
|
||||
|
||||
|
||||
def test_apply_profile_hdmi(config):
|
||||
mon0 = _make_mock_monitor()
|
||||
mon1 = _make_mock_monitor()
|
||||
|
||||
with patch("core.switcher.get_monitors", return_value=[mon0, mon1]):
|
||||
sw = MonitorSwitcher(config)
|
||||
sw.apply_profile("pc2_hdmi")
|
||||
|
||||
mon0.set_vcp_feature.assert_called_once_with(0x60, 17)
|
||||
mon1.set_vcp_feature.assert_called_once_with(0x60, 17)
|
||||
|
||||
|
||||
def test_apply_profile_skips_missing_monitor(config):
|
||||
mon0 = _make_mock_monitor()
|
||||
|
||||
with patch("core.switcher.get_monitors", return_value=[mon0]):
|
||||
sw = MonitorSwitcher(config)
|
||||
sw.apply_profile("pc1_dp")
|
||||
|
||||
mon0.set_vcp_feature.assert_called_once_with(0x60, 15)
|
||||
|
||||
|
||||
def test_apply_profile_continues_on_monitor_error(config):
|
||||
mon0 = _make_mock_monitor()
|
||||
mon1 = _make_mock_monitor()
|
||||
mon0.set_vcp_feature.side_effect = Exception("DDC/CI error")
|
||||
|
||||
with patch("core.switcher.get_monitors", return_value=[mon0, mon1]):
|
||||
sw = MonitorSwitcher(config)
|
||||
sw.apply_profile("pc1_dp")
|
||||
|
||||
mon1.set_vcp_feature.assert_called_once_with(0x60, 15)
|
||||
|
||||
|
||||
def test_apply_profile_unknown_profile_does_nothing(config):
|
||||
with patch("core.switcher.get_monitors", return_value=[]) as mock_gm:
|
||||
sw = MonitorSwitcher(config)
|
||||
sw.apply_profile("nonexistent")
|
||||
mock_gm.assert_not_called()
|
||||
|
||||
|
||||
def test_on_switch_callback_fires(config):
|
||||
mon0 = _make_mock_monitor()
|
||||
mon1 = _make_mock_monitor()
|
||||
received = []
|
||||
|
||||
with patch("core.switcher.get_monitors", return_value=[mon0, mon1]):
|
||||
sw = MonitorSwitcher(config)
|
||||
sw.on_switch(lambda pid: received.append(pid))
|
||||
sw.apply_profile("pc2_hdmi")
|
||||
|
||||
assert received == ["pc2_hdmi"]
|
||||
|
||||
|
||||
def test_apply_profile_updates_active_profile(config):
|
||||
mon0 = _make_mock_monitor()
|
||||
mon1 = _make_mock_monitor()
|
||||
|
||||
with patch("core.switcher.get_monitors", return_value=[mon0, mon1]):
|
||||
sw = MonitorSwitcher(config)
|
||||
sw.apply_profile("pc2_hdmi")
|
||||
|
||||
assert config.get_active_profile_id() == "pc2_hdmi"
|
||||
Reference in New Issue
Block a user