#!/usr/bin/env python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings # Copyright (C) 2025 sezanzeb # # This file is part of input-remapper. # # input-remapper is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # input-remapper is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . import unittest try: from pydantic.v1 import ValidationError except ImportError: from pydantic import ValidationError # type: ignore[assignment, no-redef] from inputremapper.configs.app_binding import ( AppBinding, BoundPreset, MatchType, normalize_app_id, ) from inputremapper.focus.focus_backend import FocusEvent from tests.lib.test_setup import test_setup def _event(app_id="", title="", backend="fake") -> FocusEvent: return FocusEvent(app_id=app_id, title=title, backend=backend) @test_setup class TestNormalizeAppId(unittest.TestCase): def test_lower_and_strip(self): self.assertEqual(normalize_app_id(" FireFox "), "firefox") self.assertEqual(normalize_app_id("ALACRITTY"), "alacritty") self.assertEqual(normalize_app_id(""), "") @test_setup class TestBoundPreset(unittest.TestCase): def test_valid(self): bound = BoundPreset(group_key="Foo Device", preset="preset1") self.assertEqual(bound.group_key, "Foo Device") self.assertEqual(bound.preset, "preset1") def test_empty_group_key_rejected(self): with self.assertRaises(ValidationError): BoundPreset(group_key="", preset="preset1") with self.assertRaises(ValidationError): BoundPreset(group_key=" ", preset="preset1") def test_empty_preset_rejected(self): with self.assertRaises(ValidationError): BoundPreset(group_key="Foo Device", preset="") with self.assertRaises(ValidationError): BoundPreset(group_key="Foo Device", preset=" ") @test_setup class TestAppBinding(unittest.TestCase): def test_defaults(self): binding = AppBinding(app_id="firefox") self.assertEqual(binding.match, MatchType.wm_class) self.assertEqual(binding.presets, []) def test_default_presets_are_not_shared(self): binding_a = AppBinding(app_id="firefox") binding_b = AppBinding(app_id="chromium") binding_a.presets.append(BoundPreset(group_key="Foo Device", preset="preset1")) self.assertEqual(binding_b.presets, []) def test_empty_app_id_rejected(self): with self.assertRaises(ValidationError): AppBinding(app_id="") with self.assertRaises(ValidationError): AppBinding(app_id=" ") def test_invalid_regex_rejected(self): with self.assertRaises(ValidationError): AppBinding(app_id="(unclosed", match=MatchType.title_regex) def test_invalid_regex_allowed_for_wm_class(self): # an unbalanced parenthesis is a fine literal wm_class, only title_regex # compiles it as a pattern. binding = AppBinding(app_id="(unclosed", match=MatchType.wm_class) self.assertEqual(binding.app_id, "(unclosed") def test_with_presets(self): binding = AppBinding( app_id="firefox", presets=[BoundPreset(group_key="Foo Device", preset="preset1")], ) self.assertEqual(len(binding.presets), 1) self.assertEqual(binding.presets[0].group_key, "Foo Device") # -- matches() --------------------------------------------------------- def test_matches_wm_class_normalized(self): binding = AppBinding(app_id="FireFox", match=MatchType.wm_class) self.assertTrue(binding.matches(_event(app_id="firefox"))) self.assertTrue(binding.matches(_event(app_id=" FIREFOX "))) self.assertFalse(binding.matches(_event(app_id="chromium"))) def test_matches_wm_class_ignores_title(self): binding = AppBinding(app_id="firefox", match=MatchType.wm_class) self.assertFalse( binding.matches(_event(app_id="chromium", title="firefox - page")) ) def test_matches_title_regex(self): binding = AppBinding(app_id="Privat.*Browsing", match=MatchType.title_regex) self.assertTrue( binding.matches(_event(app_id="firefox", title="Private Browsing")) ) self.assertFalse( binding.matches(_event(app_id="firefox", title="Normal Window")) ) def test_matches_title_regex_searches_substring(self): binding = AppBinding(app_id="vim", match=MatchType.title_regex) # re.search, not fullmatch self.assertTrue(binding.matches(_event(title="file.py - nvim"))) def test_matches_title_regex_case_sensitive(self): # unlike wm_class, the title regex is not normalized binding = AppBinding(app_id="Private", match=MatchType.title_regex) self.assertFalse(binding.matches(_event(title="private browsing"))) def test_roundtrip_through_json(self): binding = AppBinding( app_id="firefox", match=MatchType.wm_class, presets=[BoundPreset(group_key="Foo Device", preset="preset1")], ) import json as_dict = json.loads(binding.json()) restored = AppBinding(**as_dict) self.assertEqual(restored, binding) if __name__ == "__main__": unittest.main()