* fix(keyboard): preserve XKB layout when activating Vocalinux IBus engine When Vocalinux starts, it may start or restart the IBus daemon, which can override the user's system-level keyboard layout with IBus's configured layout (often defaulting to US). This fix captures the current XKB layout before activating the Vocalinux IBus engine and restores it after activation, ensuring users with non-US keyboard layouts don't lose their settings. Fixes #292 * fix(keyboard): restore XKB layout after engine activation, not just on shutdown (#292) The draft PR only restored the user's XKB layout when Vocalinux shut down. The French AZERTY reporter confirmed the real problem: switching to the Vocalinux IBus engine overrides the system keyboard layout immediately, so typing 'a' produces 'q' (AZERTY mapped through US QWERTY). The same applies to Spanish and other non-US layouts. Changes: - Restore XKB layout immediately after IBus engine activation in _setup_engine(), not just in stop(). This preserves the user's keyboard layout while Vocalinux is actively running. - Remove the 'layout != us' guard in stop() -- always restore whatever layout was captured, including US with custom options. - Fix 4 broken tests: wrong expected options values, missing TimeoutExpired argument, incorrect call count assertion. - Add test cases for Spanish and French AZERTY layouts. * test(keyboard): add coverage for XKB layout capture/restore in IBusTextInjector Addresses Codecov report on PR #343 - adds tests covering: - stop() restores captured XKB layout (Spanish with variant/options) - stop() skips restore when no layout was captured - _setup_engine() captures XKB layout and restores it after engine activation (French AZERTY) * fix(test): mock start_engine_process in setup_engine XKB test The test was missing a mock for start_engine_process, causing it to fail on CI where the IBus engine PID file path doesn't exist.
155 lines
5.5 KiB
Python
155 lines
5.5 KiB
Python
"""Tests for XKB layout preservation functions."""
|
|
|
|
import subprocess
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
class TestXkbLayoutFunctions(unittest.TestCase):
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_get_current_xkb_layout_success(self, mock_run):
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="rules: evdev\nlayout: us\nvariant: \noptions: compose:menu,grp:caps_toggle",
|
|
)
|
|
|
|
from vocalinux.text_injection.ibus_engine import get_current_xkb_layout
|
|
|
|
result = get_current_xkb_layout()
|
|
|
|
self.assertEqual(result, ("us", "", "compose:menu,grp:caps_toggle"))
|
|
mock_run.assert_called_once_with(
|
|
["setxkbmap", "-query"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2,
|
|
)
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_get_current_xkb_layout_spanish(self, mock_run):
|
|
"""Verify Spanish layout is captured correctly (issue #292)."""
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="rules: evdev\nlayout: es\nvariant: \noptions: ",
|
|
)
|
|
|
|
from vocalinux.text_injection.ibus_engine import get_current_xkb_layout
|
|
|
|
result = get_current_xkb_layout()
|
|
|
|
self.assertEqual(result, ("es", "", ""))
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_get_current_xkb_layout_with_variant(self, mock_run):
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="rules: evdev\nlayout: es\nvariant: catalan\noptions: compose:menu",
|
|
)
|
|
|
|
from vocalinux.text_injection.ibus_engine import get_current_xkb_layout
|
|
|
|
result = get_current_xkb_layout()
|
|
|
|
self.assertEqual(result, ("es", "catalan", "compose:menu"))
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_get_current_xkb_layout_french_azerty(self, mock_run):
|
|
"""Verify French AZERTY layout is captured correctly (issue #292)."""
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="rules: evdev\nlayout: fr\nvariant: azerty\noptions: ",
|
|
)
|
|
|
|
from vocalinux.text_injection.ibus_engine import get_current_xkb_layout
|
|
|
|
result = get_current_xkb_layout()
|
|
|
|
self.assertEqual(result, ("fr", "azerty", ""))
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_get_current_xkb_layout_with_options(self, mock_run):
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="rules: evdev\nlayout: de\nvariant: \noptions: caps:ctrl_modifier",
|
|
)
|
|
|
|
from vocalinux.text_injection.ibus_engine import get_current_xkb_layout
|
|
|
|
result = get_current_xkb_layout()
|
|
|
|
self.assertEqual(result, ("de", "", "caps:ctrl_modifier"))
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_get_current_xkb_layout_error(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
|
|
|
from vocalinux.text_injection.ibus_engine import get_current_xkb_layout
|
|
|
|
result = get_current_xkb_layout()
|
|
|
|
self.assertEqual(result, ("us", "", ""))
|
|
mock_run.assert_called_once()
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_get_current_xkb_layout_timeout(self, mock_run):
|
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd="setxkbmap", timeout=2)
|
|
|
|
from vocalinux.text_injection.ibus_engine import get_current_xkb_layout
|
|
|
|
result = get_current_xkb_layout()
|
|
|
|
self.assertEqual(result, ("us", "", ""))
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_restore_xkb_layout_with_variant_and_option(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0)
|
|
|
|
from vocalinux.text_injection.ibus_engine import restore_xkb_layout
|
|
|
|
result = restore_xkb_layout("es", "catalan", "compose:menu")
|
|
|
|
self.assertTrue(result)
|
|
mock_run.assert_called_once()
|
|
cmd = mock_run.call_args[0][0]
|
|
self.assertIn("-layout", cmd)
|
|
self.assertIn("es", cmd)
|
|
self.assertIn("-variant", cmd)
|
|
self.assertIn("catalan", cmd)
|
|
self.assertIn("-option", cmd)
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_restore_xkb_layout_no_variant(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0)
|
|
|
|
from vocalinux.text_injection.ibus_engine import restore_xkb_layout
|
|
|
|
result = restore_xkb_layout("de")
|
|
|
|
self.assertTrue(result)
|
|
mock_run.assert_called_once_with(
|
|
["setxkbmap", "-layout", "de"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2,
|
|
)
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_restore_xkb_layout_error(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=1, stderr="Error")
|
|
|
|
from vocalinux.text_injection.ibus_engine import restore_xkb_layout
|
|
|
|
result = restore_xkb_layout("es")
|
|
|
|
self.assertFalse(result)
|
|
|
|
@patch("vocalinux.text_injection.ibus_engine.subprocess.run")
|
|
def test_restore_xkb_layout_empty_layout(self, mock_run):
|
|
from vocalinux.text_injection.ibus_engine import restore_xkb_layout
|
|
|
|
result = restore_xkb_layout("")
|
|
|
|
self.assertFalse(result)
|
|
mock_run.assert_not_called()
|