qutebrowser/tests/unit/keyinput/test_modeman.py

230 lines
8.9 KiB
Python

# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import pytest
from qutebrowser.qt.core import Qt, QObject, pyqtSignal, QTimer, QEvent
from qutebrowser.qt.gui import QKeyEvent, QKeySequence
from qutebrowser.utils import usertypes, objreg
from qutebrowser.keyinput import keyutils, basekeyparser
from qutebrowser.misc import objects
class FakeKeyparser(QObject):
"""A fake BaseKeyParser which doesn't handle anything."""
keystring_updated = pyqtSignal(str)
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
forward_partial_key = pyqtSignal(str)
clear_partial_keys = pyqtSignal()
def __init__(self):
super().__init__()
self.passthrough = False
self.allow_partial_timeout = False
self.allow_forward = True
self.forward_widget_name = None
def handle(
self,
evt: QKeyEvent,
*,
dry_run: bool = False,
) -> QKeySequence.SequenceMatch:
return QKeySequence.SequenceMatch.NoMatch
@pytest.fixture
def modeman(mode_manager):
mode_manager.register(usertypes.KeyMode.normal, FakeKeyparser())
return mode_manager
@pytest.fixture(autouse=True)
def set_qapp(monkeypatch, qapp):
monkeypatch.setattr(objects, 'qapp', qapp)
@pytest.mark.parametrize('key, modifiers, filtered', [
(Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier, True),
(Qt.Key.Key_Up, Qt.KeyboardModifier.NoModifier, False),
# https://github.com/qutebrowser/qutebrowser/issues/1207
(Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier, True),
(Qt.Key.Key_A, Qt.KeyboardModifier.ShiftModifier | Qt.KeyboardModifier.ControlModifier, False),
])
def test_non_alphanumeric(key, modifiers, filtered, modeman):
"""Make sure non-alphanumeric keys are passed through correctly."""
evt = keyutils.KeyInfo(key=key, modifiers=modifiers).to_event()
assert modeman.handle_event(evt) == filtered
class FakeKeyparserWithTimeout(QObject):
"""A minimal fake BaseKeyParser for testing partial timeouts."""
keystring_updated = pyqtSignal(str)
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
forward_partial_key = pyqtSignal(str)
clear_partial_keys = pyqtSignal()
def __init__(self):
super().__init__()
self.passthrough = False
self.allow_partial_timeout = True
self.allow_forward = True
self.forward_widget_name = None
self.fake_clear_keystring_called = False
def handle(self, evt, *, dry_run=False):
txt = str(keyutils.KeyInfo.from_event(evt))
if 'a' == txt:
return QKeySequence.SequenceMatch.ExactMatch
elif 'b' == txt:
return QKeySequence.SequenceMatch.PartialMatch
else:
return QKeySequence.SequenceMatch.NoMatch
def clear_keystring(self):
self.fake_clear_keystring_called = True
self.keystring_updated.emit('')
@pytest.fixture
def modeman_with_timeout(mode_manager):
mode_manager.register(usertypes.KeyMode.normal, FakeKeyparserWithTimeout())
return mode_manager
@pytest.mark.parametrize('data_sequence', [
((Qt.Key.Key_A, 'timer_inactive'),),
((Qt.Key.Key_B, 'timer_active'),),
((Qt.Key.Key_C, 'timer_inactive'),),
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_A, 'timer_inactive'),),
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'),),
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_C, 'timer_inactive'),),
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'), (Qt.Key.Key_A, 'timer_inactive'),),
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'), (Qt.Key.Key_B, 'timer_reset'),),
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'), (Qt.Key.Key_C, 'timer_inactive'),),
])
def test_partial_keychain_timeout(modeman_with_timeout, config_stub, qtbot, data_sequence):
"""Test partial keychain timeout behavior."""
mode = modeman_with_timeout.mode
timeout = 100
config_stub.val.input.partial_timeout = timeout
timer = modeman_with_timeout._partial_timer
parser = modeman_with_timeout.parsers[mode]
assert not timer.isActive()
behavior = None
for key, behavior in data_sequence:
keyinfo = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)
if behavior == 'timer_active':
# Timer should be active
modeman_with_timeout.handle_event(keyinfo.to_event())
assert timer.isSingleShot()
assert timer.interval() == timeout
assert timer.isActive()
elif behavior == 'timer_inactive':
# Timer should be inactive
modeman_with_timeout.handle_event(keyinfo.to_event())
assert not timer.isActive()
assert not parser.fake_clear_keystring_called
elif behavior == 'timer_reset':
# Timer should be reset after handling the key
half_timer = QTimer()
half_timer.setSingleShot(True)
half_timer.setInterval(timeout//2)
half_timer.start()
# Simulate a half timeout to check for reset
qtbot.wait_signal(half_timer.timeout).wait()
assert (timeout - (timeout//4)) > timer.remainingTime()
modeman_with_timeout.handle_event(keyinfo.to_event())
assert (timeout - (timeout//4)) < timer.remainingTime()
assert timer.isActive()
else:
pytest.fail('Unreachable')
if behavior in ['timer_active', 'timer_reset']:
# Now simulate a timeout and check the keystring has been cleared.
with qtbot.wait_signal(modeman_with_timeout.keystring_updated) as blocker:
timer.timeout.emit()
assert parser.fake_clear_keystring_called
parser.fake_clear_keystring_called = False
assert blocker.args == [mode, '']
class FakeEventFilter(QObject):
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
return True
@pytest.fixture
def modeman_with_basekeyparser(mode_manager, config_stub):
fake_event_filter = FakeEventFilter()
objreg.register('fake-event-filter', fake_event_filter, scope='window',
window=0)
config_stub.val.bindings.default = {}
config_stub.val.bindings.commands = {
'normal': {
'bb': 'message-info bb',
'byy': 'message-info byy',
}
}
config_stub.val.bindings.key_mappings = {}
mode = usertypes.KeyMode.normal
mode_manager.register(mode,
basekeyparser.BaseKeyParser(mode=mode,
win_id=0,
passthrough=True,
forward_widget_name='fake-event-filter'))
yield mode_manager
objreg.delete('fake-event-filter', scope='window', window=0)
def test_release_forwarding(modeman_with_basekeyparser):
def helper_data(res, mwb):
return res, len(mwb._partial_match_events), \
len(mwb._releaseevents_to_pass)
mwb = modeman_with_basekeyparser
info_b = keyutils.KeyInfo(Qt.Key.Key_B, Qt.KeyboardModifier.NoModifier)
info_c = keyutils.KeyInfo(Qt.Key.Key_C, Qt.KeyboardModifier.NoModifier)
res = mwb.handle_event(info_b.to_event(QEvent.Type.KeyPress))
assert (True, 1, 0) == helper_data(res, mwb)
assert not mwb._partial_match_events[0].is_released()
res = mwb.handle_event(info_c.to_event(QEvent.Type.KeyPress))
assert (True, 0, 2) == helper_data(res, mwb)
res = mwb.handle_event(info_b.to_event(QEvent.Type.KeyRelease))
assert (False, 0, 1) == helper_data(res, mwb)
res = mwb.handle_event(info_c.to_event(QEvent.Type.KeyRelease))
assert (False, 0, 0) == helper_data(res, mwb)
info_y = keyutils.KeyInfo(Qt.Key.Key_Y, Qt.KeyboardModifier.NoModifier)
res = mwb.handle_event(info_b.to_event(QEvent.Type.KeyPress))
assert (True, 1, 0) == helper_data(res, mwb)
assert not mwb._partial_match_events[0].is_released()
res = mwb.handle_event(info_y.to_event(QEvent.Type.KeyPress))
assert (True, 2, 0) == helper_data(res, mwb)
assert not mwb._partial_match_events[0].is_released()
assert not mwb._partial_match_events[1].is_released()
res = mwb.handle_event(info_y.to_event(QEvent.Type.KeyRelease))
assert (True, 2, 0) == helper_data(res, mwb)
assert not mwb._partial_match_events[0].is_released()
assert mwb._partial_match_events[1].is_released()
res = mwb.handle_event(info_c.to_event(QEvent.Type.KeyPress))
assert (True, 0, 2) == helper_data(res, mwb)
res = mwb.handle_event(info_c.to_event(QEvent.Type.KeyRelease))
assert (False, 0, 1) == helper_data(res, mwb)
res = mwb.handle_event(info_b.to_event(QEvent.Type.KeyRelease))
assert (False, 0, 0) == helper_data(res, mwb)
res = mwb.handle_event(info_b.to_event(QEvent.Type.KeyPress))
assert (True, 1, 0) == helper_data(res, mwb)
assert not mwb._partial_match_events[0].is_released()
res = mwb.handle_event(info_c.to_event(QEvent.Type.KeyRelease))
assert (True, 1, 0) == helper_data(res, mwb)