369 lines
12 KiB
Python
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()
|