Improve review spelling shortcuts
This commit is contained in:
@@ -7,6 +7,42 @@ from fenrirscreenreader.core.fenrirManager import FenrirManager
|
||||
from fenrirscreenreader.core.inputManager import InputManager
|
||||
|
||||
|
||||
def create_input_manager():
|
||||
input_driver = Mock(get_led_state=Mock(return_value=False))
|
||||
settings_manager = Mock()
|
||||
settings_manager.get_setting_as_float.return_value = 0.2
|
||||
manager = InputManager()
|
||||
manager.env = {
|
||||
"input": {
|
||||
"curr_input": [],
|
||||
"prev_input": [],
|
||||
"event_buffer": [object()],
|
||||
"shortcut_repeat": 1,
|
||||
"old_num_lock": False,
|
||||
"new_num_lock": False,
|
||||
"old_caps_lock": False,
|
||||
"new_caps_lock": False,
|
||||
"old_scroll_lock": False,
|
||||
"new_scroll_lock": False,
|
||||
},
|
||||
"runtime": {
|
||||
"DebugManager": Mock(),
|
||||
"InputDriver": input_driver,
|
||||
"SettingsManager": settings_manager,
|
||||
},
|
||||
}
|
||||
manager.lastDeepestInput = []
|
||||
manager.lastInputTime = 0
|
||||
manager.handle_led_states = Mock()
|
||||
return manager
|
||||
|
||||
|
||||
def send_input(manager, event_name, event_state):
|
||||
manager.handle_input_event(
|
||||
{"event_name": event_name, "event_state": event_state}
|
||||
)
|
||||
|
||||
|
||||
def create_handle_input_manager(no_key_pressed):
|
||||
manager = FenrirManager.__new__(FenrirManager)
|
||||
manager.modifierInput = False
|
||||
@@ -48,6 +84,100 @@ def test_input_manager_recognizes_conventional_modifier_prefix():
|
||||
assert input_manager.curr_input_is_modifier_prefix() is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_bare_key_taps_advance_shortcut_repeat(monkeypatch):
|
||||
manager = create_input_manager()
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
|
||||
)
|
||||
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
assert manager.env["input"]["shortcut_repeat"] == 1
|
||||
send_input(manager, "KEY_KP5", 0)
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
assert manager.env["input"]["shortcut_repeat"] == 2
|
||||
send_input(manager, "KEY_KP5", 0)
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
assert manager.env["input"]["shortcut_repeat"] == 3
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_bare_key_taps_resolve_progressive_shortcuts(monkeypatch):
|
||||
manager = create_input_manager()
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
|
||||
)
|
||||
manager.env["runtime"]["CursorManager"] = Mock(
|
||||
should_process_numpad_commands=Mock(return_value=True)
|
||||
)
|
||||
manager.env["bindings"] = {
|
||||
str([1, ["KEY_KP5"]]): "REVIEW_CURR_WORD",
|
||||
str([2, ["KEY_KP5"]]): "REVIEW_CURR_WORD_SPELL",
|
||||
str([3, ["KEY_KP5"]]): "REVIEW_CURR_WORD_PHONETIC",
|
||||
}
|
||||
|
||||
resolved_commands = []
|
||||
for _tap in range(3):
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
shortcut = manager.get_curr_shortcut()
|
||||
resolved_commands.append(manager.get_command_for_shortcut(shortcut))
|
||||
send_input(manager, "KEY_KP5", 0)
|
||||
|
||||
assert resolved_commands == [
|
||||
"REVIEW_CURR_WORD",
|
||||
"REVIEW_CURR_WORD_SPELL",
|
||||
"REVIEW_CURR_WORD_PHONETIC",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_different_bare_key_starts_new_repeat_sequence(monkeypatch):
|
||||
manager = create_input_manager()
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
|
||||
)
|
||||
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
send_input(manager, "KEY_KP5", 0)
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
assert manager.env["input"]["shortcut_repeat"] == 2
|
||||
send_input(manager, "KEY_KP5", 0)
|
||||
send_input(manager, "KEY_KP2", 1)
|
||||
assert manager.env["input"]["shortcut_repeat"] == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_bare_key_tap_after_timeout_starts_new_sequence(monkeypatch):
|
||||
manager = create_input_manager()
|
||||
current_time = [1.0]
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.inputManager.time.time",
|
||||
lambda: current_time[0],
|
||||
)
|
||||
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
send_input(manager, "KEY_KP5", 0)
|
||||
current_time[0] = 2.0
|
||||
send_input(manager, "KEY_KP5", 1)
|
||||
|
||||
assert manager.env["input"]["shortcut_repeat"] == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_modifier_chord_taps_still_advance_shortcut_repeat(monkeypatch):
|
||||
manager = create_input_manager()
|
||||
monkeypatch.setattr(
|
||||
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
|
||||
)
|
||||
|
||||
send_input(manager, "KEY_FENRIR", 1)
|
||||
send_input(manager, "KEY_T", 1)
|
||||
assert manager.env["input"]["shortcut_repeat"] == 1
|
||||
send_input(manager, "KEY_T", 0)
|
||||
send_input(manager, "KEY_T", 1)
|
||||
assert manager.env["input"]["shortcut_repeat"] == 2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_first_conventional_modifier_starts_deferred_prefix():
|
||||
manager = FenrirManager.__new__(FenrirManager)
|
||||
@@ -196,4 +326,5 @@ def test_modifier_prefix_is_forwarded_when_released_unused():
|
||||
)
|
||||
|
||||
input_manager.write_event_buffer.assert_called_once_with()
|
||||
input_manager.clear_last_deep_input.assert_not_called()
|
||||
assert manager.modifier_prefix_input is False
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fenrirscreenreader.core.inputManager import InputManager
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
COMMANDS_DIR = (
|
||||
PROJECT_ROOT / "src" / "fenrirscreenreader" / "commands" / "commands"
|
||||
)
|
||||
KEYBOARD_DIR = PROJECT_ROOT / "config" / "keyboard"
|
||||
|
||||
|
||||
def load_command(name):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"fenrir_{name}", COMMANDS_DIR / f"{name}.py"
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module.command()
|
||||
|
||||
|
||||
def run_command(name, content, cursor_x=0, cursor_y=0):
|
||||
output_manager = Mock()
|
||||
settings_manager = Mock()
|
||||
settings_manager.get_setting_as_bool.return_value = True
|
||||
env = {
|
||||
"punctuation": {"PUNCTDICT": {" ": "space"}},
|
||||
"screen": {
|
||||
"newCursorReview": {"x": cursor_x, "y": cursor_y},
|
||||
"new_content_text": content,
|
||||
},
|
||||
"runtime": {
|
||||
"CursorManager": Mock(),
|
||||
"OutputManager": output_manager,
|
||||
"SettingsManager": settings_manager,
|
||||
},
|
||||
}
|
||||
command = load_command(name)
|
||||
command.initialize(env)
|
||||
command.run()
|
||||
return output_manager
|
||||
|
||||
|
||||
def load_bindings(layout):
|
||||
manager = InputManager()
|
||||
manager.env = {
|
||||
"bindings": {},
|
||||
"rawBindings": {},
|
||||
"runtime": {"DebugManager": Mock()},
|
||||
}
|
||||
manager.load_shortcuts(KEYBOARD_DIR / f"{layout}.conf")
|
||||
return manager.env["bindings"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_current_word_is_spelled_one_character_at_a_time():
|
||||
output_manager = run_command(
|
||||
"review_curr_word_spell", "hello world", cursor_x=2
|
||||
)
|
||||
|
||||
calls = output_manager.present_text.call_args_list
|
||||
assert [call.args[0] for call in calls] == list("hello")
|
||||
assert calls[0].kwargs == {
|
||||
"interrupt": True,
|
||||
"ignore_punctuation": True,
|
||||
"announce_capital": True,
|
||||
"flush": False,
|
||||
}
|
||||
assert all(call.kwargs["interrupt"] is False for call in calls[1:])
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_current_word_spelling_preserves_capitals_and_punctuation():
|
||||
output_manager = run_command("review_curr_word_spell", "Hi!", cursor_x=1)
|
||||
|
||||
assert [
|
||||
call.args[0] for call in output_manager.present_text.call_args_list
|
||||
] == ["H", "i", "!"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"command_name",
|
||||
["review_curr_word_spell", "review_curr_word_phonetic"],
|
||||
)
|
||||
def test_current_word_spelling_announces_blank_when_no_word_exists(
|
||||
command_name,
|
||||
):
|
||||
output_manager = run_command(command_name, " ")
|
||||
|
||||
output_manager.present_text.assert_called_once_with(
|
||||
"blank", interrupt=True, flush=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_current_word_phonetic_spelling_uses_phonetic_names():
|
||||
output_manager = run_command(
|
||||
"review_curr_word_phonetic", "Az!", cursor_x=1
|
||||
)
|
||||
|
||||
calls = output_manager.present_text.call_args_list
|
||||
assert [call.args[0] for call in calls] == ["Alpha", "zulu", "!"]
|
||||
assert calls[0].kwargs["interrupt"] is True
|
||||
assert all(call.kwargs["interrupt"] is False for call in calls[1:])
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
("layout", "character_keys", "word_keys"),
|
||||
[
|
||||
("desktop", ["KEY_KP2"], ["KEY_KP5"]),
|
||||
("laptop", ["KEY_COMMA", "KEY_FENRIR"], ["KEY_FENRIR", "KEY_K"]),
|
||||
],
|
||||
)
|
||||
def test_progressive_review_bindings(layout, character_keys, word_keys):
|
||||
bindings = load_bindings(layout)
|
||||
|
||||
assert bindings[str([1, character_keys])] == "REVIEW_CURR_CHAR"
|
||||
assert bindings[str([2, character_keys])] == "REVIEW_CURR_CHAR_PHONETIC"
|
||||
assert bindings[str([1, word_keys])] == "REVIEW_CURR_WORD"
|
||||
assert bindings[str([2, word_keys])] == "REVIEW_CURR_WORD_SPELL"
|
||||
assert bindings[str([3, word_keys])] == "REVIEW_CURR_WORD_PHONETIC"
|
||||
|
||||
removed_commands = {
|
||||
"REVIEW_PREV_CHAR_PHONETIC",
|
||||
"REVIEW_NEXT_CHAR_PHONETIC",
|
||||
"REVIEW_PREV_WORD_PHONETIC",
|
||||
"REVIEW_NEXT_WORD_PHONETIC",
|
||||
}
|
||||
assert removed_commands.isdisjoint(bindings.values())
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_obsolete_phonetic_navigation_commands_are_removed():
|
||||
removed_commands = [
|
||||
"review_prev_char_phonetic.py",
|
||||
"review_next_char_phonetic.py",
|
||||
"review_prev_word_phonetic.py",
|
||||
"review_next_word_phonetic.py",
|
||||
]
|
||||
|
||||
assert all(not (COMMANDS_DIR / name).exists() for name in removed_commands)
|
||||
@@ -427,6 +427,43 @@ def test_x11_handle_key_event_keeps_event_buffer_for_input_manager():
|
||||
event_queue.put.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
("event_type", "event_mode"),
|
||||
[
|
||||
(X.FocusOut, X.NotifyGrab),
|
||||
(X.FocusIn, X.NotifyUngrab),
|
||||
],
|
||||
)
|
||||
def test_x11_transient_grab_focus_events_preserve_input_state(
|
||||
event_type, event_mode
|
||||
):
|
||||
x11 = X11Driver()
|
||||
x11.active = True
|
||||
x11.clear_event_buffer = Mock()
|
||||
x11.reset_input_state = Mock()
|
||||
event = Mock(type=event_type, mode=event_mode)
|
||||
|
||||
x11.handle_x_event(event, Mock())
|
||||
|
||||
assert x11.active is True
|
||||
x11.clear_event_buffer.assert_not_called()
|
||||
x11.reset_input_state.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_normal_focus_out_resets_input_state():
|
||||
x11 = X11Driver()
|
||||
x11.active = True
|
||||
x11.reset_input_state = Mock()
|
||||
event = Mock(type=X.FocusOut, mode=X.NotifyNormal)
|
||||
|
||||
x11.handle_x_event(event, Mock())
|
||||
|
||||
assert x11.active is False
|
||||
x11.reset_input_state.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_fenrir_press_emits_modifiers_from_raw_state_first():
|
||||
x11 = X11Driver()
|
||||
@@ -485,3 +522,40 @@ def test_x11_fenrir_release_balances_synthesized_modifiers():
|
||||
assert fenrir_event["data"]["event_name"] == "KEY_KP0"
|
||||
assert modifier_event["data"]["event_name"] == "KEY_CTRL"
|
||||
assert modifier_event["data"]["event_state"] == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_fenrir_release_balances_held_chord_key():
|
||||
x11 = X11Driver()
|
||||
x11.active = True
|
||||
x11.fenrir_keys = {"KEY_KP0"}
|
||||
x11.interesting_keys = {"KEY_KP0", "KEY_KP9"}
|
||||
x11.env = {
|
||||
"input": {
|
||||
"curr_input": [],
|
||||
"event_buffer": [],
|
||||
},
|
||||
"runtime": {
|
||||
"InputManager": Mock(convert_event_name=lambda key: key),
|
||||
"DebugManager": Mock(),
|
||||
},
|
||||
}
|
||||
event_queue = Mock()
|
||||
key_names = {90: "KEY_KP0", 81: "KEY_KP9"}
|
||||
x11.keycode_to_key_name = Mock(
|
||||
side_effect=lambda keycode, state=0: key_names[keycode]
|
||||
)
|
||||
|
||||
x11.handle_x_event(
|
||||
Mock(type=X.KeyPress, detail=90, state=0), event_queue
|
||||
)
|
||||
x11.handle_x_event(
|
||||
Mock(type=X.KeyPress, detail=81, state=0), event_queue
|
||||
)
|
||||
x11.handle_x_event(
|
||||
Mock(type=X.KeyRelease, detail=90, state=0), event_queue
|
||||
)
|
||||
|
||||
synthetic_release = event_queue.put.call_args_list[-1].args[0]
|
||||
assert synthetic_release["data"]["event_name"] == "KEY_KP9"
|
||||
assert synthetic_release["data"]["event_state"] == 0
|
||||
|
||||
Reference in New Issue
Block a user