Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
454b0f34c4 | ||
|
|
14227981cb | ||
|
|
5cf7a11260 | ||
|
|
312cb46f22 | ||
|
|
e180fd53fa | ||
|
|
81f1757357 |
@@ -59,15 +59,6 @@ pytest tests/ --cov=src/fenrirscreenreader --cov-report=html
|
|||||||
can run multiple foreground X terminal instances. Ask user to run tests that
|
can run multiple foreground X terminal instances. Ask user to run tests that
|
||||||
need real Fenrir instances or root access.
|
need real Fenrir instances or root access.
|
||||||
|
|
||||||
### Packaging Test Layouts
|
|
||||||
|
|
||||||
- Debian `pybuild` runs copied tests from a nested `.pybuild/.../build` tree
|
|
||||||
and may omit top-level source directories such as `config/`.
|
|
||||||
- Tests that inspect source resources must locate them by walking upward from
|
|
||||||
the resolved test path; do not assume a fixed `Path(__file__).parents[N]`.
|
|
||||||
- Packaging-sensitive test changes should be checked from both the source tree
|
|
||||||
and a representative nested build directory under `/tmp`.
|
|
||||||
|
|
||||||
## Creating Commands
|
## Creating Commands
|
||||||
|
|
||||||
Use `commands/command_template.py` as base. Required methods:
|
Use `commands/command_template.py` as base. Required methods:
|
||||||
|
|||||||
@@ -408,20 +408,6 @@ class command:
|
|||||||
text,
|
text,
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
if not interruptible_activity_match:
|
|
||||||
interruptible_activity_match = re.search(
|
|
||||||
(
|
|
||||||
r'^\s*(?:thinking|working|processing|analyzing|searching|'
|
|
||||||
r'reading|writing|planning|running|executing|updating|'
|
|
||||||
r'building|installing|compiling|downloading|reviewing|'
|
|
||||||
r'generating|responding|applying|fixing|editing|creating|'
|
|
||||||
r'preparing|checking|opening|loading|fetching|retrieving|'
|
|
||||||
r'scanning|indexing|summarizing)\b.*'
|
|
||||||
r'\([^)]*(?:esc|ctrl\+c) to interrupt[^)]*\)\s*$'
|
|
||||||
),
|
|
||||||
text,
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
if interruptible_activity_match:
|
if interruptible_activity_match:
|
||||||
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
|
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
|||||||
@@ -249,20 +249,13 @@ class FenrirManager:
|
|||||||
):
|
):
|
||||||
self.environment["runtime"]["InputManager"].clear_last_deep_input()
|
self.environment["runtime"]["InputManager"].clear_last_deep_input()
|
||||||
|
|
||||||
if not self._input_target_is_active():
|
|
||||||
self.environment["runtime"]["InputManager"].clear_last_deep_input()
|
|
||||||
return
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self._has_direct_user_input()
|
self.environment["runtime"][
|
||||||
and (
|
"CursorManager"
|
||||||
self.environment["runtime"][
|
].is_cursor_vertical_move()
|
||||||
"CursorManager"
|
or self.environment["runtime"][
|
||||||
].is_cursor_vertical_move()
|
"CursorManager"
|
||||||
or self.environment["runtime"][
|
].is_cursor_horizontal_move()
|
||||||
"CursorManager"
|
|
||||||
].is_cursor_horizontal_move()
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
self.environment["runtime"][
|
self.environment["runtime"][
|
||||||
"CommandManager"
|
"CommandManager"
|
||||||
@@ -271,22 +264,7 @@ class FenrirManager:
|
|||||||
self.environment["runtime"]["CommandManager"].execute_default_trigger(
|
self.environment["runtime"]["CommandManager"].execute_default_trigger(
|
||||||
"onScreenUpdate"
|
"onScreenUpdate"
|
||||||
)
|
)
|
||||||
|
self.environment["runtime"]["InputManager"].clear_last_deep_input()
|
||||||
def _input_target_is_active(self):
|
|
||||||
input_driver = self.environment["runtime"].get("InputDriver")
|
|
||||||
if input_driver is None:
|
|
||||||
return True
|
|
||||||
return input_driver.is_active()
|
|
||||||
|
|
||||||
def _has_direct_user_input(self):
|
|
||||||
deepest_input = self.environment["runtime"][
|
|
||||||
"InputManager"
|
|
||||||
].get_last_deepest_input()
|
|
||||||
if not deepest_input:
|
|
||||||
return False
|
|
||||||
return not any(
|
|
||||||
key in deepest_input for key in ("KEY_FENRIR", "KEY_SCRIPT")
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_plug_input_device(self, event):
|
def handle_plug_input_device(self, event):
|
||||||
try:
|
try:
|
||||||
@@ -310,8 +288,6 @@ class FenrirManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def handle_heart_beat(self, event):
|
def handle_heart_beat(self, event):
|
||||||
if not self._input_target_is_active():
|
|
||||||
return
|
|
||||||
self.environment["runtime"]["CommandManager"].execute_default_trigger(
|
self.environment["runtime"]["CommandManager"].execute_default_trigger(
|
||||||
"onHeartBeat", force=True
|
"onHeartBeat", force=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ class InputDriver:
|
|||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def is_active(self):
|
|
||||||
"""Return whether this input driver's target currently has focus."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def clear_event_buffer(self):
|
def clear_event_buffer(self):
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
version = "2026.09.12"
|
version = "2026.08.15"
|
||||||
code_name = "testing"
|
code_name = "master"
|
||||||
|
|||||||
@@ -267,9 +267,6 @@ class driver(inputDriver):
|
|||||||
return None
|
return None
|
||||||
return int(prop.value[0])
|
return int(prop.value[0])
|
||||||
|
|
||||||
def is_active(self):
|
|
||||||
return self.active
|
|
||||||
|
|
||||||
def format_window_id(self, window_id):
|
def format_window_id(self, window_id):
|
||||||
if window_id is None:
|
if window_id is None:
|
||||||
return "None"
|
return "None"
|
||||||
@@ -334,9 +331,6 @@ class driver(inputDriver):
|
|||||||
if event_type == X.FocusOut:
|
if event_type == X.FocusOut:
|
||||||
self.active = False
|
self.active = False
|
||||||
self.reset_input_state()
|
self.reset_input_state()
|
||||||
output_manager = self.env["runtime"].get("OutputManager")
|
|
||||||
if output_manager:
|
|
||||||
output_manager.interrupt_output_async()
|
|
||||||
return
|
return
|
||||||
if event_type not in [X.KeyPress, X.KeyRelease]:
|
if event_type not in [X.KeyPress, X.KeyRelease]:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -49,23 +49,6 @@ class PTYConstants:
|
|||||||
|
|
||||||
|
|
||||||
class FenrirScreen(pyte.Screen):
|
class FenrirScreen(pyte.Screen):
|
||||||
@property
|
|
||||||
def display(self):
|
|
||||||
def render(line):
|
|
||||||
skip_stub = False
|
|
||||||
for x in range(self.columns):
|
|
||||||
if skip_stub:
|
|
||||||
skip_stub = False
|
|
||||||
continue
|
|
||||||
char = line[x].data
|
|
||||||
if not char:
|
|
||||||
yield " "
|
|
||||||
continue
|
|
||||||
skip_stub = pyte.screens.wcwidth(char[0]) == 2
|
|
||||||
yield char
|
|
||||||
|
|
||||||
return ["".join(render(self.buffer[y])) for y in range(self.lines)]
|
|
||||||
|
|
||||||
def set_margins(self, *args, **kwargs):
|
def set_margins(self, *args, **kwargs):
|
||||||
kwargs.pop("private", None)
|
kwargs.pop("private", None)
|
||||||
super(FenrirScreen, self).set_margins(*args, **kwargs)
|
super(FenrirScreen, self).set_margins(*args, **kwargs)
|
||||||
@@ -462,10 +445,11 @@ class driver(screenDriver):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def record_stdin_keypress(self, msg_bytes):
|
def record_stdin_keypress(self, msg_bytes):
|
||||||
key_name = self.stdin_key_name(msg_bytes)
|
if msg_bytes != b"\t":
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
self.env["runtime"]["InputManager"].record_unmanaged_keypress(
|
self.env["runtime"]["InputManager"].record_unmanaged_keypress(
|
||||||
key_name
|
"KEY_TAB"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
@@ -473,38 +457,6 @@ class driver(screenDriver):
|
|||||||
debug.DebugLevel.ERROR,
|
debug.DebugLevel.ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
def stdin_key_name(self, msg_bytes):
|
|
||||||
key_map = {
|
|
||||||
b"\t": "KEY_TAB",
|
|
||||||
b"\x08": "KEY_BACKSPACE",
|
|
||||||
b"\x7f": "KEY_BACKSPACE",
|
|
||||||
b"\x1b[A": "KEY_UP",
|
|
||||||
b"\x1bOA": "KEY_UP",
|
|
||||||
b"\x1b[B": "KEY_DOWN",
|
|
||||||
b"\x1bOB": "KEY_DOWN",
|
|
||||||
b"\x1b[C": "KEY_RIGHT",
|
|
||||||
b"\x1bOC": "KEY_RIGHT",
|
|
||||||
b"\x1b[D": "KEY_LEFT",
|
|
||||||
b"\x1bOD": "KEY_LEFT",
|
|
||||||
b"\x1b[H": "KEY_HOME",
|
|
||||||
b"\x1bOH": "KEY_HOME",
|
|
||||||
b"\x1b[F": "KEY_END",
|
|
||||||
b"\x1bOF": "KEY_END",
|
|
||||||
b"\x1b[5~": "KEY_PAGEUP",
|
|
||||||
b"\x1b[6~": "KEY_PAGEDOWN",
|
|
||||||
b"\x1b[3~": "KEY_DELETE",
|
|
||||||
b"\r": "KEY_ENTER",
|
|
||||||
b"\n": "KEY_ENTER",
|
|
||||||
b" ": "KEY_SPACE",
|
|
||||||
}
|
|
||||||
if msg_bytes in key_map:
|
|
||||||
return key_map[msg_bytes]
|
|
||||||
if len(msg_bytes) == 1:
|
|
||||||
char = chr(msg_bytes[0])
|
|
||||||
if char.isascii() and char.isalnum():
|
|
||||||
return "KEY_" + char.upper()
|
|
||||||
return "KEY_TERMINAL_INPUT"
|
|
||||||
|
|
||||||
def synthesize_backspace_shortcut(self, msg_bytes, event_queue):
|
def synthesize_backspace_shortcut(self, msg_bytes, event_queue):
|
||||||
if not self.is_backspace_shortcut_sequence(msg_bytes):
|
if not self.is_backspace_shortcut_sequence(msg_bytes):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -68,14 +68,11 @@ def _run_command(command, display, input_text=None):
|
|||||||
input_bytes = None
|
input_bytes = None
|
||||||
if input_text is not None:
|
if input_text is not None:
|
||||||
input_bytes = input_text.encode(_ENCODING)
|
input_bytes = input_text.encode(_ENCODING)
|
||||||
output_target = (
|
|
||||||
subprocess.PIPE if input_text is None else subprocess.DEVNULL
|
|
||||||
)
|
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
command,
|
command,
|
||||||
input=input_bytes,
|
input=input_bytes,
|
||||||
stdout=output_target,
|
stdout=subprocess.PIPE,
|
||||||
stderr=output_target,
|
stderr=subprocess.PIPE,
|
||||||
env=_env_for_display(display),
|
env=_env_for_display(display),
|
||||||
timeout=_CLIPBOARD_TIMEOUT,
|
timeout=_CLIPBOARD_TIMEOUT,
|
||||||
check=False,
|
check=False,
|
||||||
@@ -83,9 +80,7 @@ def _run_command(command, display, input_text=None):
|
|||||||
|
|
||||||
|
|
||||||
def _command_error(command, result):
|
def _command_error(command, result):
|
||||||
stderr = (result.stderr or b"").decode(
|
stderr = result.stderr.decode(_ENCODING, errors="replace").strip()
|
||||||
_ENCODING, errors="replace"
|
|
||||||
).strip()
|
|
||||||
if stderr:
|
if stderr:
|
||||||
return RuntimeError(stderr)
|
return RuntimeError(stderr)
|
||||||
return RuntimeError(
|
return RuntimeError(
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
from configparser import ConfigParser
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def load_source_settings():
|
|
||||||
"""Load settings.conf from a checkout or a nested package build tree."""
|
|
||||||
for parent in Path(__file__).resolve().parents:
|
|
||||||
settings_path = parent / "config/settings/settings.conf"
|
|
||||||
if settings_path.is_file():
|
|
||||||
config = ConfigParser(interpolation=None)
|
|
||||||
config.read(settings_path)
|
|
||||||
return config
|
|
||||||
|
|
||||||
raise FileNotFoundError(
|
|
||||||
"Could not find config/settings/settings.conf above the test directory"
|
|
||||||
)
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import time
|
|
||||||
from unittest.mock import Mock, call
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from fenrirscreenreader.core.fenrirManager import FenrirManager
|
|
||||||
|
|
||||||
|
|
||||||
def _build_manager(input_active=True, deepest_input=None):
|
|
||||||
manager = FenrirManager.__new__(FenrirManager)
|
|
||||||
input_manager = Mock(
|
|
||||||
get_last_input_time=Mock(return_value=time.time()),
|
|
||||||
get_last_deepest_input=Mock(return_value=deepest_input or []),
|
|
||||||
)
|
|
||||||
command_manager = Mock(execute_default_trigger=Mock())
|
|
||||||
screen_manager = Mock(handle_screen_update=Mock())
|
|
||||||
cursor_manager = Mock(
|
|
||||||
is_cursor_vertical_move=Mock(return_value=True),
|
|
||||||
is_cursor_horizontal_move=Mock(return_value=False),
|
|
||||||
)
|
|
||||||
manager.environment = {
|
|
||||||
"runtime": {
|
|
||||||
"InputDriver": Mock(is_active=Mock(return_value=input_active)),
|
|
||||||
"InputManager": input_manager,
|
|
||||||
"ScreenManager": screen_manager,
|
|
||||||
"CursorManager": cursor_manager,
|
|
||||||
"CommandManager": command_manager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return manager, screen_manager, input_manager, command_manager
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_screen_update_while_x11_target_is_unfocused_updates_state_silently():
|
|
||||||
manager, screen_manager, input_manager, command_manager = _build_manager(
|
|
||||||
input_active=False,
|
|
||||||
deepest_input=["KEY_DOWN"],
|
|
||||||
)
|
|
||||||
event = {"data": {"text": "background repaint"}}
|
|
||||||
|
|
||||||
manager.handle_screen_update(event)
|
|
||||||
|
|
||||||
screen_manager.handle_screen_update.assert_called_once_with(event["data"])
|
|
||||||
command_manager.execute_default_trigger.assert_not_called()
|
|
||||||
input_manager.clear_last_deep_input.assert_called_once_with()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_programmatic_cursor_move_does_not_run_cursor_change_triggers():
|
|
||||||
manager, _screen_manager, _input_manager, command_manager = _build_manager(
|
|
||||||
deepest_input=[]
|
|
||||||
)
|
|
||||||
|
|
||||||
manager.handle_screen_update({"data": {"text": "spinner repaint"}})
|
|
||||||
|
|
||||||
command_manager.execute_default_trigger.assert_called_once_with(
|
|
||||||
"onScreenUpdate"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_navigation_cursor_move_runs_cursor_change_triggers():
|
|
||||||
manager, _screen_manager, _input_manager, command_manager = _build_manager(
|
|
||||||
deepest_input=["KEY_DOWN"]
|
|
||||||
)
|
|
||||||
|
|
||||||
manager.handle_screen_update({"data": {"text": "cursor moved"}})
|
|
||||||
|
|
||||||
assert command_manager.execute_default_trigger.call_args_list == [
|
|
||||||
call("onCursorChange"),
|
|
||||||
call("onScreenUpdate"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
@pytest.mark.parametrize("key_name", ["KEY_RIGHT", "KEY_BACKSPACE"])
|
|
||||||
def test_recent_cursor_input_survives_update_before_cursor_moves(key_name):
|
|
||||||
manager, _screen_manager, input_manager, command_manager = _build_manager(
|
|
||||||
deepest_input=[key_name]
|
|
||||||
)
|
|
||||||
manager.environment["runtime"][
|
|
||||||
"CursorManager"
|
|
||||||
].is_cursor_vertical_move.return_value = False
|
|
||||||
manager.environment["runtime"][
|
|
||||||
"CursorManager"
|
|
||||||
].is_cursor_horizontal_move.return_value = False
|
|
||||||
|
|
||||||
manager.handle_screen_update({"data": {"text": "intermediate repaint"}})
|
|
||||||
|
|
||||||
command_manager.execute_default_trigger.assert_called_once_with(
|
|
||||||
"onScreenUpdate"
|
|
||||||
)
|
|
||||||
input_manager.clear_last_deep_input.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_fenrir_review_command_does_not_enable_screen_cursor_speech():
|
|
||||||
manager, _screen_manager, _input_manager, command_manager = _build_manager(
|
|
||||||
deepest_input=["KEY_FENRIR", "KEY_KP8"]
|
|
||||||
)
|
|
||||||
|
|
||||||
manager.handle_screen_update({"data": {"text": "background repaint"}})
|
|
||||||
|
|
||||||
command_manager.execute_default_trigger.assert_called_once_with(
|
|
||||||
"onScreenUpdate"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_heartbeat_triggers_are_silent_while_x11_target_is_unfocused():
|
|
||||||
manager, _screen_manager, _input_manager, command_manager = _build_manager(
|
|
||||||
input_active=False
|
|
||||||
)
|
|
||||||
|
|
||||||
manager.handle_heart_beat({"data": None})
|
|
||||||
|
|
||||||
command_manager.execute_default_trigger.assert_not_called()
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -5,8 +7,6 @@ import pytest
|
|||||||
from fenrirscreenreader.core.settingsData import settings_data
|
from fenrirscreenreader.core.settingsData import settings_data
|
||||||
from fenrirscreenreader.soundDriver import genericDriver
|
from fenrirscreenreader.soundDriver import genericDriver
|
||||||
|
|
||||||
from .config_test_utils import load_source_settings
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsManager:
|
class SettingsManager:
|
||||||
def __init__(self, settings):
|
def __init__(self, settings):
|
||||||
@@ -26,7 +26,8 @@ def _sound_driver(settings):
|
|||||||
|
|
||||||
|
|
||||||
def _configured_sound_settings():
|
def _configured_sound_settings():
|
||||||
config = load_source_settings()
|
config = ConfigParser(interpolation=None)
|
||||||
|
config.read(Path(__file__).parents[2] / "config/settings/settings.conf")
|
||||||
return {
|
return {
|
||||||
"generic_play_file_command": config.get(
|
"generic_play_file_command": config.get(
|
||||||
"sound", "generic_play_file_command"
|
"sound", "generic_play_file_command"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from fenrirscreenreader.core.settingsData import settings_data
|
from fenrirscreenreader.core.settingsData import settings_data
|
||||||
from fenrirscreenreader.speechDriver import genericDriver
|
from fenrirscreenreader.speechDriver import genericDriver
|
||||||
|
|
||||||
from .config_test_utils import load_source_settings
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_speech_command_replaces_snake_case_placeholders():
|
def test_build_speech_command_replaces_snake_case_placeholders():
|
||||||
speech_driver = genericDriver.driver()
|
speech_driver = genericDriver.driver()
|
||||||
@@ -42,7 +43,8 @@ def test_build_speech_command_replaces_snake_case_placeholders():
|
|||||||
|
|
||||||
|
|
||||||
def _configured_speech_command():
|
def _configured_speech_command():
|
||||||
config = load_source_settings()
|
config = ConfigParser(interpolation=None)
|
||||||
|
config.read(Path(__file__).parents[2] / "config/settings/settings.conf")
|
||||||
return config.get("speech", "generic_speech_command")
|
return config.get("speech", "generic_speech_command")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -142,37 +142,6 @@ def test_progress_detector_beeps_for_interruptible_status_without_ellipsis():
|
|||||||
command.play_activity_beep.assert_called_once_with()
|
command.play_activity_beep.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_progress_detector_beeps_for_plain_working_elapsed_status():
|
|
||||||
progress_module = _load_progress_module()
|
|
||||||
command = progress_module.command()
|
|
||||||
sample = "Working (21s • esc to interrupt)"
|
|
||||||
command.env = {
|
|
||||||
"commandBuffer": {
|
|
||||||
"progress_monitoring": True,
|
|
||||||
"lastProgressValue": -1,
|
|
||||||
"lastProgressTime": 0,
|
|
||||||
},
|
|
||||||
"runtime": {
|
|
||||||
"DebugManager": Mock(write_debug_out=Mock()),
|
|
||||||
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
|
|
||||||
"CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)),
|
|
||||||
},
|
|
||||||
"screen": {
|
|
||||||
"new_delta": sample,
|
|
||||||
"new_delta_is_typing": False,
|
|
||||||
"new_content_text": sample,
|
|
||||||
"old_cursor": {"x": 0, "y": 0},
|
|
||||||
"new_cursor": {"x": 0, "y": 0},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
command.play_activity_beep = Mock()
|
|
||||||
|
|
||||||
command.run()
|
|
||||||
|
|
||||||
command.play_activity_beep.assert_called_once_with()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_progress_detector_allows_long_key_value_status_delta():
|
def test_progress_detector_allows_long_key_value_status_delta():
|
||||||
progress_module = _load_progress_module()
|
progress_module = _load_progress_module()
|
||||||
|
|||||||
@@ -39,26 +39,6 @@ def test_private_sgr_sequence_from_fullscreen_apps_does_not_crash():
|
|||||||
assert screen["text"].splitlines()[0] == "X "
|
assert screen["text"].splitlines()[0] == "X "
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_overwritten_wide_character_stub_renders_as_space():
|
|
||||||
terminal = Terminal(4, 1, DummyProcessInput())
|
|
||||||
|
|
||||||
terminal.feed("界\rX".encode())
|
|
||||||
screen = terminal.get_screen_content()
|
|
||||||
|
|
||||||
assert screen["text"] == "X "
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_wide_character_and_stub_keep_terminal_width():
|
|
||||||
terminal = Terminal(4, 1, DummyProcessInput())
|
|
||||||
|
|
||||||
terminal.feed("界".encode())
|
|
||||||
screen = terminal.get_screen_content()
|
|
||||||
|
|
||||||
assert screen["text"] == "界 "
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_optional_float_setting_uses_default_when_missing():
|
def test_optional_float_setting_uses_default_when_missing():
|
||||||
settings_manager = type(
|
settings_manager = type(
|
||||||
@@ -160,22 +140,7 @@ def test_pty_raw_tab_records_recent_tab_keypress():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@pytest.mark.parametrize(
|
def test_pty_plain_stdin_does_not_record_tab_keypress():
|
||||||
("sequence", "key_name"),
|
|
||||||
[
|
|
||||||
(b"a", "KEY_A"),
|
|
||||||
(b"\x7f", "KEY_BACKSPACE"),
|
|
||||||
(b"\x1b[D", "KEY_LEFT"),
|
|
||||||
(b"\x1bOD", "KEY_LEFT"),
|
|
||||||
(b"\x1b[C", "KEY_RIGHT"),
|
|
||||||
(b"\x1bOC", "KEY_RIGHT"),
|
|
||||||
(b"\x1b[A", "KEY_UP"),
|
|
||||||
(b"\x1bOA", "KEY_UP"),
|
|
||||||
(b"\x1b[B", "KEY_DOWN"),
|
|
||||||
(b"\x1bOB", "KEY_DOWN"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_pty_forwarded_stdin_records_recent_terminal_input(sequence, key_name):
|
|
||||||
pty_driver = PtyDriver()
|
pty_driver = PtyDriver()
|
||||||
settings_manager = Mock()
|
settings_manager = Mock()
|
||||||
settings_manager.get_setting_as_bool.return_value = False
|
settings_manager.get_setting_as_bool.return_value = False
|
||||||
@@ -190,10 +155,10 @@ def test_pty_forwarded_stdin_records_recent_terminal_input(sequence, key_name):
|
|||||||
}
|
}
|
||||||
pty_driver.inject_text_to_screen = Mock()
|
pty_driver.inject_text_to_screen = Mock()
|
||||||
|
|
||||||
pty_driver.handle_stdin_input(sequence, Mock())
|
pty_driver.handle_stdin_input(b"a", Mock())
|
||||||
|
|
||||||
input_manager.record_unmanaged_keypress.assert_called_once_with(key_name)
|
input_manager.record_unmanaged_keypress.assert_not_called()
|
||||||
pty_driver.inject_text_to_screen.assert_called_once_with(sequence)
|
pty_driver.inject_text_to_screen.assert_called_once_with(b"a")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
@@ -531,27 +531,12 @@ def test_x11_normal_focus_out_resets_input_state():
|
|||||||
x11 = X11Driver()
|
x11 = X11Driver()
|
||||||
x11.active = True
|
x11.active = True
|
||||||
x11.reset_input_state = Mock()
|
x11.reset_input_state = Mock()
|
||||||
output_manager = Mock()
|
|
||||||
x11.env = {"runtime": {"OutputManager": output_manager}}
|
|
||||||
event = Mock(type=X.FocusOut, mode=X.NotifyNormal)
|
event = Mock(type=X.FocusOut, mode=X.NotifyNormal)
|
||||||
|
|
||||||
x11.handle_x_event(event, Mock())
|
x11.handle_x_event(event, Mock())
|
||||||
|
|
||||||
assert x11.active is False
|
assert x11.active is False
|
||||||
x11.reset_input_state.assert_called_once_with()
|
x11.reset_input_state.assert_called_once_with()
|
||||||
output_manager.interrupt_output_async.assert_called_once_with()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_x11_reports_target_focus_state():
|
|
||||||
x11 = X11Driver()
|
|
||||||
x11.active = False
|
|
||||||
|
|
||||||
assert x11.is_active() is False
|
|
||||||
|
|
||||||
x11.active = True
|
|
||||||
|
|
||||||
assert x11.is_active() is True
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
@@ -37,25 +37,6 @@ def test_write_text_uses_display_env_without_mutating_process_env(
|
|||||||
assert os.environ.get("DISPLAY") is None
|
assert os.environ.get("DISPLAY") is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
def test_write_text_does_not_capture_daemonized_writer_pipes(monkeypatch):
|
|
||||||
monkeypatch.setattr(x_clipboard.shutil, "which", command_exists)
|
|
||||||
|
|
||||||
def run_command(command, **kwargs):
|
|
||||||
if (
|
|
||||||
kwargs["stdout"] == subprocess.PIPE
|
|
||||||
or kwargs["stderr"] == subprocess.PIPE
|
|
||||||
):
|
|
||||||
raise subprocess.TimeoutExpired(command, 2.0)
|
|
||||||
return subprocess.CompletedProcess(
|
|
||||||
command, 0, stdout=None, stderr=None
|
|
||||||
)
|
|
||||||
|
|
||||||
monkeypatch.setattr(x_clipboard.subprocess, "run", run_command)
|
|
||||||
|
|
||||||
assert x_clipboard.write_text("clipboard text", ":3") is True
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_read_text_scans_displays_until_text_is_found(monkeypatch):
|
def test_read_text_scans_displays_until_text_is_found(monkeypatch):
|
||||||
monkeypatch.delenv("DISPLAY", raising=False)
|
monkeypatch.delenv("DISPLAY", raising=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user