Files
InputRemapper/tests/system/gui/test_gui.py
Blomios 0ab77d2a64 chore(repo): baseline du fork input-remapper (upstream v2.2.1)
Le dépôt ne contenait que README.md ; ajout de l'intégralité du code
fonctionnel du fork comme socle stable de la branche de release.
L'état runtime d'orchestration (.ideai/) est exclu du suivi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:47:12 +02:00

1620 lines
60 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 os
import time
import unittest
from typing import Tuple, Iterable
from unittest.mock import patch, MagicMock, call
import evdev
import gi
from evdev.ecodes import (
EV_KEY,
EV_ABS,
KEY_LEFTSHIFT,
KEY_A,
KEY_Q,
EV_REL,
)
from inputremapper.input_event import InputEvent
from tests.system.gui.test_components import FlowBoxTestUtils
from tests.lib.fixtures import fixtures
from tests.lib.logger import logger
from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe
from tests.lib.spy import spy
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GtkSource", "4")
gi.require_version("GLib", "2.0")
from gi.repository import Gtk
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.paths import PathUtils
from inputremapper.gui.messages.message_broker import (
MessageType,
)
from inputremapper.gui.messages.message_data import StatusData, CombinationRecorded
from inputremapper.gui.components.editor import (
MappingSelectionLabel,
SET_KEY_FIRST,
CodeEditor,
)
from inputremapper.gui.components.device_groups import DeviceGroupEntry
from inputremapper.gui.utils import gtk_iteration
from inputremapper.injection.injector import InjectorState
from inputremapper.configs.input_config import InputCombination, InputConfig
from tests.lib.test_setup import test_setup
from tests.system.gui.gui_test_base import GuiTestBase, patch_confirm_delete
@test_setup
class TestGui(GuiTestBase):
"""For tests that use the window.
It is intentional that there is no access to the Components.
Try to modify the configuration only by calling functions of the window.
For example by simulating clicks on buttons. Get the widget to interact with
by going through the windows children. (See click_on_group for inspiration)
"""
def click_on_group(self, group_key: str):
for child in self.device_selection.get_children():
device_group_entry = child.get_children()[0]
if device_group_entry.group_key == group_key:
device_group_entry.set_active(True)
def test_can_start(self):
self.assertIsNotNone(self.user_interface)
self.assertTrue(self.user_interface.window.get_visible())
def assert_gui_clean(self):
selection_labels = self.selection_label_listbox.get_children()
self.assertEqual(len(selection_labels), 0)
self.assertEqual(len(self.data_manager.active_preset), 0)
self.assertEqual(
FlowBoxTestUtils.get_active_entry(self.preset_selection).name, "new preset"
)
self.assertEqual(self.recording_toggle.get_label(), "Record")
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
def test_initial_state(self):
self.assertEqual(self.data_manager.active_group.key, "Foo Device")
self.assertEqual(
FlowBoxTestUtils.get_active_entry(self.device_selection).name, "Foo Device"
)
self.assertEqual(self.data_manager.active_preset.name, "preset3")
self.assertEqual(
FlowBoxTestUtils.get_active_entry(self.preset_selection).name, "preset3"
)
self.assertFalse(self.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active())
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination([InputConfig(type=1, code=5)]),
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
[
InputConfig(type=1, code=5),
]
),
)
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "4")
self.assertIsNone(self.data_manager.active_mapping.name)
self.assertTrue(self.data_manager.active_mapping.is_valid())
self.assertTrue(self.data_manager.active_preset.is_valid())
# todo
def test_set_autoload_refreshes_service_config(self):
self.assertFalse(self.data_manager.get_autoload())
with spy(self.daemon, "set_config_dir") as set_config_dir:
self.autoload_toggle.set_active(True)
gtk_iteration()
set_config_dir.assert_called_once()
self.assertTrue(self.data_manager.get_autoload())
def test_autoload_sets_correctly(self):
self.assertFalse(self.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active())
self.autoload_toggle.set_active(True)
gtk_iteration()
self.assertTrue(self.data_manager.get_autoload())
self.assertTrue(self.autoload_toggle.get_active())
self.autoload_toggle.set_active(False)
gtk_iteration()
self.assertFalse(self.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active())
def test_autoload_is_set_when_changing_preset(self):
self.assertFalse(self.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active())
self.click_on_group("Foo Device 2")
FlowBoxTestUtils.set_active(self.preset_selection, "preset2")
gtk_iteration()
self.assertTrue(self.data_manager.get_autoload())
self.assertTrue(self.autoload_toggle.get_active())
def test_only_one_autoload_per_group(self):
self.assertFalse(self.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active())
self.click_on_group("Foo Device 2")
FlowBoxTestUtils.set_active(self.preset_selection, "preset2")
gtk_iteration()
self.assertTrue(self.data_manager.get_autoload())
self.assertTrue(self.autoload_toggle.get_active())
FlowBoxTestUtils.set_active(self.preset_selection, "preset3")
gtk_iteration()
self.autoload_toggle.set_active(True)
gtk_iteration()
FlowBoxTestUtils.set_active(self.preset_selection, "preset2")
gtk_iteration()
self.assertFalse(self.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active())
def test_each_device_can_have_autoload(self):
self.autoload_toggle.set_active(True)
gtk_iteration()
self.assertTrue(self.data_manager.get_autoload())
self.assertTrue(self.autoload_toggle.get_active())
self.click_on_group("Foo Device 2")
gtk_iteration()
self.autoload_toggle.set_active(True)
gtk_iteration()
self.assertTrue(self.data_manager.get_autoload())
self.assertTrue(self.autoload_toggle.get_active())
self.click_on_group("Foo Device")
gtk_iteration()
self.assertTrue(self.data_manager.get_autoload())
self.assertTrue(self.autoload_toggle.get_active())
def test_select_device_without_preset(self):
# creates a new empty preset when no preset exists for the device
self.click_on_group("Bar Device")
self.assertEqual(
FlowBoxTestUtils.get_active_entry(self.preset_selection).name, "new preset"
)
self.assertEqual(len(self.data_manager.active_preset), 0)
# it creates the file for that right away. It may have been possible
# to write it such that it doesn't (its empty anyway), but it does,
# so use that to test it in more detail.
path = PathUtils.get_preset_path("Bar Device", "new preset")
self.assertTrue(os.path.exists(path))
with open(path, "r") as file:
self.assertEqual(file.read(), "")
def test_recording_toggle_labels(self):
self.assertFalse(self.recording_status.get_visible())
self.recording_toggle.set_active(True)
gtk_iteration()
self.assertTrue(self.recording_status.get_visible())
self.recording_toggle.set_active(False)
gtk_iteration()
self.assertFalse(self.recording_status.get_visible())
def test_recording_label_updates_on_recording_finished(self):
self.assertFalse(self.recording_status.get_visible())
self.recording_toggle.set_active(True)
gtk_iteration()
self.assertTrue(self.recording_status.get_visible())
self.message_broker.signal(MessageType.recording_finished)
gtk_iteration()
self.assertFalse(self.recording_status.get_visible())
self.assertFalse(self.recording_toggle.get_active())
def test_events_from_reader_service_arrive(self):
# load a device with more capabilities
self.controller.load_group("Foo Device 2")
gtk_iteration()
mock1 = MagicMock()
mock2 = MagicMock()
mock3 = MagicMock()
self.message_broker.subscribe(MessageType.combination_recorded, mock1)
self.message_broker.subscribe(MessageType.recording_finished, mock2)
self.message_broker.subscribe(MessageType.recording_started, mock3)
self.recording_toggle.set_active(True)
mock3.assert_called_once()
gtk_iteration()
push_events(
fixtures.foo_device_2_keyboard,
[
InputEvent(0, 0, 1, 30, 1),
InputEvent(0, 0, 1, 31, 1),
],
)
self.throttle(60)
origin = fixtures.foo_device_2_keyboard.get_device_hash()
mock1.assert_has_calls(
(
call(
CombinationRecorded(
InputCombination(
[InputConfig(type=1, code=30, origin_hash=origin)]
)
)
),
call(
CombinationRecorded(
InputCombination(
[
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
]
)
)
),
),
any_order=False,
)
self.assertEqual(mock1.call_count, 2)
mock2.assert_not_called()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 0)])
self.throttle(60)
self.assertEqual(mock1.call_count, 2)
mock2.assert_not_called()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)])
self.throttle(60)
self.assertEqual(mock1.call_count, 2)
mock2.assert_called_once()
self.assertFalse(self.recording_toggle.get_active())
mock3.assert_called_once()
def test_cannot_create_duplicate_input_combination(self):
# load a device with more capabilities
self.controller.load_group("Foo Device 2")
gtk_iteration()
# update the combination of the active mapping
self.controller.start_key_recording()
push_events(
fixtures.foo_device_2_keyboard,
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)],
)
self.throttle(60)
# if this fails with <InputCombination (1, 5, 1)>: this is the initial
# mapping or something, so it was never overwritten.
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
# create a new mapping
self.controller.create_mapping()
gtk_iteration()
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
# try to record the same combination
self.controller.start_key_recording()
push_events(
fixtures.foo_device_2_keyboard,
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)],
)
self.throttle(60)
# should still be the empty mapping
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
# try to record a different combination
self.controller.start_key_recording()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)])
self.throttle(60)
# nothing changed yet, as we got the duplicate combination
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 1)])
self.throttle(60)
# now the combination is different
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
[
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
]
),
)
# let's make the combination even longer
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 32, 1)])
self.throttle(60)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
[
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
InputConfig(type=1, code=32, origin_hash=origin),
]
),
)
# make sure we stop recording by releasing all keys
push_events(
fixtures.foo_device_2_keyboard,
[
InputEvent(0, 0, 1, 31, 0),
InputEvent(0, 0, 1, 30, 0),
InputEvent(0, 0, 1, 32, 0),
],
)
self.throttle(60)
# sending a combination update now should not do anything
self.message_broker.publish(
CombinationRecorded(InputCombination([InputConfig(type=1, code=35)]))
)
gtk_iteration()
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
[
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
InputConfig(type=1, code=32, origin_hash=origin),
]
),
)
def test_create_simple_mapping(self):
self.click_on_group("Foo Device 2")
# 1. create a mapping
self.create_mapping_btn.clicked()
gtk_iteration()
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination.empty_combination(),
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
self.assertEqual(
self.selection_label_listbox.get_selected_row().name, "Empty Mapping"
)
self.assertIsNone(self.data_manager.active_mapping.name)
# there are now 2 mappings
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
self.assertEqual(len(self.data_manager.active_preset), 2)
# 2. record a combination for that mapping
self.recording_toggle.set_active(True)
gtk_iteration()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)])
self.throttle(60)
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)])
self.throttle(60)
# check the input_combination
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a")
self.assertIsNone(self.data_manager.active_mapping.name)
# 3. set the output symbol
self.code_editor.get_buffer().set_text("Shift_L")
gtk_iteration()
# the mapping and preset should be valid by now
self.assertTrue(self.data_manager.active_mapping.is_valid())
self.assertTrue(self.data_manager.active_preset.is_valid())
self.assertEqual(
self.data_manager.active_mapping,
Mapping(
input_combination=InputCombination(
[InputConfig(type=1, code=30, origin_hash=origin)]
),
output_symbol="Shift_L",
target_uinput="keyboard",
),
)
self.assertEqual(self.target_selection.get_active_id(), "keyboard")
buffer = self.code_editor.get_buffer()
self.assertEqual(
buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True),
"Shift_L",
)
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
# 4. update target
self.target_selection.set_active_id("keyboard + mouse")
gtk_iteration()
self.assertEqual(
self.data_manager.active_mapping,
Mapping(
input_combination=InputCombination(
[InputConfig(type=1, code=30, origin_hash=origin)]
),
output_symbol="Shift_L",
target_uinput="keyboard + mouse",
),
)
def test_show_status(self):
self.message_broker.publish(StatusData(0, "a"))
text = self.get_status_text()
self.assertEqual("a", text)
def test_hat_switch(self):
# load a device with more capabilities
self.controller.load_group("Foo Device 2")
gtk_iteration()
# it should be possible to add all of them
ev_1 = InputEvent.abs(evdev.ecodes.ABS_HAT0X, -1)
ev_2 = InputEvent.abs(evdev.ecodes.ABS_HAT0X, 1)
ev_3 = InputEvent.abs(evdev.ecodes.ABS_HAT0Y, -1)
ev_4 = InputEvent.abs(evdev.ecodes.ABS_HAT0Y, 1)
def add_mapping(event, symbol) -> InputCombination:
"""adds mapping and returns the expected input combination"""
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
push_events(fixtures.foo_device_2_gamepad, [event, event.modify(value=0)])
self.throttle(60)
gtk_iteration()
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
return InputCombination(
[
InputConfig.from_input_event(event).modify(
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash()
)
]
)
config_1 = add_mapping(ev_1, "a")
config_2 = add_mapping(ev_2, "b")
config_3 = add_mapping(ev_3, "c")
config_4 = add_mapping(ev_4, "d")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
InputCombination(config_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
InputCombination(config_2)
).output_symbol,
"b",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
InputCombination(config_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
InputCombination(config_4)
).output_symbol,
"d",
)
def test_combination(self):
# if this test freezes, try waiting a few minutes and then look for
# stack traces in the console
# load a device with more capabilities
self.controller.load_group("Foo Device 2")
gtk_iteration()
# it should be possible to write a combination
ev_1 = InputEvent.key(
evdev.ecodes.KEY_A,
1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
ev_2 = InputEvent.abs(
evdev.ecodes.ABS_HAT0X,
1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
ev_3 = InputEvent.key(
evdev.ecodes.KEY_C,
1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
ev_4 = InputEvent.abs(
evdev.ecodes.ABS_HAT0X,
-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
combination_1 = (ev_1, ev_2, ev_3)
combination_2 = (ev_2, ev_1, ev_3)
# same as 1, but different D-Pad direction
combination_3 = (ev_1, ev_4, ev_3)
combination_4 = (ev_4, ev_1, ev_3)
# same as 1, but the last combination is different
combination_5 = (ev_1, ev_3, ev_2)
combination_6 = (ev_3, ev_1, ev_2)
def get_combination(combi: Iterable[InputEvent]) -> InputCombination:
"""Create an InputCombination from a list of events.
Ensures the origin_hash is set correctly.
"""
configs = []
for event in combi:
config = InputConfig.from_input_event(event)
configs.append(config)
return InputCombination(configs)
def add_mapping(combi: Iterable[InputEvent], symbol):
logger.info("add_mapping %s", combi)
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
for event in combi:
if event.type == EV_KEY:
push_event(fixtures.foo_device_2_keyboard, event)
if event.type == EV_ABS:
push_event(fixtures.foo_device_2_gamepad, event)
if event.type == EV_REL:
push_event(fixtures.foo_device_2_mouse, event)
# avoid race condition if we switch fixture in push_event. The order
# of events needs to be correct.
self.throttle(20)
for event in combi:
if event.type == EV_KEY:
push_event(fixtures.foo_device_2_keyboard, event.modify(value=0))
if event.type == EV_ABS:
push_event(fixtures.foo_device_2_gamepad, event.modify(value=0))
if event.type == EV_REL:
pass
self.throttle(60)
gtk_iteration()
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
add_mapping(combination_1, "a")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_3))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_4))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
# it won't write the same combination again, even if the
# first two events are in a different order
add_mapping(combination_2, "b")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_3))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_4))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
add_mapping(combination_3, "c")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_4)
).output_symbol,
"c",
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
# same as with combination_2, the existing combination_3 blocks
# combination_4 because they have the same keys and end in the
# same key.
add_mapping(combination_4, "d")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_4)
).output_symbol,
"c",
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
add_mapping(combination_5, "e")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_4)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_5)
).output_symbol,
"e",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_6)
).output_symbol,
"e",
)
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
self.assertFalse(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
def test_only_one_empty_mapping_possible(self):
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination([InputConfig(type=1, code=5)]),
)
self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
self.assertEqual(len(self.data_manager.active_preset), 1)
self.create_mapping_btn.clicked()
gtk_iteration()
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination.empty_combination(),
)
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
self.assertEqual(len(self.data_manager.active_preset), 2)
self.create_mapping_btn.clicked()
gtk_iteration()
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
self.assertEqual(len(self.data_manager.active_preset), 2)
def test_selection_labels_sort_alphabetically(self):
self.controller.load_preset("preset1")
# contains two mappings (1,1,1 -> b) and (1,2,1 -> a)
gtk_iteration()
# we expect (1,2,1 -> a) to be selected because "1" < "Escape"
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
self.assertIs(
self.selection_label_listbox.get_row_at_index(0),
self.selection_label_listbox.get_selected_row(),
)
self.recording_toggle.set_active(True)
gtk_iteration()
self.message_broker.publish(
CombinationRecorded(
InputCombination([InputConfig(type=EV_KEY, code=KEY_Q)])
)
)
gtk_iteration()
self.message_broker.signal(MessageType.recording_finished)
gtk_iteration()
# the combination and the order changed "Escape" < "q"
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
self.assertIs(
self.selection_label_listbox.get_row_at_index(1),
self.selection_label_listbox.get_selected_row(),
)
def test_selection_labels_sort_empty_mapping_to_the_bottom(self):
# make sure we have a mapping which would sort to the bottom only
# considering alphanumeric sorting: "q" > "Empty Mapping"
self.controller.load_preset("preset1")
gtk_iteration()
self.recording_toggle.set_active(True)
gtk_iteration()
self.message_broker.publish(
CombinationRecorded(
InputCombination([InputConfig(type=EV_KEY, code=KEY_Q)])
)
)
gtk_iteration()
self.message_broker.signal(MessageType.recording_finished)
gtk_iteration()
self.controller.create_mapping()
gtk_iteration()
row: MappingSelectionLabel = self.selection_label_listbox.get_selected_row()
self.assertEqual(row.combination, InputCombination.empty_combination())
self.assertEqual(row.label.get_text(), "Empty Mapping")
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
def test_select_mapping(self):
self.controller.load_preset("preset1")
# contains two mappings (1,1,1 -> b) and (1,2,1 -> a)
gtk_iteration()
# we expect (1,2,1 -> a) to be selected because "1" < "Escape"
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
# select the second entry in the listbox
row = self.selection_label_listbox.get_row_at_index(1)
self.selection_label_listbox.select_row(row)
gtk_iteration()
self.assertEqual(self.data_manager.active_mapping.output_symbol, "b")
def test_selection_label_uses_name_if_available(self):
self.controller.load_preset("preset1")
gtk_iteration()
row: MappingSelectionLabel = self.selection_label_listbox.get_selected_row()
self.assertEqual(row.label.get_text(), "1")
self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
self.controller.update_mapping(name="foo")
gtk_iteration()
self.assertEqual(row.label.get_text(), "foo")
self.assertIs(row, self.selection_label_listbox.get_row_at_index(1))
# Empty Mapping still sorts to the bottom
self.controller.create_mapping()
gtk_iteration()
row = self.selection_label_listbox.get_selected_row()
self.assertEqual(row.combination, InputCombination.empty_combination())
self.assertEqual(row.label.get_text(), "Empty Mapping")
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
def test_fake_empty_mapping_does_not_sort_to_bottom(self):
"""If someone chooses to name a mapping "Empty Mapping"
it is not sorted to the bottom"""
self.controller.load_preset("preset1")
gtk_iteration()
self.controller.update_mapping(name="Empty Mapping")
self.throttle(20) # sorting seems to take a bit
# "Empty Mapping" < "Escape" so we still expect this to be the first row
row = self.selection_label_listbox.get_selected_row()
self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
# now create a real empty mapping
self.controller.create_mapping()
self.throttle(20)
# for some reason we no longer can use assertIs maybe a gtk bug?
# self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
# we expect the fake empty mapping in row 0 and the real one in row 2
self.selection_label_listbox.select_row(
self.selection_label_listbox.get_row_at_index(0)
)
gtk_iteration()
self.assertEqual(self.data_manager.active_mapping.name, "Empty Mapping")
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
self.selection_label_listbox.select_row(
self.selection_label_listbox.get_row_at_index(2)
)
self.assertIsNone(self.data_manager.active_mapping.name)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
def test_remove_mapping(self):
self.controller.load_preset("preset1")
gtk_iteration()
self.assertEqual(len(self.data_manager.active_preset), 2)
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
with patch_confirm_delete(self.user_interface):
self.delete_mapping_btn.clicked()
gtk_iteration()
self.assertEqual(len(self.data_manager.active_preset), 1)
self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
def test_problematic_combination(self):
# load a device with more capabilities
self.controller.load_group("Foo Device 2")
gtk_iteration()
def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol):
combi = [InputEvent(0, 0, *t) for t in combi]
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
push_events(fixtures.foo_device_2_keyboard, combi)
push_events(
fixtures.foo_device_2_keyboard,
[event.modify(value=0) for event in combi],
)
self.throttle(60)
gtk_iteration()
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
combination = [(EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)]
add_mapping(combination, "b")
text = self.get_status_text()
self.assertIn("shift", text)
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
self.assertFalse(error_icon.get_visible())
self.assertTrue(warning_icon.get_visible())
def test_rename_and_save(self):
# only a basic test, TestController and TestDataManager go more in detail
self.rename_input.set_text("foo")
self.rename_btn.clicked()
gtk_iteration()
preset_path = f"{PathUtils.config_path()}/presets/Foo Device/foo.json"
self.assertTrue(os.path.exists(preset_path))
error_icon = self.user_interface.get("error_status_icon")
self.assertFalse(error_icon.get_visible())
def save():
raise PermissionError
with patch.object(self.data_manager.active_preset, "save", save):
self.code_editor.get_buffer().set_text("f")
gtk_iteration()
status = self.get_status_text()
self.assertIn("Permission denied", status)
with patch_confirm_delete(self.user_interface):
self.delete_preset_btn.clicked()
gtk_iteration()
self.assertFalse(os.path.exists(preset_path))
def test_check_for_unknown_symbols(self):
first_input = InputCombination([InputConfig(type=1, code=1)])
second_input = InputCombination([InputConfig(type=1, code=2)])
status = self.user_interface.get("status_bar")
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
self.controller.load_preset("preset1")
self.throttle(20)
# Switch to the first mapping, and change it
self.controller.load_mapping(first_input)
gtk_iteration()
self.controller.update_mapping(output_symbol="foo")
gtk_iteration()
# Switch to the second mapping, and change it
self.controller.load_mapping(second_input)
gtk_iteration()
self.controller.update_mapping(output_symbol="qux")
gtk_iteration()
# The tooltip should show the error of the currently selected mapping
tooltip = status.get_tooltip_text().lower()
self.assertIn("qux", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
# So switching to the other mapping changes the tooltip
self.controller.load_mapping(first_input)
gtk_iteration()
tooltip = status.get_tooltip_text().lower()
self.assertIn("foo", tooltip)
# It will still save it though
with open(PathUtils.get_preset_path("Foo Device", "preset1")) as f:
content = f.read()
self.assertIn("qux", content)
self.assertIn("foo", content)
# Fix the current active mapping.
# It should show the error of the other mapping now.
self.controller.update_mapping(output_symbol="a")
gtk_iteration()
tooltip = status.get_tooltip_text().lower()
self.assertIn("qux", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
# Fix the other mapping as well. No tooltip should be shown afterward.
self.controller.load_mapping(second_input)
gtk_iteration()
self.controller.update_mapping(output_symbol="b")
gtk_iteration()
tooltip = status.get_tooltip_text()
self.assertIsNone(tooltip)
self.assertFalse(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
def test_no_validation_tooltip_for_empty_mappings(self):
self.controller.load_preset("preset1")
self.throttle(20)
status = self.user_interface.get("status_bar")
self.assertIsNone(status.get_tooltip_text())
self.controller.create_mapping()
gtk_iteration()
self.assertTrue(self.controller.is_empty_mapping())
self.assertIsNone(status.get_tooltip_text())
def test_check_macro_syntax(self):
status = self.status_bar
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
self.code_editor.get_buffer().set_text("k(1))")
tooltip = status.get_tooltip_text().lower()
self.assertIn("brackets", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
self.code_editor.get_buffer().set_text("k(1)")
tooltip = (status.get_tooltip_text() or "").lower()
self.assertNotIn("brackets", tooltip)
self.assertFalse(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
self.assertEqual(
self.data_manager.active_mapping.output_symbol,
"k(1)",
)
def test_check_on_typing(self):
status = self.user_interface.get("status_bar")
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
tooltip = status.get_tooltip_text()
# nothing wrong yet
self.assertIsNone(tooltip)
# now change the mapping by typing into the field
buffer = self.code_editor.get_buffer()
buffer.set_text("sdfgkj()")
gtk_iteration()
# the mapping is validated
tooltip = status.get_tooltip_text()
self.assertIn("Unknown function sdfgkj", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
self.assertEqual(self.data_manager.active_mapping.output_symbol, "sdfgkj()")
def test_select_device(self):
# simple test to make sure we can switch between devices
# more detailed tests in TestController and TestDataManager
self.click_on_group("Bar Device")
gtk_iteration()
entries = {*FlowBoxTestUtils.get_child_names(self.preset_selection)}
self.assertEqual(entries, {"new preset"})
self.click_on_group("Foo Device")
gtk_iteration()
entries = {*FlowBoxTestUtils.get_child_names(self.preset_selection)}
self.assertEqual(entries, {"preset1", "preset2", "preset3"})
# make sure a preset and mapping was loaded
self.assertIsNotNone(self.data_manager.active_preset)
self.assertEqual(
self.data_manager.active_preset.name,
FlowBoxTestUtils.get_active_entry(self.preset_selection).name,
)
self.assertIsNotNone(self.data_manager.active_mapping)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
self.selection_label_listbox.get_selected_row().combination,
)
def test_select_preset(self):
# simple test to make sure we can switch between presets
# more detailed tests in TestController and TestDataManager
self.click_on_group("Foo Device 2")
gtk_iteration()
FlowBoxTestUtils.set_active(self.preset_selection, "preset1")
gtk_iteration()
mappings = {
row.combination for row in self.selection_label_listbox.get_children()
}
self.assertEqual(
mappings,
{
InputCombination([InputConfig(type=1, code=1)]),
InputCombination([InputConfig(type=1, code=2)]),
},
)
self.assertFalse(self.autoload_toggle.get_active())
FlowBoxTestUtils.set_active(self.preset_selection, "preset2")
gtk_iteration()
mappings = {
row.combination for row in self.selection_label_listbox.get_children()
}
self.assertEqual(
mappings,
{
InputCombination([InputConfig(type=1, code=3)]),
InputCombination([InputConfig(type=1, code=4)]),
},
)
self.assertTrue(self.autoload_toggle.get_active())
def test_copy_preset(self):
# simple tests to ensure it works
# more detailed tests in TestController and TestDataManager
# check the initial state
entries = {*FlowBoxTestUtils.get_child_names(self.preset_selection)}
self.assertEqual(entries, {"preset1", "preset2", "preset3"})
self.assertEqual(
FlowBoxTestUtils.get_active_entry(self.preset_selection).name, "preset3"
)
self.copy_preset_btn.clicked()
gtk_iteration()
entries = {*FlowBoxTestUtils.get_child_names(self.preset_selection)}
self.assertEqual(entries, {"preset1", "preset2", "preset3", "preset3 copy"})
self.assertEqual(
FlowBoxTestUtils.get_active_entry(self.preset_selection).name,
"preset3 copy",
)
self.copy_preset_btn.clicked()
gtk_iteration()
entries = {*FlowBoxTestUtils.get_child_names(self.preset_selection)}
self.assertEqual(
entries, {"preset1", "preset2", "preset3", "preset3 copy", "preset3 copy 2"}
)
def test_wont_start(self):
def wait():
"""Wait for the injector process to finish doing stuff."""
for _ in range(10):
time.sleep(0.1)
gtk_iteration()
if "Starting" not in self.get_status_text():
return
error_icon = self.user_interface.get("error_status_icon")
self.controller.load_group("Bar Device")
# empty
self.start_injector_btn.clicked()
gtk_iteration()
wait()
text = self.get_status_text()
self.assertIn("add mappings", text)
self.assertTrue(error_icon.get_visible())
self.assertNotEqual(self.daemon.get_state("Bar Device"), InjectorState.RUNNING)
# device grabbing fails
self.controller.load_group("Foo Device 2")
gtk_iteration()
for i in range(2):
# just pressing apply again will overwrite the previous error
self.grab_fails = True
self.start_injector_btn.clicked()
gtk_iteration()
text = self.get_status_text()
# it takes a little bit of time
self.assertIn("Starting injection", text)
self.assertFalse(error_icon.get_visible())
wait()
text = self.get_status_text()
self.assertIn("Failed to apply preset", text)
self.assertTrue(error_icon.get_visible())
self.assertNotEqual(
self.daemon.get_state("Foo Device 2"), InjectorState.RUNNING
)
# this time work properly
self.grab_fails = False
self.start_injector_btn.clicked()
gtk_iteration()
text = self.get_status_text()
self.assertIn("Starting injection", text)
self.assertFalse(error_icon.get_visible())
wait()
text = self.get_status_text()
self.assertIn("Applied", text)
text = self.get_status_text()
self.assertNotIn("CTRL + DEL", text) # only shown if btn_left mapped
self.assertFalse(error_icon.get_visible())
self.assertEqual(self.daemon.get_state("Foo Device 2"), InjectorState.RUNNING)
def test_start_with_btn_left(self):
self.controller.load_group("Foo Device 2")
gtk_iteration()
self.controller.create_mapping()
gtk_iteration()
self.controller.update_mapping(
input_combination=InputCombination([InputConfig.btn_left()]),
output_symbol="a",
)
gtk_iteration()
def wait():
"""Wait for the injector process to finish doing stuff."""
for _ in range(10):
time.sleep(0.1)
gtk_iteration()
if "Starting" not in self.get_status_text():
return
# first apply, shows btn_left warning
self.start_injector_btn.clicked()
gtk_iteration()
text = self.get_status_text()
self.assertIn("click", text)
self.assertEqual(self.daemon.get_state("Foo Device 2"), InjectorState.UNKNOWN)
# second apply, overwrites
self.start_injector_btn.clicked()
gtk_iteration()
wait()
self.assertEqual(self.daemon.get_state("Foo Device 2"), InjectorState.RUNNING)
text = self.get_status_text()
# because btn_left is mapped, shows help on how to stop
# injecting via the keyboard
self.assertIn("CTRL + DEL", text)
def test_cannot_record_keys(self):
self.controller.load_group("Foo Device 2")
self.assertNotEqual(self.data_manager.get_state(), InjectorState.RUNNING)
self.assertNotIn("Stop", self.get_status_text())
self.recording_toggle.set_active(True)
gtk_iteration()
self.assertTrue(self.recording_toggle.get_active())
self.controller.stop_key_recording()
gtk_iteration()
self.assertFalse(self.recording_toggle.get_active())
self.start_injector_btn.clicked()
gtk_iteration()
# wait for the injector to start
for _ in range(10):
time.sleep(0.1)
gtk_iteration()
if "Starting" not in self.get_status_text():
break
self.assertEqual(self.data_manager.get_state(), InjectorState.RUNNING)
# the toggle button should reset itself shortly
self.recording_toggle.set_active(True)
gtk_iteration()
self.assertFalse(self.recording_toggle.get_active())
text = self.get_status_text()
self.assertIn("Stop", text)
def test_start_injecting(self):
self.controller.load_group("Foo Device 2")
with spy(self.daemon, "set_config_dir") as spy1:
with spy(self.daemon, "start_injecting") as spy2:
self.start_injector_btn.clicked()
gtk_iteration()
# correctly uses group.key, not group.name
spy2.assert_called_once_with("Foo Device 2", "preset3")
spy1.assert_called_once_with(PathUtils.get_config_path())
for _ in range(10):
time.sleep(0.1)
gtk_iteration()
if self.data_manager.get_state() == InjectorState.RUNNING:
break
# fail here so we don't block forever
self.assertEqual(self.data_manager.get_state(), InjectorState.RUNNING)
# this is a stupid workaround for the bad test fixtures
# by switching the group we make sure that the reader-service no longer
# listens for events on "Foo Device 2" otherwise we would have two processes
# (reader-service and injector) reading the same pipe which can block this test
# indefinitely
self.controller.load_group("Foo Device")
gtk_iteration()
push_events(
fixtures.foo_device_2_keyboard,
[
InputEvent.key(5, 1),
InputEvent.key(5, 0),
],
)
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, evdev.events.EV_KEY)
self.assertEqual(event.code, KEY_A)
self.assertEqual(event.value, 1)
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, evdev.events.EV_KEY)
self.assertEqual(event.code, KEY_A)
self.assertEqual(event.value, 0)
# the input-remapper device will not be shown
self.controller.refresh_groups()
gtk_iteration()
for child in self.device_selection.get_children():
device_group_entry = child.get_children()[0]
self.assertNotIn("input-remapper", device_group_entry.name)
def test_stop_injecting(self):
self.controller.load_group("Foo Device 2")
self.start_injector_btn.clicked()
gtk_iteration()
for _ in range(10):
time.sleep(0.1)
gtk_iteration()
if self.data_manager.get_state() == InjectorState.RUNNING:
break
# fail here so we don't block forever
self.assertEqual(self.data_manager.get_state(), InjectorState.RUNNING)
# stupid fixture workaround
self.controller.load_group("Foo Device")
gtk_iteration()
pipe = uinput_write_history_pipe[0]
self.assertFalse(pipe.poll())
push_events(
fixtures.foo_device_2_keyboard,
[
InputEvent.key(5, 1),
InputEvent.key(5, 0),
],
)
time.sleep(0.2)
self.assertTrue(pipe.poll())
while pipe.poll():
pipe.recv()
self.controller.load_group("Foo Device 2")
self.controller.stop_injecting()
gtk_iteration()
for _ in range(10):
time.sleep(0.1)
gtk_iteration()
if self.data_manager.get_state() == InjectorState.STOPPED:
break
self.assertEqual(self.data_manager.get_state(), InjectorState.STOPPED)
push_events(
fixtures.foo_device_2_keyboard,
[
InputEvent.key(5, 1),
InputEvent.key(5, 0),
],
)
time.sleep(0.2)
self.assertFalse(pipe.poll())
def test_delete_preset(self):
# as per test_initial_state we already have preset3 loaded
self.assertTrue(
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3"))
)
with patch_confirm_delete(self.user_interface, Gtk.ResponseType.CANCEL):
self.delete_preset_btn.clicked()
gtk_iteration()
self.assertTrue(
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3"))
)
self.assertEqual(self.data_manager.active_preset.name, "preset3")
self.assertEqual(self.data_manager.active_group.name, "Foo Device")
with patch_confirm_delete(self.user_interface):
self.delete_preset_btn.clicked()
gtk_iteration()
self.assertFalse(
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3"))
)
self.assertEqual(self.data_manager.active_preset.name, "preset2")
self.assertEqual(self.data_manager.active_group.name, "Foo Device")
def test_refresh_groups(self):
# sanity check: preset3 should be the newest
self.assertEqual(
FlowBoxTestUtils.get_active_entry(self.preset_selection).name, "preset3"
)
# select the older one
FlowBoxTestUtils.set_active(self.preset_selection, "preset1")
gtk_iteration()
self.assertEqual(self.data_manager.active_preset.name, "preset1")
# add a device that doesn't exist to the dropdown
unknown_key = "key-1234"
self.device_selection.insert(
DeviceGroupEntry(self.message_broker, self.controller, None, unknown_key),
0,
# 0, [unknown_key, None, "foo"]
)
self.controller.refresh_groups()
gtk_iteration()
self.throttle(200)
# the gui should not jump to a different preset suddenly
self.assertEqual(self.data_manager.active_preset.name, "preset1")
# just to verify that the mtime still tells us that preset3 is the newest one
self.assertEqual(self.controller.get_a_preset(), "preset3")
# the list contains correct entries
# and the non-existing entry should be removed
names = FlowBoxTestUtils.get_child_names(self.device_selection)
icons = FlowBoxTestUtils.get_child_icons(self.device_selection)
self.assertNotIn(unknown_key, names)
self.assertIn("Foo Device", names)
self.assertIn("Foo Device 2", names)
self.assertIn("Bar Device", names)
self.assertIn("gamepad", names)
self.assertIn("input-keyboard", icons)
self.assertIn("input-gaming", icons)
self.assertIn("input-keyboard", icons)
self.assertIn("input-gaming", icons)
# it won't crash due to "list index out of range"
# when `types` is an empty list. Won't show an icon
self.data_manager._reader_client.groups.find(key="Foo Device 2").types = []
self.data_manager._reader_client.publish_groups()
gtk_iteration()
self.assertIn(
"Foo Device 2",
FlowBoxTestUtils.get_child_names(self.device_selection),
)
def test_shared_presets(self):
# devices with the same name (but different key because the key is
# unique) share the same presets.
# Those devices would usually be of the same model of keyboard for example
# Todo: move this to unit tests, there is no point in having the ui around
self.controller.load_group("Foo Device")
presets1 = self.data_manager.get_preset_names()
self.controller.load_group("Foo Device 2")
gtk_iteration()
presets2 = self.data_manager.get_preset_names()
self.controller.load_group("Bar Device")
gtk_iteration()
presets3 = self.data_manager.get_preset_names()
self.assertEqual(presets1, presets2)
self.assertNotEqual(presets1, presets3)
def test_delete_last_preset(self):
with patch_confirm_delete(self.user_interface):
# as per test_initial_state we already have preset3 loaded
self.assertEqual(self.data_manager.active_preset.name, "preset3")
self.delete_preset_btn.clicked()
gtk_iteration()
# the next newest preset should be loaded
self.assertEqual(self.data_manager.active_preset.name, "preset2")
self.delete_preset_btn.clicked()
gtk_iteration()
self.delete_preset_btn.clicked()
# the ui should be clean
self.assert_gui_clean()
device_path = f"{PathUtils.config_path()}/presets/{self.data_manager.active_group.name}"
self.assertTrue(os.path.exists(f"{device_path}/new preset.json"))
self.delete_preset_btn.clicked()
gtk_iteration()
# deleting an empty preset als doesn't do weird stuff
self.assert_gui_clean()
device_path = f"{PathUtils.config_path()}/presets/{self.data_manager.active_group.name}"
self.assertTrue(os.path.exists(f"{device_path}/new preset.json"))
def test_enable_disable_output(self):
# load a group without any presets
self.controller.load_group("Bar Device")
# should be disabled by default since no key is recorded yet
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
self.assertFalse(self.output_box.get_sensitive())
# create a mapping
self.controller.create_mapping()
gtk_iteration()
# should still be disabled
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
self.assertFalse(self.output_box.get_sensitive())
# enable it by sending a combination
self.controller.start_key_recording()
gtk_iteration()
push_events(
fixtures.bar_device,
[
InputEvent(0, 0, 1, 30, 1),
InputEvent(0, 0, 1, 30, 0),
],
)
self.throttle(100) # give time for the input to arrive
self.assertEqual(
self.get_unfiltered_symbol_input_text(), CodeEditor.placeholder
)
self.assertTrue(self.output_box.get_sensitive())
# disable it by deleting the mapping
with patch_confirm_delete(self.user_interface):
self.delete_mapping_btn.clicked()
gtk_iteration()
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
self.assertFalse(self.output_box.get_sensitive())
if __name__ == "__main__":
unittest.main()