Files
InputRemapper/tests/unit/test_app_binding.py
Blomios 9ef2c1439c feat(app-binding): lier les presets aux applications
Ajoute la configuration des associations application/preset, le service de suivi du focus, l'intégration GUI et les fichiers d'installation nécessaires.

Ajoute les tests unitaires ciblés des bindings, du service de focus, des migrations et des composants GUI associés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 23:52:34 +02:00

155 lines
5.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
#
# 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 <https://www.gnu.org/licenses/>.
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()