feat(dictation): append trailing space after completed transcription (#608)
* feat(dictation): append trailing space after completed transcription
Fix cross-session glueing ("Hello.This") for push-to-talk and toggle
by appending a trailing space to each injected segment instead of relying
on in-session leading-space memory that is cleared on IDLE.
Adds an optional Settings toggle (default on) and preserves legacy
leading-space behavior when disabled. Newlines from voice commands are
left without a trailing space.
Fixes #605
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(dictation): honor trailing-space setting from disk live
Read append_trailing_space from config.json on each injection so the
Settings toggle applies without restart. main() and TrayIndicator each
own a ConfigManager, so an in-memory read missed Settings writes — same
pattern as TextInjector.copy_to_clipboard.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* test(dictation): align capitalize tests with trailing-space default
Update auto-capitalize expectations to include the default trailing
space, and add edge-case coverage for legacy mode, newlines, empty
input, and config-read failures so patch coverage clears 80%.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
This commit is contained in:
co-authored by
Jatin K Malik
Cursor Agent
parent
7422bf927e
commit
8322306aac
+51
-14
@@ -21,6 +21,30 @@ logger = logging.getLogger(__name__)
|
||||
# dependency checking to provide better error messages for pip/pipx users
|
||||
|
||||
|
||||
def _should_append_trailing_space() -> bool:
|
||||
"""Return whether completed transcriptions should get a trailing space.
|
||||
|
||||
Reads config.json from disk on each call so Settings toggles take effect
|
||||
immediately. TrayIndicator and main() each construct their own
|
||||
ConfigManager, so an in-memory read from main's instance would miss
|
||||
Settings writes (same pattern as TextInjector._should_copy_to_clipboard).
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
import os
|
||||
|
||||
from .utils.paths import config_dir
|
||||
|
||||
config_path = os.path.join(config_dir(), "config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r") as f:
|
||||
config = json.load(f)
|
||||
return bool(config.get("text_injection", {}).get("append_trailing_space", True))
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not read append_trailing_space setting: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(prog="vocalinux", description="Vocalinux")
|
||||
@@ -434,9 +458,9 @@ def main():
|
||||
#
|
||||
# text_callback(text: str)
|
||||
# Called on the recognition thread when a transcription segment
|
||||
# is finalised. The wrapper below strips whitespace, inserts
|
||||
# inter-segment spaces, injects the text, and records it so
|
||||
# "delete that" can undo it.
|
||||
# is finalised. The wrapper below strips whitespace, optionally
|
||||
# appends a trailing space (or legacy leading separator), injects
|
||||
# the text, and records it so "delete that" can undo it.
|
||||
#
|
||||
# action_callback(action: str) -> bool
|
||||
# Called when a voice command (e.g. "undo", "select all") is
|
||||
@@ -452,24 +476,20 @@ def main():
|
||||
"""Bridge between speech engine text events and the text injector.
|
||||
|
||||
Called on the recognition thread with each finalised transcription
|
||||
segment. Strips leading/trailing whitespace (whisper tokenizer
|
||||
sometimes prepends spaces), inserts a single space between
|
||||
consecutive segments, then injects via TextInjector.
|
||||
segment. Strips leading whitespace and trailing spaces/tabs (but
|
||||
preserves trailing newlines from voice commands), then either
|
||||
appends a trailing space (default) or uses the legacy in-session
|
||||
leading-space separator, and injects via TextInjector.
|
||||
|
||||
Args:
|
||||
text: Raw transcription segment from the speech engine.
|
||||
"""
|
||||
text_to_inject = text.strip()
|
||||
# Preserve trailing newlines ("new line" / "new paragraph"); only
|
||||
# strip spaces/tabs that whisper sometimes wraps around tokens.
|
||||
text_to_inject = text.lstrip().rstrip(" \t")
|
||||
if not text_to_inject:
|
||||
return
|
||||
|
||||
# Add a separating space between consecutive dictation segments,
|
||||
# but never for the very first segment (avoids unwanted leading space
|
||||
# when starting dictation in an empty text field).
|
||||
if action_handler.last_injected_text and action_handler.last_injected_text.strip():
|
||||
text_to_inject = " " + text_to_inject
|
||||
logger.debug("Added space separator before new segment")
|
||||
|
||||
# Auto-capitalize sentences if enabled (Vosk only - Whisper outputs proper casing)
|
||||
auto_capitalize = config_manager.get("text_injection", "auto_capitalize")
|
||||
if auto_capitalize and speech_engine.engine == "vosk":
|
||||
@@ -477,6 +497,23 @@ def main():
|
||||
|
||||
text_to_inject = capitalize_sentences(text_to_inject)
|
||||
|
||||
# Read from disk so the Settings toggle applies without restart
|
||||
# (main and tray each own a ConfigManager instance).
|
||||
append_trailing_space = _should_append_trailing_space()
|
||||
|
||||
if append_trailing_space:
|
||||
# Put the separator into the previous field so the next session
|
||||
# (push-to-talk / toggle) continues cleanly without needing
|
||||
# cross-session memory — and without a leading space in empty
|
||||
# fields. Skip after newlines from "new line" / "new paragraph".
|
||||
if not text_to_inject.endswith((" ", "\t", "\n")):
|
||||
text_to_inject += " "
|
||||
logger.debug("Appended trailing space after transcription segment")
|
||||
elif action_handler.last_injected_text and action_handler.last_injected_text.strip():
|
||||
# Legacy: leading space between consecutive in-session segments.
|
||||
text_to_inject = " " + text_to_inject
|
||||
logger.debug("Added space separator before new segment")
|
||||
|
||||
success = text_system.inject_text(text_to_inject)
|
||||
if success:
|
||||
action_handler.set_last_injected_text(text_to_inject)
|
||||
|
||||
@@ -75,6 +75,10 @@ DEFAULT_CONFIG = {
|
||||
"text_injection": {
|
||||
"copy_to_clipboard": False, # Disabled by default; users can enable in Settings
|
||||
"auto_capitalize": True, # Capitalize first letter of each sentence
|
||||
# Append a trailing space after each completed transcription segment so
|
||||
# the next dictation session (push-to-talk / toggle) continues cleanly
|
||||
# without glueing onto the previous sentence ("Hello.This").
|
||||
"append_trailing_space": True,
|
||||
},
|
||||
"advanced": {
|
||||
"power_user_mode": False,
|
||||
|
||||
@@ -1922,6 +1922,18 @@ class SettingsDialog(Gtk.Dialog):
|
||||
logger.info(f"Auto-capitalize {'enabled' if enabled else 'disabled'}")
|
||||
return False
|
||||
|
||||
def _on_append_trailing_space_toggled(self, widget, state):
|
||||
"""Handle toggle of the trailing space after dictation switch."""
|
||||
if self._initializing or self._applying_settings:
|
||||
return False
|
||||
|
||||
enabled = bool(state)
|
||||
logger.info(f"Append trailing space toggled: {enabled}")
|
||||
self.config_manager.set("text_injection", "append_trailing_space", enabled)
|
||||
self.config_manager.save_settings()
|
||||
logger.info(f"Append trailing space {'enabled' if enabled else 'disabled'}")
|
||||
return False
|
||||
|
||||
def _on_sound_effects_toggled(self, widget, state):
|
||||
if self._initializing or self._applying_settings:
|
||||
return False
|
||||
@@ -2200,9 +2212,26 @@ class SettingsDialog(Gtk.Dialog):
|
||||
)
|
||||
output_group.add_row(copy_to_clipboard_row)
|
||||
|
||||
self.append_trailing_space_switch = Gtk.Switch()
|
||||
self.append_trailing_space_switch.set_tooltip_text(
|
||||
"Append a space after each completed transcription so the next "
|
||||
"dictation session continues without gluing onto the previous text "
|
||||
'(e.g. "Hello. This" instead of "Hello.This").'
|
||||
)
|
||||
append_trailing_space_row = PreferenceRow(
|
||||
title="Trailing Space After Dictation",
|
||||
subtitle="Insert a space after each completed transcription segment",
|
||||
widget=self.append_trailing_space_switch,
|
||||
keywords=("space", "spacing", "punctuation", "push-to-talk"),
|
||||
)
|
||||
output_group.add_row(append_trailing_space_row)
|
||||
|
||||
self.recognition_settings_tab.pack_start(output_group, False, False, 0)
|
||||
self.copy_to_clipboard_switch.connect("state-set", self._on_copy_to_clipboard_toggled)
|
||||
self.auto_capitalize_switch.connect("state-set", self._on_auto_capitalize_toggled)
|
||||
self.append_trailing_space_switch.connect(
|
||||
"state-set", self._on_append_trailing_space_toggled
|
||||
)
|
||||
|
||||
if not silero_active:
|
||||
vad_info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
@@ -3131,11 +3160,13 @@ class SettingsDialog(Gtk.Dialog):
|
||||
start_minimized = ui_settings.get("start_minimized", False)
|
||||
copy_to_clipboard = text_injection_settings.get("copy_to_clipboard", False)
|
||||
auto_capitalize = text_injection_settings.get("auto_capitalize", True)
|
||||
append_trailing_space = text_injection_settings.get("append_trailing_space", True)
|
||||
|
||||
self.autostart_switch.set_active(autostart_enabled)
|
||||
self.start_minimized_switch.set_active(start_minimized)
|
||||
self.copy_to_clipboard_switch.set_active(copy_to_clipboard)
|
||||
self.auto_capitalize_switch.set_active(auto_capitalize)
|
||||
self.append_trailing_space_switch.set_active(append_trailing_space)
|
||||
self.sound_effects_switch.set_active(self.config_manager.is_sound_effects_enabled())
|
||||
|
||||
auto_pause_settings = self.config_manager.get_settings().get("auto_pause", {})
|
||||
|
||||
+178
-18
@@ -318,12 +318,14 @@ class TestMainModule(unittest.TestCase):
|
||||
text_callback("World")
|
||||
|
||||
calls = [call.args[0] for call in mock_text_instance.inject_text.call_args_list]
|
||||
self.assertEqual(calls, ["Hello.", " World"])
|
||||
self.assertEqual(calls, ["Hello. ", "World "])
|
||||
|
||||
state_callback(RecognitionState.IDLE)
|
||||
mock_text_instance.inject_text.reset_mock()
|
||||
text_callback("Next session")
|
||||
mock_text_instance.inject_text.assert_called_once_with("Next session")
|
||||
# Trailing space persists in the previous field; next session starts clean
|
||||
# (no leading space) but still gets its own trailing space.
|
||||
mock_text_instance.inject_text.assert_called_once_with("Next session ")
|
||||
|
||||
def _run_main_and_get_text_callback(
|
||||
self,
|
||||
@@ -402,7 +404,7 @@ class TestMainModule(unittest.TestCase):
|
||||
)
|
||||
|
||||
text_callback("hello world. goodbye")
|
||||
mock_text_instance.inject_text.assert_called_once_with("Hello world. Goodbye")
|
||||
mock_text_instance.inject_text.assert_called_once_with("Hello world. Goodbye ")
|
||||
|
||||
@patch("vocalinux.main.check_dependencies")
|
||||
@patch("vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager")
|
||||
@@ -431,7 +433,7 @@ class TestMainModule(unittest.TestCase):
|
||||
)
|
||||
|
||||
text_callback("hello world. goodbye")
|
||||
mock_text_instance.inject_text.assert_called_once_with("hello world. goodbye")
|
||||
mock_text_instance.inject_text.assert_called_once_with("hello world. goodbye ")
|
||||
|
||||
@patch("vocalinux.main.check_dependencies")
|
||||
@patch("vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager")
|
||||
@@ -460,7 +462,7 @@ class TestMainModule(unittest.TestCase):
|
||||
)
|
||||
|
||||
text_callback("hello world. goodbye")
|
||||
mock_text_instance.inject_text.assert_called_once_with("hello world. goodbye")
|
||||
mock_text_instance.inject_text.assert_called_once_with("hello world. goodbye ")
|
||||
|
||||
@patch("vocalinux.main.check_dependencies")
|
||||
@patch("vocalinux.ui.action_handler.ActionHandler")
|
||||
@@ -895,7 +897,7 @@ class TestMainConfigPrecedence(unittest.TestCase):
|
||||
class TestTextCallbackSpacing(unittest.TestCase):
|
||||
"""Test spacing logic in text_callback_wrapper."""
|
||||
|
||||
def _make_callback(self):
|
||||
def _make_callback(self, append_trailing_space: bool = True):
|
||||
"""Build text_callback_wrapper with mocked dependencies."""
|
||||
from vocalinux.ui.action_handler import ActionHandler
|
||||
|
||||
@@ -904,10 +906,13 @@ class TestTextCallbackSpacing(unittest.TestCase):
|
||||
action_handler = ActionHandler(text_system)
|
||||
|
||||
def text_callback_wrapper(text: str):
|
||||
text_to_inject = text.strip()
|
||||
text_to_inject = text.lstrip().rstrip(" \t")
|
||||
if not text_to_inject:
|
||||
return
|
||||
if action_handler.last_injected_text and action_handler.last_injected_text.strip():
|
||||
if append_trailing_space:
|
||||
if not text_to_inject.endswith((" ", "\t", "\n")):
|
||||
text_to_inject += " "
|
||||
elif action_handler.last_injected_text and action_handler.last_injected_text.strip():
|
||||
text_to_inject = " " + text_to_inject
|
||||
success = text_system.inject_text(text_to_inject)
|
||||
if success:
|
||||
@@ -919,25 +924,26 @@ class TestTextCallbackSpacing(unittest.TestCase):
|
||||
|
||||
return text_callback_wrapper, on_state_change, text_system, action_handler
|
||||
|
||||
def test_first_segment_has_no_leading_space(self):
|
||||
def test_first_segment_has_trailing_space(self):
|
||||
cb, _, text_system, _ = self._make_callback()
|
||||
cb("Hello world")
|
||||
text_system.inject_text.assert_called_once_with("Hello world")
|
||||
text_system.inject_text.assert_called_once_with("Hello world ")
|
||||
|
||||
def test_subsequent_segment_gets_space_separator(self):
|
||||
def test_subsequent_segment_has_trailing_space_not_leading(self):
|
||||
cb, _, text_system, _ = self._make_callback()
|
||||
cb("Hello")
|
||||
cb("world")
|
||||
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
|
||||
self.assertEqual(calls, ["Hello", " world"])
|
||||
self.assertEqual(calls, ["Hello ", "world "])
|
||||
|
||||
def test_reset_clears_leading_space(self):
|
||||
def test_cross_session_keeps_trailing_space_without_leading_space(self):
|
||||
cb, on_state_change, text_system, _ = self._make_callback()
|
||||
cb("first session")
|
||||
on_state_change(RecognitionState.IDLE)
|
||||
text_system.inject_text.reset_mock()
|
||||
cb("second session")
|
||||
text_system.inject_text.assert_called_once_with("second session")
|
||||
# No leading space (empty-field safe); trailing space still appended.
|
||||
text_system.inject_text.assert_called_once_with("second session ")
|
||||
|
||||
def test_whitespace_only_input_is_skipped(self):
|
||||
cb, _, text_system, _ = self._make_callback()
|
||||
@@ -947,22 +953,27 @@ class TestTextCallbackSpacing(unittest.TestCase):
|
||||
def test_input_with_leading_space_is_stripped(self):
|
||||
cb, _, text_system, _ = self._make_callback()
|
||||
cb(" Hello world")
|
||||
text_system.inject_text.assert_called_once_with("Hello world")
|
||||
text_system.inject_text.assert_called_once_with("Hello world ")
|
||||
|
||||
def test_multiple_segments_all_get_separators(self):
|
||||
def test_multiple_segments_all_get_trailing_spaces(self):
|
||||
cb, _, text_system, _ = self._make_callback()
|
||||
cb("one")
|
||||
cb("two")
|
||||
cb("three")
|
||||
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
|
||||
self.assertEqual(calls, ["one", " two", " three"])
|
||||
self.assertEqual(calls, ["one ", "two ", "three "])
|
||||
|
||||
def test_space_after_punctuation_segment(self):
|
||||
cb, _, text_system, _ = self._make_callback()
|
||||
cb("Hello.")
|
||||
cb("World")
|
||||
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
|
||||
self.assertEqual(calls, ["Hello.", " World"])
|
||||
self.assertEqual(calls, ["Hello. ", "World "])
|
||||
|
||||
def test_newline_segment_does_not_get_trailing_space(self):
|
||||
cb, _, text_system, _ = self._make_callback()
|
||||
cb("Hello.\n")
|
||||
text_system.inject_text.assert_called_once_with("Hello.\n")
|
||||
|
||||
def test_processing_to_listening_keeps_segment_spacing(self):
|
||||
cb, on_state_change, text_system, _ = self._make_callback()
|
||||
@@ -971,8 +982,157 @@ class TestTextCallbackSpacing(unittest.TestCase):
|
||||
on_state_change(RecognitionState.LISTENING)
|
||||
cb("World")
|
||||
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
|
||||
self.assertEqual(calls, ["Hello. ", "World "])
|
||||
|
||||
def test_legacy_mode_uses_leading_space_in_session(self):
|
||||
cb, _, text_system, _ = self._make_callback(append_trailing_space=False)
|
||||
cb("Hello.")
|
||||
cb("World")
|
||||
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
|
||||
self.assertEqual(calls, ["Hello.", " World"])
|
||||
|
||||
def test_legacy_mode_clears_leading_space_across_sessions(self):
|
||||
cb, on_state_change, text_system, _ = self._make_callback(append_trailing_space=False)
|
||||
cb("first session")
|
||||
on_state_change(RecognitionState.IDLE)
|
||||
text_system.inject_text.reset_mock()
|
||||
cb("second session")
|
||||
text_system.inject_text.assert_called_once_with("second session")
|
||||
|
||||
|
||||
class TestShouldAppendTrailingSpace(unittest.TestCase):
|
||||
"""Test disk-backed trailing-space setting reader."""
|
||||
|
||||
def test_reads_setting_from_disk_with_true_default(self):
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from vocalinux.main import _should_append_trailing_space
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with patch("vocalinux.utils.paths.config_dir", return_value=d):
|
||||
# No config file -> default True
|
||||
self.assertTrue(_should_append_trailing_space())
|
||||
|
||||
cfg = os.path.join(d, "config.json")
|
||||
with open(cfg, "w") as f:
|
||||
json.dump({"text_injection": {}}, f)
|
||||
self.assertTrue(_should_append_trailing_space())
|
||||
|
||||
with open(cfg, "w") as f:
|
||||
json.dump({"text_injection": {"append_trailing_space": False}}, f)
|
||||
self.assertFalse(_should_append_trailing_space())
|
||||
|
||||
with open(cfg, "w") as f:
|
||||
json.dump({"text_injection": {"append_trailing_space": True}}, f)
|
||||
self.assertTrue(_should_append_trailing_space())
|
||||
|
||||
def test_returns_true_when_config_read_fails(self):
|
||||
from vocalinux.main import _should_append_trailing_space
|
||||
|
||||
with patch("vocalinux.utils.paths.config_dir", side_effect=OSError("boom")):
|
||||
self.assertTrue(_should_append_trailing_space())
|
||||
|
||||
|
||||
class TestMainCallbackTrailingSpaceEdges(unittest.TestCase):
|
||||
"""Exercise trailing-space edge paths through the real main() callback."""
|
||||
|
||||
def _boot_under_patches(self, *, append_trailing_space: bool = True, inject_ok: bool = True):
|
||||
"""Return (exit_stack, text_cb, mock_text) with patches still active."""
|
||||
from contextlib import ExitStack
|
||||
|
||||
stack = ExitStack()
|
||||
stack.enter_context(patch("vocalinux.main.check_dependencies", return_value=True))
|
||||
mock_config_cls = stack.enter_context(patch("vocalinux.ui.config_manager.ConfigManager"))
|
||||
mock_config = MagicMock()
|
||||
mock_config.get_settings.return_value = {
|
||||
"speech_recognition": {},
|
||||
"general": {"first_run": False},
|
||||
}
|
||||
mock_config.get.return_value = False # auto_capitalize off
|
||||
mock_config_cls.return_value = mock_config
|
||||
|
||||
mock_speech_cls = stack.enter_context(
|
||||
patch("vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager")
|
||||
)
|
||||
mock_speech = MagicMock()
|
||||
mock_speech.engine = "whisper_cpp"
|
||||
mock_speech_cls.return_value = mock_speech
|
||||
|
||||
mock_text_cls = stack.enter_context(
|
||||
patch("vocalinux.text_injection.text_injector.TextInjector")
|
||||
)
|
||||
mock_text = MagicMock()
|
||||
mock_text.inject_text.return_value = inject_ok
|
||||
mock_text_cls.return_value = mock_text
|
||||
|
||||
mock_tray_cls = stack.enter_context(patch("vocalinux.ui.tray_indicator.TrayIndicator"))
|
||||
mock_tray_cls.return_value = MagicMock()
|
||||
|
||||
stack.enter_context(patch("vocalinux.ui.logging_manager.initialize_logging"))
|
||||
stack.enter_context(
|
||||
patch(
|
||||
"vocalinux.main._should_append_trailing_space",
|
||||
return_value=append_trailing_space,
|
||||
)
|
||||
)
|
||||
mock_parse = stack.enter_context(patch("vocalinux.main.parse_arguments"))
|
||||
stack.enter_context(patch("sys.argv", ["vocalinux"]))
|
||||
|
||||
mock_args = MagicMock()
|
||||
mock_args.debug = False
|
||||
mock_args.model = "tiny"
|
||||
mock_args.engine = "whisper_cpp"
|
||||
mock_args.language = "en-us"
|
||||
mock_args.wayland = False
|
||||
mock_args.start_minimized = False
|
||||
mock_parse.return_value = mock_args
|
||||
main()
|
||||
|
||||
text_cb = mock_speech.register_text_callback.call_args.args[0]
|
||||
return stack, text_cb, mock_text
|
||||
|
||||
def test_whitespace_only_is_skipped_through_main(self):
|
||||
stack, text_cb, mock_text = self._boot_under_patches()
|
||||
try:
|
||||
text_cb(" \t ")
|
||||
mock_text.inject_text.assert_not_called()
|
||||
finally:
|
||||
stack.close()
|
||||
|
||||
def test_newline_segment_skips_trailing_space_through_main(self):
|
||||
stack, text_cb, mock_text = self._boot_under_patches()
|
||||
try:
|
||||
text_cb("Hello.\n")
|
||||
mock_text.inject_text.assert_called_once_with("Hello.\n")
|
||||
finally:
|
||||
stack.close()
|
||||
|
||||
def test_legacy_mode_adds_leading_space_in_session(self):
|
||||
stack, text_cb, mock_text = self._boot_under_patches(append_trailing_space=False)
|
||||
try:
|
||||
text_cb("Hello.")
|
||||
text_cb("World")
|
||||
calls = [c.args[0] for c in mock_text.inject_text.call_args_list]
|
||||
self.assertEqual(calls, ["Hello.", " World"])
|
||||
finally:
|
||||
stack.close()
|
||||
|
||||
def test_failed_inject_does_not_remember_text(self):
|
||||
stack, text_cb, mock_text = self._boot_under_patches(
|
||||
append_trailing_space=False, inject_ok=False
|
||||
)
|
||||
try:
|
||||
text_cb("Hello.")
|
||||
mock_text.inject_text.reset_mock()
|
||||
mock_text.inject_text.return_value = True
|
||||
text_cb("World")
|
||||
# Failure means last_injected stays empty; next segment has no leading space
|
||||
mock_text.inject_text.assert_called_once_with("World")
|
||||
finally:
|
||||
stack.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user