- Rename middle-click-reader to read-aloud service - Change hotkey from Ctrl+middle-click to Alt+R - Replace edge-tts with Piper TTS for local neural voices - Update desktop and service files - Add piper-tts dependency - Update tests and setup scripts
206 lines
7.2 KiB
Python
206 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Test Suite for Read-Aloud Service (Alt+R)
|
|
Tests on-demand text-to-speech functionality
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
import tempfile
|
|
from unittest.mock import Mock, patch, MagicMock, call
|
|
|
|
# Add src to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
|
|
class TestReadAloud(unittest.TestCase):
|
|
"""Test read-aloud service functionality"""
|
|
|
|
def test_can_import_read_aloud(self):
|
|
"""Test that read-aloud service can be imported"""
|
|
try:
|
|
from dictation_service import read_aloud
|
|
self.assertTrue(hasattr(read_aloud, 'MiddleClickReader'))
|
|
self.assertTrue(hasattr(read_aloud, 'main'))
|
|
except ImportError as e:
|
|
self.fail(f"Cannot import read-aloud service: {e}")
|
|
|
|
@patch('subprocess.run')
|
|
def test_get_selected_text(self, mock_run):
|
|
"""Test getting selected text from xclip"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
|
|
reader = MiddleClickReader()
|
|
|
|
# Mock xclip returning selected text
|
|
mock_run.return_value = Mock(returncode=0, stdout="Hello World")
|
|
result = reader.get_selected_text()
|
|
|
|
# Verify xclip was called correctly
|
|
mock_run.assert_called_once()
|
|
call_args = mock_run.call_args
|
|
self.assertIn('xclip', call_args[0][0])
|
|
self.assertIn('primary', call_args[0][0])
|
|
|
|
@patch('subprocess.run')
|
|
@patch('tempfile.NamedTemporaryFile')
|
|
@patch('os.path.exists')
|
|
@patch('os.remove')
|
|
def test_read_text(self, mock_remove, mock_exists, mock_temp, mock_run):
|
|
"""Test reading text with edge-tts"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
|
|
reader = MiddleClickReader()
|
|
|
|
# Setup mocks
|
|
mock_temp_file = MagicMock()
|
|
mock_temp_file.name = '/tmp/test.mp3'
|
|
mock_temp.__enter__ = Mock(return_value=mock_temp_file)
|
|
mock_temp.__exit__ = Mock(return_value=False)
|
|
mock_exists.return_value = True
|
|
mock_run.return_value = Mock(returncode=0)
|
|
|
|
# Test reading text
|
|
reader.read_text("Hello World")
|
|
|
|
# Verify TTS was called
|
|
self.assertTrue(mock_run.called)
|
|
|
|
# Check that edge-tts command was used
|
|
calls = [call[0][0] for call in mock_run.call_args_list]
|
|
edge_tts_called = any('edge-tts' in str(cmd) for cmd in calls)
|
|
self.assertTrue(edge_tts_called or mock_run.called)
|
|
|
|
def test_minimum_text_length(self):
|
|
"""Test that short text is not read"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
|
|
reader = MiddleClickReader()
|
|
|
|
with patch('subprocess.run') as mock_run:
|
|
# Text too short should not trigger TTS
|
|
reader.read_text("a")
|
|
reader.read_text("")
|
|
|
|
# Should not have called edge-tts
|
|
# (only xclip might be called)
|
|
edge_tts_calls = [
|
|
call for call in mock_run.call_args_list
|
|
if 'edge-tts' in str(call)
|
|
]
|
|
self.assertEqual(len(edge_tts_calls), 0)
|
|
|
|
def test_lock_file_creation(self):
|
|
"""Test that lock file is created during reading"""
|
|
from dictation_service.read_aloud import LOCK_FILE
|
|
|
|
# Verify lock file path
|
|
self.assertEqual(LOCK_FILE, "/tmp/dictation_speaking.lock")
|
|
|
|
@patch('pynput.mouse.Listener')
|
|
def test_mouse_listener_initialization(self, mock_listener):
|
|
"""Test that mouse listener can be initialized"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
|
|
reader = MiddleClickReader()
|
|
|
|
# Mock listener
|
|
mock_listener_instance = MagicMock()
|
|
mock_listener.return_value.__enter__ = Mock(return_value=mock_listener_instance)
|
|
mock_listener.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
# This would normally block, so we just test initialization
|
|
self.assertIsNotNone(reader)
|
|
|
|
def test_middle_click_detection(self):
|
|
"""Test middle-click detection logic"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
from pynput import mouse
|
|
|
|
reader = MiddleClickReader()
|
|
reader.ctrl_pressed = True # Simulate Ctrl being held
|
|
|
|
with patch.object(reader, 'get_selected_text', return_value="Test text"):
|
|
with patch.object(reader, 'read_text') as mock_read:
|
|
# Simulate Ctrl+middle-click press
|
|
reader.on_click(100, 100, mouse.Button.middle, True)
|
|
|
|
# Should have called read_text (in a thread, so wait a moment)
|
|
import time
|
|
time.sleep(0.1)
|
|
mock_read.assert_called_once_with("Test text")
|
|
|
|
def test_ignores_non_middle_clicks(self):
|
|
"""Test that non-middle clicks are ignored"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
from pynput import mouse
|
|
|
|
reader = MiddleClickReader()
|
|
|
|
with patch.object(reader, 'get_selected_text') as mock_get:
|
|
with patch.object(reader, 'read_text') as mock_read:
|
|
# Simulate left click
|
|
reader.on_click(100, 100, mouse.Button.left, True)
|
|
|
|
# Should not have called get_selected_text or read_text
|
|
mock_get.assert_not_called()
|
|
mock_read.assert_not_called()
|
|
|
|
def test_concurrent_reading_prevention(self):
|
|
"""Test that concurrent reading is prevented"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
|
|
reader = MiddleClickReader()
|
|
|
|
# Set reading flag
|
|
reader.is_reading = True
|
|
|
|
with patch('subprocess.run') as mock_run:
|
|
# Try to read while already reading
|
|
reader.read_text("Test text")
|
|
|
|
# Should not have called subprocess
|
|
mock_run.assert_not_called()
|
|
|
|
|
|
class TestEdgeTTSIntegration(unittest.TestCase):
|
|
"""Test Edge-TTS integration"""
|
|
|
|
@patch('subprocess.run')
|
|
def test_edge_tts_voice_configuration(self, mock_run):
|
|
"""Test that correct voice is used"""
|
|
from dictation_service.read_aloud import EDGE_TTS_VOICE
|
|
|
|
# Verify default voice
|
|
self.assertEqual(EDGE_TTS_VOICE, "en-US-ChristopherNeural")
|
|
|
|
@patch('subprocess.run')
|
|
def test_mpv_playback(self, mock_run):
|
|
"""Test that mpv is used for playback"""
|
|
from dictation_service.read_aloud import MiddleClickReader
|
|
|
|
reader = MiddleClickReader()
|
|
reader.is_reading = False
|
|
|
|
with patch('tempfile.NamedTemporaryFile') as mock_temp:
|
|
mock_temp_file = MagicMock()
|
|
mock_temp_file.name = '/tmp/test.mp3'
|
|
mock_temp.return_value.__enter__ = Mock(return_value=mock_temp_file)
|
|
mock_temp.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
with patch('os.path.exists', return_value=True):
|
|
with patch('os.remove'):
|
|
mock_run.return_value = Mock(returncode=0)
|
|
|
|
reader.read_text("Test text")
|
|
|
|
# Check that mpv was called
|
|
calls = [str(call) for call in mock_run.call_args_list]
|
|
mpv_called = any('mpv' in call for call in calls)
|
|
self.assertTrue(mpv_called or mock_run.called)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|