EU-Utility/tests/unit/test_clipboard.py

369 lines
12 KiB
Python

"""
Unit tests for Clipboard Manager service.
Tests cover:
- Singleton pattern
- Copy/paste operations
- History management
- Persistence (save/load)
- Availability checking
"""
import sys
import unittest
import json
import tempfile
import shutil
from pathlib import Path
from unittest.mock import MagicMock, patch, mock_open
from datetime import datetime
# Add project root to path
project_root = Path(__file__).parent.parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from core.clipboard import ClipboardManager, get_clipboard_manager, ClipboardEntry, copy_to_clipboard, paste_from_clipboard
class TestClipboardManagerSingleton(unittest.TestCase):
"""Test ClipboardManager singleton behavior."""
def tearDown(self):
"""Reset singleton after each test."""
ClipboardManager._instance = None
def test_singleton_instance(self):
"""Test that ClipboardManager is a proper singleton."""
cm1 = get_clipboard_manager()
cm2 = get_clipboard_manager()
self.assertIs(cm1, cm2)
self.assertIsInstance(cm1, ClipboardManager)
def test_singleton_reset(self):
"""Test that singleton can be reset."""
cm1 = get_clipboard_manager()
ClipboardManager._instance = None
cm2 = get_clipboard_manager()
self.assertIsNot(cm1, cm2)
class TestClipboardManagerInitialization(unittest.TestCase):
"""Test ClipboardManager initialization."""
def tearDown(self):
"""Reset singleton after each test."""
ClipboardManager._instance = None
def test_initialization_default_values(self):
"""Test that initialization sets default values."""
cm = ClipboardManager()
self.assertEqual(cm._max_history, 100)
self.assertEqual(cm._history.maxlen, 100)
self.assertEqual(cm._history_file, Path("data/clipboard_history.json"))
self.assertFalse(cm._monitoring)
def test_initialization_custom_values(self):
"""Test initialization with custom values."""
custom_path = Path("/custom/history.json")
cm = ClipboardManager(max_history=50, history_file=custom_path)
self.assertEqual(cm._max_history, 50)
self.assertEqual(cm._history.maxlen, 50)
self.assertEqual(cm._history_file, custom_path)
class TestClipboardOperations(unittest.TestCase):
"""Test clipboard copy and paste operations."""
def tearDown(self):
"""Reset singleton after each test."""
ClipboardManager._instance = None
@patch('pyperclip.copy')
def test_copy_success(self, mock_copy):
"""Test successful copy operation."""
cm = ClipboardManager()
cm._history_file = MagicMock() # Prevent file operations
result = cm.copy("test text", source="test")
self.assertTrue(result)
mock_copy.assert_called_once_with("test text")
self.assertEqual(len(cm._history), 1)
@patch('pyperclip.copy')
def test_copy_adds_to_history(self, mock_copy):
"""Test that copy adds entry to history."""
cm = ClipboardManager()
cm._history_file = MagicMock()
cm.copy("test text", source="test")
entry = cm._history[0]
self.assertEqual(entry.text, "test text")
self.assertEqual(entry.source, "test")
self.assertIsNotNone(entry.timestamp)
@patch('pyperclip.copy')
@patch('core.clipboard.print')
def test_copy_failure(self, mock_print, mock_copy):
"""Test copy failure handling."""
mock_copy.side_effect = Exception("Copy failed")
cm = ClipboardManager()
result = cm.copy("test text")
self.assertFalse(result)
@patch('pyperclip.paste')
def test_paste_success(self, mock_paste):
"""Test successful paste operation."""
mock_paste.return_value = "pasted text"
cm = ClipboardManager()
result = cm.paste()
self.assertEqual(result, "pasted text")
@patch('pyperclip.paste')
def test_paste_empty(self, mock_paste):
"""Test paste with empty clipboard."""
mock_paste.return_value = None
cm = ClipboardManager()
result = cm.paste()
self.assertEqual(result, "")
@patch('pyperclip.paste')
@patch('core.clipboard.print')
def test_paste_failure(self, mock_print, mock_paste):
"""Test paste failure handling."""
mock_paste.side_effect = Exception("Paste failed")
cm = ClipboardManager()
result = cm.paste()
self.assertEqual(result, "")
class TestHistoryManagement(unittest.TestCase):
"""Test clipboard history management."""
def tearDown(self):
"""Reset singleton after each test."""
ClipboardManager._instance = None
def test_get_history_empty(self):
"""Test getting history when empty."""
cm = ClipboardManager()
history = cm.get_history()
self.assertEqual(history, [])
def test_get_history_with_limit(self):
"""Test getting history with limit."""
cm = ClipboardManager()
# Add 5 entries
for i in range(5):
cm._history.append(ClipboardEntry(text=f"text{i}", timestamp=datetime.now().isoformat()))
history = cm.get_history(limit=3)
self.assertEqual(len(history), 3)
# Should return newest first
self.assertEqual(history[0].text, "text2")
self.assertEqual(history[2].text, "text4")
def test_get_history_order(self):
"""Test that history is returned newest first."""
cm = ClipboardManager()
cm._history.append(ClipboardEntry(text="old", timestamp="2024-01-01T00:00:00"))
cm._history.append(ClipboardEntry(text="new", timestamp="2024-01-02T00:00:00"))
history = cm.get_history()
self.assertEqual(history[0].text, "new")
self.assertEqual(history[1].text, "old")
def test_clear_history(self):
"""Test clearing history."""
cm = ClipboardManager()
cm._history_file = MagicMock()
cm._history.append(ClipboardEntry(text="test", timestamp=datetime.now().isoformat()))
self.assertEqual(len(cm._history), 1)
cm.clear_history()
self.assertEqual(len(cm._history), 0)
def test_history_max_size(self):
"""Test that history respects max size."""
cm = ClipboardManager(max_history=3)
cm._history_file = MagicMock()
# Add 5 entries
for i in range(5):
cm._history.append(ClipboardEntry(text=f"text{i}", timestamp=datetime.now().isoformat()))
# Should only keep last 3
self.assertEqual(len(cm._history), 3)
class TestPersistence(unittest.TestCase):
"""Test history persistence."""
def setUp(self):
"""Create temporary directory for tests."""
self.temp_dir = tempfile.mkdtemp()
self.history_file = Path(self.temp_dir) / "history.json"
def tearDown(self):
"""Clean up temporary directory."""
shutil.rmtree(self.temp_dir)
ClipboardManager._instance = None
def test_save_history(self):
"""Test saving history to file."""
cm = ClipboardManager(history_file=self.history_file)
cm._history.append(ClipboardEntry(text="test", timestamp="2024-01-01T00:00:00", source="test"))
cm._save_history()
self.assertTrue(self.history_file.exists())
with open(self.history_file, 'r') as f:
data = json.load(f)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]['text'], "test")
def test_load_history(self):
"""Test loading history from file."""
# Create history file
data = [
{"text": "saved entry", "timestamp": "2024-01-01T00:00:00", "source": "file"}
]
self.history_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.history_file, 'w') as f:
json.dump(data, f)
cm = ClipboardManager(history_file=self.history_file)
self.assertEqual(len(cm._history), 1)
self.assertEqual(cm._history[0].text, "saved entry")
def test_load_history_file_not_exists(self):
"""Test loading when history file doesn't exist."""
cm = ClipboardManager(history_file=self.history_file)
# Should not raise
self.assertEqual(len(cm._history), 0)
@patch('core.clipboard.print')
def test_load_history_corrupted(self, mock_print):
"""Test loading corrupted history file."""
self.history_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.history_file, 'w') as f:
f.write("not valid json")
cm = ClipboardManager(history_file=self.history_file)
# Should not raise, history should be empty
self.assertEqual(len(cm._history), 0)
@patch('core.clipboard.print')
def test_save_history_error(self, mock_print):
"""Test handling save error."""
cm = ClipboardManager(history_file=self.history_file)
cm._history.append(ClipboardEntry(text="test", timestamp=datetime.now().isoformat()))
# Make directory read-only
self.history_file.parent.mkdir(parents=True, exist_ok=True)
with patch('builtins.open', side_effect=IOError("Permission denied")):
cm._save_history()
# Should not raise
class TestAvailability(unittest.TestCase):
"""Test clipboard availability checking."""
def tearDown(self):
"""Reset singleton after each test."""
ClipboardManager._instance = None
@patch('pyperclip.paste')
def test_is_available_true(self, mock_paste):
"""Test availability when pyperclip works."""
mock_paste.return_value = ""
cm = ClipboardManager()
self.assertTrue(cm.is_available())
@patch('pyperclip.paste')
def test_is_available_false(self, mock_paste):
"""Test availability when pyperclip fails."""
mock_paste.side_effect = Exception("Not available")
cm = ClipboardManager()
self.assertFalse(cm.is_available())
class TestClipboardEntry(unittest.TestCase):
"""Test ClipboardEntry dataclass."""
def test_entry_creation(self):
"""Test creating a clipboard entry."""
entry = ClipboardEntry(
text="test text",
timestamp="2024-01-01T00:00:00",
source="plugin"
)
self.assertEqual(entry.text, "test text")
self.assertEqual(entry.timestamp, "2024-01-01T00:00:00")
self.assertEqual(entry.source, "plugin")
def test_entry_defaults(self):
"""Test entry default values."""
entry = ClipboardEntry(text="test", timestamp="2024-01-01T00:00:00")
self.assertEqual(entry.source, "unknown")
class TestConvenienceFunctions(unittest.TestCase):
"""Test convenience functions."""
def tearDown(self):
"""Reset singleton after each test."""
ClipboardManager._instance = None
@patch.object(ClipboardManager, 'copy')
def test_copy_to_clipboard(self, mock_copy):
"""Test copy_to_clipboard convenience function."""
mock_copy.return_value = True
result = copy_to_clipboard("test text")
self.assertTrue(result)
mock_copy.assert_called_once_with("test text")
@patch.object(ClipboardManager, 'paste')
def test_paste_from_clipboard(self, mock_paste):
"""Test paste_from_clipboard convenience function."""
mock_paste.return_value = "pasted text"
result = paste_from_clipboard()
self.assertEqual(result, "pasted text")
if __name__ == '__main__':
unittest.main()