#!/usr/bin/env python3 """ Test Suite for Middle-Click Read-Aloud Service 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 TestMiddleClickReader(unittest.TestCase): """Test middle-click reader functionality""" def test_can_import_middle_click_reader(self): """Test that middle-click reader can be imported""" try: from dictation_service import middle_click_reader self.assertTrue(hasattr(middle_click_reader, 'MiddleClickReader')) self.assertTrue(hasattr(middle_click_reader, 'main')) except ImportError as e: self.fail(f"Cannot import middle-click reader: {e}") @patch('subprocess.run') def test_get_selected_text(self, mock_run): """Test getting selected text from xclip""" from dictation_service.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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.middle_click_reader 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()