Improve tutorial help navigation
This commit is contained in:
@@ -101,6 +101,98 @@ def test_bare_key_taps_advance_shortcut_repeat(monkeypatch):
|
||||
assert manager.env["input"]["shortcut_repeat"] == 3
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tutorial_toggle_runs_while_tutorial_mode_is_active():
|
||||
manager = FenrirManager.__new__(FenrirManager)
|
||||
command_manager = Mock()
|
||||
command_manager.command_exists.return_value = False
|
||||
manager.environment = {
|
||||
"runtime": {
|
||||
"HelpManager": Mock(
|
||||
is_tutorial_mode=Mock(return_value=True)
|
||||
),
|
||||
"CommandManager": command_manager,
|
||||
"ReadAllManager": Mock(is_active=Mock(return_value=False)),
|
||||
"VmenuManager": Mock(get_active=Mock(return_value=False)),
|
||||
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
|
||||
}
|
||||
}
|
||||
|
||||
manager.handle_execute_command({"data": "TOGGLE_TUTORIAL_MODE"})
|
||||
|
||||
command_manager.run_command.assert_called_once_with(
|
||||
"TOGGLE_TUTORIAL_MODE", "commands"
|
||||
)
|
||||
command_manager.execute_command.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tutorial_mode_swallows_input_on_ignored_screen():
|
||||
manager, input_manager = create_handle_input_manager(
|
||||
no_key_pressed=False
|
||||
)
|
||||
manager.environment["runtime"]["HelpManager"].is_tutorial_mode.return_value = (
|
||||
True
|
||||
)
|
||||
manager.environment["runtime"]["ScreenManager"].is_ignored_screen.return_value = (
|
||||
True
|
||||
)
|
||||
|
||||
manager.handle_input(
|
||||
{"data": {"event_name": "KEY_A", "event_state": 1}}
|
||||
)
|
||||
|
||||
input_manager.clear_event_buffer.assert_called()
|
||||
input_manager.write_event_buffer.assert_not_called()
|
||||
input_manager.key_echo.assert_called_once_with(
|
||||
{"event_name": "KEY_A", "event_state": 1}
|
||||
)
|
||||
manager.detect_shortcut_command.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tutorial_mode_promotes_non_fenrir_chord_to_command():
|
||||
manager = FenrirManager.__new__(FenrirManager)
|
||||
manager.modifierInput = False
|
||||
manager.modifier_prefix_input = False
|
||||
manager.singleKeyCommand = False
|
||||
manager.command = ""
|
||||
input_manager = Mock(
|
||||
is_key_press=Mock(return_value=False),
|
||||
curr_input_has_command_modifier=Mock(return_value=False),
|
||||
curr_input_is_modifier_prefix=Mock(return_value=False),
|
||||
no_key_pressed=Mock(return_value=False),
|
||||
get_curr_shortcut=Mock(
|
||||
return_value=str([1, ["KEY_CTRL", "KEY_S"]])
|
||||
),
|
||||
get_command_for_shortcut=Mock(return_value="SPELL_CHECK"),
|
||||
)
|
||||
event_manager = Mock()
|
||||
manager.environment = {
|
||||
"input": {
|
||||
"key_forward": 0,
|
||||
"prev_input": ["KEY_CTRL"],
|
||||
"curr_input": ["KEY_CTRL", "KEY_S"],
|
||||
},
|
||||
"runtime": {
|
||||
"InputManager": input_manager,
|
||||
"EventManager": event_manager,
|
||||
"HelpManager": Mock(
|
||||
is_tutorial_mode=Mock(return_value=True)
|
||||
),
|
||||
"VmenuManager": Mock(get_active=Mock(return_value=False)),
|
||||
"DiffReviewManager": Mock(is_active=Mock(return_value=False)),
|
||||
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
|
||||
},
|
||||
}
|
||||
|
||||
manager.detect_shortcut_command()
|
||||
|
||||
event_manager.put_to_event_queue.assert_called_once_with(
|
||||
FenrirEventType.execute_command, "SPELL_CHECK"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_bare_key_taps_resolve_progressive_shortcuts(monkeypatch):
|
||||
manager = create_input_manager()
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import ast
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fenrirscreenreader.core.commandManager import CommandManager
|
||||
from fenrirscreenreader.core.helpManager import HelpManager
|
||||
|
||||
|
||||
def create_help_manager(capture_succeeds=True):
|
||||
manager = HelpManager()
|
||||
bindings = {
|
||||
str([1, ["KEY_F1", "KEY_FENRIR"]]): "TOGGLE_TUTORIAL_MODE",
|
||||
str([1, ["KEY_FENRIR", "KEY_S"]]): "SPELL_CHECK",
|
||||
str([1, ["KEY_FENRIR", "KEY_W"]]): "READ_WORD",
|
||||
str([2, ["KEY_FENRIR", "KEY_W"]]): "READ_WORD",
|
||||
str([1, ["KEY_SCRIPT", "KEY_U"]]): (
|
||||
"MUSICPLAYER__-__KEY_U"
|
||||
),
|
||||
}
|
||||
raw_bindings = {
|
||||
shortcut: ast.literal_eval(shortcut) for shortcut in bindings
|
||||
}
|
||||
command_manager = Mock()
|
||||
descriptions = {
|
||||
"SPELL_CHECK": "spell check",
|
||||
"READ_WORD": "read current word",
|
||||
"UNBOUND_ACTION": "an action without a shortcut",
|
||||
"MUSICPLAYER__-__KEY_U": "music player plugin",
|
||||
}
|
||||
command_manager.get_command_description.side_effect = (
|
||||
lambda command, section="commands": descriptions.get(command, "")
|
||||
)
|
||||
input_manager = Mock()
|
||||
input_manager.set_help_capture.return_value = capture_succeeds
|
||||
internal_command = Mock()
|
||||
internal_command.help_visible = False
|
||||
plugin_command = Mock()
|
||||
plugin_command.help_category = "plugins"
|
||||
manager.initialize(
|
||||
{
|
||||
"general": {"tutorialMode": False},
|
||||
"bindings": bindings.copy(),
|
||||
"rawBindings": {
|
||||
shortcut: [raw[0], raw[1].copy()]
|
||||
for shortcut, raw in raw_bindings.items()
|
||||
},
|
||||
"commands": {
|
||||
"commands": {
|
||||
"READ_WORD": Mock(),
|
||||
"SPELL_CHECK": Mock(),
|
||||
"TOGGLE_TUTORIAL_MODE": Mock(),
|
||||
"UNBOUND_ACTION": Mock(),
|
||||
"00_INIT_COMMANDS": internal_command,
|
||||
"MUSICPLAYER__-__KEY_U": plugin_command,
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"CommandManager": command_manager,
|
||||
"DebugManager": Mock(),
|
||||
"InputDriver": Mock(refresh_grabs=Mock()),
|
||||
"InputManager": input_manager,
|
||||
"VmenuManager": Mock(get_active=Mock(return_value=False)),
|
||||
},
|
||||
}
|
||||
)
|
||||
return manager, bindings, raw_bindings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_lists_bound_actions_once_with_all_shortcuts():
|
||||
manager, _bindings, raw_bindings = create_help_manager()
|
||||
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
active_text = " ".join(manager.help_lists["active"])
|
||||
assert active_text.count("read word") == 1
|
||||
assert "fenrir, w" in active_text
|
||||
assert "2 times fenrir, w" in active_text
|
||||
assert "unbound action" not in active_text
|
||||
assert "musicplayer" not in active_text
|
||||
assert "00 init commands" not in active_text
|
||||
assert all(
|
||||
"00 init commands" not in entry
|
||||
for entry in manager.help_lists["unbound"]
|
||||
)
|
||||
assert manager.help_lists["unbound"] == [
|
||||
"unbound action, Shortcuts unbound, Description an action without a shortcut"
|
||||
]
|
||||
assert manager.help_lists["plugins"] == [
|
||||
"musicplayer, Shortcuts script, u, Description music player plugin"
|
||||
]
|
||||
assert raw_bindings[str([1, ["KEY_FENRIR", "KEY_S"]])] == [
|
||||
1,
|
||||
["KEY_FENRIR", "KEY_S"],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_navigation_defaults_to_active_and_starts_before_first_item():
|
||||
manager, _bindings, _raw_bindings = create_help_manager()
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
assert manager.get_help_section_name() == "Active actions"
|
||||
assert manager.get_help_for_current_index() == "Active actions"
|
||||
|
||||
manager.next_index()
|
||||
assert manager.get_help_for_current_index() == manager.help_lists["active"][0]
|
||||
|
||||
assert manager.select_help_section("unbound") == "Unbound actions"
|
||||
assert manager.get_help_for_current_index() == "Unbound actions"
|
||||
manager.prev_index()
|
||||
assert manager.get_help_for_current_index() == manager.help_lists["unbound"][-1]
|
||||
|
||||
assert manager.next_help_section() == "Plugins"
|
||||
assert manager.next_help_section() == "Active actions"
|
||||
assert manager.prev_help_section() == "Plugins"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_installs_and_restores_modal_bindings_exactly():
|
||||
manager, bindings, raw_bindings = create_help_manager()
|
||||
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
assert (
|
||||
manager.env["bindings"][str([1, ["KEY_LEFT"]])]
|
||||
== "PREV_HELP_SECTION"
|
||||
)
|
||||
assert (
|
||||
manager.env["bindings"][str([1, ["KEY_RIGHT"]])]
|
||||
== "NEXT_HELP_SECTION"
|
||||
)
|
||||
assert manager.env["bindings"][str([1, ["KEY_ESC"]])] == "TOGGLE_TUTORIAL_MODE"
|
||||
assert manager.env["rawBindings"][str([1, ["KEY_RIGHT"]])] == [
|
||||
1,
|
||||
["KEY_RIGHT"],
|
||||
]
|
||||
manager.env["runtime"]["InputManager"].set_help_capture.assert_called_once_with(
|
||||
True
|
||||
)
|
||||
|
||||
manager.set_tutorial_mode(False)
|
||||
|
||||
assert manager.env["bindings"] == bindings
|
||||
assert manager.env["rawBindings"] == raw_bindings
|
||||
manager.env["runtime"]["InputManager"].set_help_capture.assert_called_with(
|
||||
False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_records_degraded_capture_without_refusing_mode():
|
||||
manager, _bindings, _raw_bindings = create_help_manager(
|
||||
capture_succeeds=False
|
||||
)
|
||||
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
assert manager.is_tutorial_mode() is True
|
||||
assert manager.is_capture_degraded() is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_does_not_claim_exit_when_capture_release_fails():
|
||||
manager, bindings, raw_bindings = create_help_manager()
|
||||
manager.set_tutorial_mode(True)
|
||||
manager.env["runtime"]["InputManager"].set_help_capture.return_value = False
|
||||
|
||||
assert manager.set_tutorial_mode(False) is False
|
||||
|
||||
assert manager.is_tutorial_mode() is True
|
||||
assert manager.env["bindings"] != bindings
|
||||
assert manager.env["rawBindings"] != raw_bindings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_command_description_is_presented_on_ignored_screen_in_help():
|
||||
command_manager = CommandManager()
|
||||
output_manager = Mock()
|
||||
command_manager.env = {
|
||||
"runtime": {
|
||||
"ScreenManager": Mock(
|
||||
is_ignored_screen=Mock(return_value=True)
|
||||
),
|
||||
"HelpManager": Mock(
|
||||
is_tutorial_mode=Mock(return_value=True)
|
||||
),
|
||||
"DebugManager": Mock(),
|
||||
"OutputManager": output_manager,
|
||||
}
|
||||
}
|
||||
command_manager.command_exists = Mock(return_value=True)
|
||||
command_manager.get_command_description = Mock(
|
||||
return_value="spell check"
|
||||
)
|
||||
|
||||
command_manager.execute_command("SPELL_CHECK", "commands")
|
||||
|
||||
output_manager.present_text.assert_called_once_with(
|
||||
"spell check", interrupt=False
|
||||
)
|
||||
@@ -126,6 +126,14 @@ def test_progressive_review_bindings(layout, character_keys, word_keys):
|
||||
assert bindings[str([2, word_keys])] == "REVIEW_CURR_WORD_SPELL"
|
||||
assert bindings[str([3, word_keys])] == "REVIEW_CURR_WORD_PHONETIC"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("layout", ["desktop", "laptop"])
|
||||
def test_layouts_do_not_bind_removed_keyboard_layout_cycle(layout):
|
||||
bindings = load_bindings(layout)
|
||||
|
||||
assert "CYCLE_KEYBOARD_LAYOUT" not in bindings.values()
|
||||
|
||||
removed_commands = {
|
||||
"REVIEW_PREV_CHAR_PHONETIC",
|
||||
"REVIEW_NEXT_CHAR_PHONETIC",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
COMMAND_PATH = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src"
|
||||
/ "fenrirscreenreader"
|
||||
/ "commands"
|
||||
/ "commands"
|
||||
/ "toggle_vmenu_mode.py"
|
||||
)
|
||||
|
||||
|
||||
def load_command():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"fenrir_toggle_vmenu_mode", COMMAND_PATH
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module.command()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_description_does_not_toggle_vmenu_mode():
|
||||
vmenu_manager = Mock()
|
||||
command = load_command()
|
||||
command.initialize({"runtime": {"VmenuManager": vmenu_manager}})
|
||||
|
||||
assert command.get_description() == "Entering or Leaving v menu mode."
|
||||
vmenu_manager.toggle_vmenu_mode.assert_not_called()
|
||||
@@ -189,6 +189,29 @@ def test_x11_should_emit_unbound_mapped_keys_for_speech_interrupt():
|
||||
assert x11.should_emit_key("KEY_ENTER") is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
("keysym_name", "key_name"),
|
||||
[
|
||||
("-", "KEY_MINUS"),
|
||||
("=", "KEY_EQUAL"),
|
||||
("[", "KEY_LEFTBRACE"),
|
||||
("]", "KEY_RIGHTBRACE"),
|
||||
("\\", "KEY_BACKSLASH"),
|
||||
(";", "KEY_SEMICOLON"),
|
||||
("'", "KEY_APOSTROPHE"),
|
||||
("`", "KEY_GRAVE"),
|
||||
(",", "KEY_COMMA"),
|
||||
(".", "KEY_DOT"),
|
||||
("/", "KEY_SLASH"),
|
||||
],
|
||||
)
|
||||
def test_x11_maps_literal_punctuation_keysyms(keysym_name, key_name):
|
||||
x11 = X11Driver()
|
||||
|
||||
assert x11.keysym_name_to_key_name(keysym_name) == key_name
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_build_passive_grabs_for_fenrir_keys_and_shortcuts():
|
||||
x11 = X11Driver()
|
||||
@@ -372,6 +395,58 @@ def test_x11_write_event_buffer_does_not_replay_key_release():
|
||||
assert x11.env["input"]["event_buffer"] == []
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_help_capture_grabs_and_releases_keyboard():
|
||||
x11 = X11Driver()
|
||||
x11._initialized = True
|
||||
x11.window = Mock()
|
||||
x11.window.grab_keyboard.return_value = X.GrabSuccess
|
||||
x11.display = Mock()
|
||||
x11.env = {"runtime": {"DebugManager": Mock()}}
|
||||
|
||||
assert x11.set_help_capture(True) is True
|
||||
x11.window.grab_keyboard.assert_called_once_with(
|
||||
False,
|
||||
X.GrabModeAsync,
|
||||
X.GrabModeAsync,
|
||||
X.CurrentTime,
|
||||
)
|
||||
assert x11.help_keyboard_grabbed is True
|
||||
|
||||
assert x11.set_help_capture(False) is True
|
||||
x11.display.ungrab_keyboard.assert_called_once_with(X.CurrentTime)
|
||||
x11.display.sync.assert_called_once_with()
|
||||
assert x11.help_keyboard_grabbed is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_help_capture_reports_failed_grab():
|
||||
x11 = X11Driver()
|
||||
x11._initialized = True
|
||||
x11.window = Mock()
|
||||
x11.window.grab_keyboard.return_value = X.AlreadyGrabbed
|
||||
x11.display = Mock()
|
||||
x11.env = {"runtime": {"DebugManager": Mock()}}
|
||||
|
||||
assert x11.set_help_capture(True) is False
|
||||
assert x11.help_keyboard_grabbed is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_help_capture_retries_failed_keyboard_release():
|
||||
x11 = X11Driver()
|
||||
x11._initialized = True
|
||||
x11.help_keyboard_grabbed = True
|
||||
x11.window = Mock()
|
||||
x11.display = Mock()
|
||||
x11.display.sync.side_effect = RuntimeError("ungrab failed")
|
||||
x11.env = {"runtime": {"DebugManager": Mock()}}
|
||||
|
||||
assert x11.set_help_capture(False) is False
|
||||
assert x11.display.ungrab_keyboard.call_count == 3
|
||||
assert x11.help_keyboard_grabbed is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_map_event_keeps_x_event_time_for_replay():
|
||||
x11 = X11Driver()
|
||||
|
||||
Reference in New Issue
Block a user