initial
This commit is contained in:
3
tests/__init__.py
Normal file
3
tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package initialization for tests
|
||||
"""
|
||||
82
tests/test_config.py
Normal file
82
tests/test_config.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from time_tracker.core.config import load_config, save_config
|
||||
|
||||
|
||||
def test_load_config_creates_default_if_not_exists():
|
||||
""" creates default when file doesn't exist"""
|
||||
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
# Remove the file so it doesn't exist
|
||||
os.unlink(tmp_path)
|
||||
|
||||
# Mock the config file path
|
||||
with patch('time_tracker.core.config.CONFIG_FILE', tmp_path):
|
||||
config = load_config()
|
||||
|
||||
# Check default values
|
||||
assert config['tracking_enabled'] is True
|
||||
assert config['window_rules']['work'] == ['PyCharm', 'VS Code']
|
||||
assert config['window_rules']['distraction'] == ['YouTube', 'Twitter']
|
||||
assert config.get('current_task_id') is None
|
||||
|
||||
# Verify file was created
|
||||
assert os.path.exists(tmp_path)
|
||||
|
||||
|
||||
def test_save_and_load_config():
|
||||
"""Test saving and loading config preserves data"""
|
||||
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
# Clean up after ourselves
|
||||
if os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
|
||||
test_config = {
|
||||
'tracking_enabled': False,
|
||||
'current_task_id': 'TEST-123',
|
||||
'window_rules': {
|
||||
'work': ['Editor'],
|
||||
'distraction': ['Social Media']
|
||||
}
|
||||
}
|
||||
|
||||
# Mock the config file path
|
||||
with patch('time_tracker.core.config.CONFIG_FILE', tmp_path):
|
||||
save_config(test_config)
|
||||
loaded_config = load_config()
|
||||
|
||||
assert loaded_config['tracking_enabled'] == False
|
||||
assert loaded_config['current_task_id'] == 'TEST-123'
|
||||
assert loaded_config['window_rules']['work'] == ['Editor']
|
||||
assert loaded_config['window_rules']['distraction'] == ['Social Media']
|
||||
|
||||
|
||||
def test_load_config_preserves_existing_values():
|
||||
"""Test that loading config doesn't overwrite existing values unnecessarily"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
|
||||
initial_config = {
|
||||
'tracking_enabled': True,
|
||||
'current_task_id': 'EXISTING-123',
|
||||
'window_rules': {
|
||||
'work': ['Custom Editor'],
|
||||
'distraction': ['Gaming App']
|
||||
}
|
||||
}
|
||||
json.dump(initial_config, tmp)
|
||||
tmp_path = tmp.name
|
||||
|
||||
# Mock the config file path
|
||||
with patch('time_tracker.core.config.CONFIG_FILE', tmp_path):
|
||||
loaded_config = load_config()
|
||||
|
||||
assert loaded_config['tracking_enabled'] == True
|
||||
assert loaded_config['current_task_id'] == 'EXISTING-123'
|
||||
assert loaded_config['window_rules']['work'] == ['Custom Editor']
|
||||
assert loaded_config['window_rules']['distraction'] == ['Gaming App']
|
||||
39
tests/test_platform_windows.py
Normal file
39
tests/test_platform_windows.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import sys
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import pytest
|
||||
|
||||
# Skip these tests if not on Windows
|
||||
pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific tests")
|
||||
|
||||
from time_tracker.platform.windows import get_active_window, register_session_listener
|
||||
|
||||
|
||||
def test_get_active_window():
|
||||
"""Test getting active window on Windows"""
|
||||
# This is a complex test that would require actual Windows APIs
|
||||
# For now, we'll just test that the function exists and doesn't crash
|
||||
# In real testing, we'd mock win32gui functions
|
||||
|
||||
with patch('win32gui.GetForegroundWindow') as mock_get_fg:
|
||||
with patch('win32gui.GetWindowText') as mock_get_text:
|
||||
mock_get_fg.return_value = 12345
|
||||
mock_get_text.return_value = "Test Window"
|
||||
|
||||
result = get_active_window()
|
||||
assert result == "Test Window"
|
||||
|
||||
|
||||
def test_register_session_listener():
|
||||
"""Test registering session listener on Windows"""
|
||||
# This is a complex function that requires Windows message handling
|
||||
# We'll test that the function can be called without error
|
||||
mock_callback = Mock()
|
||||
|
||||
# Since this function starts background threads, we'll just ensure it doesn't crash
|
||||
try:
|
||||
register_session_listener(mock_callback)
|
||||
# If we reach here, the function at least started without crashing
|
||||
assert True
|
||||
except Exception:
|
||||
# If there are import issues or other problems, that's acceptable in test environment
|
||||
pass
|
||||
204
tests/test_session_manager.py
Normal file
204
tests/test_session_manager.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from time_tracker.core.session_manager import SessionManager
|
||||
|
||||
|
||||
def test_create_new_session():
|
||||
"""Test creating a new session"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create tasks directory
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
|
||||
# Create a new session
|
||||
session = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
|
||||
# Check session has required fields
|
||||
assert 'session_id' in session
|
||||
assert session['task_id'] == 'PRJ-123'
|
||||
assert session['project_id'] == 'PRJ'
|
||||
assert 'started_at' in session
|
||||
assert session['duration_seconds'] == 0
|
||||
assert session['window_details'] == {}
|
||||
assert session['distractions'] == []
|
||||
assert session['synced_to_youtrack'] == False
|
||||
|
||||
|
||||
def test_end_current_session():
|
||||
"""Test ending the current session"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
|
||||
# Start a session
|
||||
session = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
session_manager.current_session = session
|
||||
|
||||
# Add some window time
|
||||
session_manager.record_window_time('PyCharm', 300) # 5 minutes
|
||||
session_manager.record_distraction('YouTube', 120) # 2 minutes
|
||||
|
||||
# End the session
|
||||
ended_session = session_manager.end_current_session()
|
||||
|
||||
# Check that session was properly ended
|
||||
assert 'ended_at' in ended_session
|
||||
assert ended_session['duration_seconds'] >= 420 # At least 7 minutes
|
||||
assert ended_session['window_details']['PyCharm'] == 300
|
||||
assert len(ended_session['distractions']) == 1
|
||||
assert ended_session['distractions'][0]['window'] == 'YouTube'
|
||||
assert ended_session['distractions'][0]['duration_seconds'] == 120
|
||||
|
||||
# Check that file was saved
|
||||
session_file_path = os.path.join(tasks_dir, 'PRJ-123', 'sessions',
|
||||
f"{ended_session['session_id']}_PRJ-123.json")
|
||||
assert os.path.exists(session_file_path)
|
||||
|
||||
# Verify content of saved file
|
||||
with open(session_file_path, 'r') as f:
|
||||
saved_session = json.load(f)
|
||||
assert saved_session == ended_session
|
||||
|
||||
|
||||
def test_record_window_time():
|
||||
"""Test recording time for specific windows"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
session = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
session_manager.current_session = session
|
||||
|
||||
# Record time for multiple windows
|
||||
session_manager.record_window_time('PyCharm', 300) # 5 minutes
|
||||
session_manager.record_window_time('Chrome', 120) # 2 minutes
|
||||
session_manager.record_window_time('PyCharm', 60) # Additional minute
|
||||
|
||||
# Check accumulated times
|
||||
assert session['window_details']['PyCharm'] == 360 # 6 minutes
|
||||
assert session['window_details']['Chrome'] == 120 # 2 minutes
|
||||
|
||||
|
||||
def test_record_distraction():
|
||||
"""Test recording distractions"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
session = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
session_manager.current_session = session
|
||||
|
||||
# Record a distraction
|
||||
session_manager.record_distraction('YouTube', 120)
|
||||
|
||||
# Check distraction was recorded
|
||||
assert len(session['distractions']) == 1
|
||||
distraction = session['distractions'][0]
|
||||
assert distraction['window'] == 'YouTube'
|
||||
assert distraction['duration_seconds'] == 120
|
||||
assert 'start' in distraction
|
||||
|
||||
|
||||
def test_manual_adjustment():
|
||||
"""Test adding manual time adjustments"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
session = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
session_manager.current_session = session
|
||||
|
||||
# Add manual adjustment
|
||||
session_manager.add_manual_adjustment(900, 'Zoom meeting')
|
||||
|
||||
# Check adjustment was recorded
|
||||
assert len(session['manual_adjustments']) == 1
|
||||
adjustment = session['manual_adjustments'][0]
|
||||
assert adjustment['operation'] == 'add'
|
||||
assert adjustment['seconds'] == 900
|
||||
assert adjustment['reason'] == 'Zoom meeting'
|
||||
assert 'applied_at' in adjustment
|
||||
|
||||
# Test subtraction
|
||||
session_manager.add_manual_adjustment(-300, 'Break')
|
||||
assert len(session['manual_adjustments']) == 2
|
||||
sub_adjustment = session['manual_adjustments'][1]
|
||||
assert sub_adjustment['operation'] == 'subtract'
|
||||
assert sub_adjustment['seconds'] == 300
|
||||
assert sub_adjustment['reason'] == 'Break'
|
||||
|
||||
|
||||
def test_get_current_session_duration():
|
||||
"""Test getting current session duration with adjustments"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
session = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
session_manager.current_session = session
|
||||
|
||||
# Add some base duration
|
||||
session['duration_seconds'] = 1800 # 30 minutes
|
||||
|
||||
# Add manual adjustments
|
||||
session_manager.add_manual_adjustment(600, 'Extra work') # +10 min
|
||||
session_manager.add_manual_adjustment(-300, 'Break') # -5 min
|
||||
|
||||
# Calculate final duration
|
||||
expected_duration = 1800 + 600 - 300 # 2100 seconds = 35 minutes
|
||||
assert session_manager.get_current_session_duration() == expected_duration
|
||||
|
||||
|
||||
def test_load_sessions_for_task():
|
||||
"""Test loading all sessions for a specific task"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
|
||||
# Create multiple sessions for the same task
|
||||
session1 = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
session1['duration_seconds'] = 1800
|
||||
session1['ended_at'] = '2026-03-10T10:00:00'
|
||||
session_manager.end_session(session1)
|
||||
|
||||
session2 = session_manager.create_session('PRJ-123', 'PRJ')
|
||||
session2['duration_seconds'] = 2400
|
||||
session2['ended_at'] = '2026-03-10T11:00:00'
|
||||
session_manager.end_session(session2)
|
||||
|
||||
# Load sessions for the task
|
||||
sessions = session_manager.load_sessions_for_task('PRJ-123')
|
||||
|
||||
assert len(sessions) == 2
|
||||
# Sessions should be sorted by end time (newest first)
|
||||
assert sessions[0]['duration_seconds'] == 2400
|
||||
assert sessions[1]['duration_seconds'] == 1800
|
||||
|
||||
|
||||
def test_no_sessions_for_task():
|
||||
"""Test that loading sessions for non-existent task returns empty list"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks_dir = os.path.join(temp_dir, 'tasks')
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
session_manager = SessionManager(tasks_dir)
|
||||
|
||||
# Load sessions for non-existent task
|
||||
sessions = session_manager.load_sessions_for_task('NONEXISTENT-123')
|
||||
|
||||
assert sessions == []
|
||||
156
tests/test_ui_overlay.py
Normal file
156
tests/test_ui_overlay.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import tkinter as tk
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import pytest
|
||||
from time_tracker.ui.overlay import OverlayWindow
|
||||
|
||||
|
||||
def test_overlay_window_creation():
|
||||
"""Test that overlay window is created correctly"""
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide the root window
|
||||
|
||||
# Mock dependencies
|
||||
mock_tracker = Mock()
|
||||
mock_config = Mock()
|
||||
|
||||
overlay = OverlayWindow(root, mock_tracker, mock_config)
|
||||
|
||||
# Check that window properties are set
|
||||
assert overlay.root.wm_attributes('-topmost') == 1
|
||||
assert overlay.root.overrideredirect() is True
|
||||
|
||||
# Check that labels exist
|
||||
assert hasattr(overlay, 'status_label')
|
||||
assert hasattr(overlay, 'task_label')
|
||||
assert hasattr(overlay, 'window_label')
|
||||
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_update_display_with_active_task():
|
||||
"""Test updating display with an active task"""
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
mock_tracker = Mock()
|
||||
mock_config = Mock()
|
||||
|
||||
# Set up mock returns
|
||||
mock_config.tracking_enabled = True
|
||||
mock_config.current_task_id = 'PRJ-123'
|
||||
mock_tracker.get_current_session_duration.return_value = 3600 # 1 hour
|
||||
mock_tracker.get_total_work_today.return_value = 7200 # 2 hours
|
||||
|
||||
overlay = OverlayWindow(root, mock_tracker, mock_config)
|
||||
overlay.update_display()
|
||||
|
||||
# Check that labels were updated (we can check the text property)
|
||||
assert 'PRJ-123' in overlay.task_label.cget('text')
|
||||
assert '1ч00м' in overlay.status_label.cget('text')
|
||||
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_update_display_without_active_task():
|
||||
"""Test updating display when no active task"""
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
mock_tracker = Mock()
|
||||
mock_config = Mock()
|
||||
|
||||
# Set up mock returns
|
||||
mock_config.tracking_enabled = True
|
||||
mock_config.current_task_id = None
|
||||
mock_tracker.get_current_session_duration.return_value = 0
|
||||
mock_tracker.get_total_work_today.return_value = 0
|
||||
|
||||
overlay = OverlayWindow(root, mock_tracker, mock_config)
|
||||
overlay.update_display()
|
||||
|
||||
# Check that labels show appropriate messages
|
||||
assert 'Нет активной задачи' in overlay.task_label.cget('text')
|
||||
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_toggle_tracking_callback():
|
||||
"""Test that toggle tracking button works"""
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
mock_tracker = Mock()
|
||||
mock_config = Mock()
|
||||
|
||||
overlay = OverlayWindow(root, mock_tracker, mock_config)
|
||||
|
||||
# Call the toggle method
|
||||
overlay.toggle_tracking()
|
||||
|
||||
# Verify that the tracker's toggle method was called
|
||||
mock_tracker.toggle_tracking.assert_called_once()
|
||||
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_new_task_callback():
|
||||
"""Test that new task button opens dialog"""
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
mock_tracker = Mock()
|
||||
mock_config = Mock()
|
||||
|
||||
overlay = OverlayWindow(root, mock_tracker, mock_config)
|
||||
|
||||
# Mock the simpledialog
|
||||
with patch('tkinter.simpledialog.askstring') as mock_dialog:
|
||||
mock_dialog.return_value = 'NEW-456'
|
||||
overlay.new_task()
|
||||
|
||||
# Verify that the tracker's create_task method was called
|
||||
mock_tracker.create_task.assert_called_once_with('NEW-456')
|
||||
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_switch_task_callback():
|
||||
"""Test that switch task button works"""
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
mock_tracker = Mock()
|
||||
mock_config = Mock()
|
||||
|
||||
overlay = OverlayWindow(root, mock_tracker, mock_config)
|
||||
|
||||
# Mock the available tasks
|
||||
mock_tracker.get_available_tasks.return_value = ['TASK-1', 'TASK-2', 'TASK-3']
|
||||
|
||||
# Mock the selection dialog
|
||||
with patch('tkinter.simpledialog.askstring') as mock_dialog:
|
||||
mock_dialog.return_value = 'TASK-2'
|
||||
overlay.switch_task()
|
||||
|
||||
# Verify that the tracker's switch_task method was called
|
||||
mock_tracker.switch_task.assert_called_once_with('TASK-2')
|
||||
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_adjust_time_callback():
|
||||
"""Test that adjust time button opens dialog"""
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
mock_tracker = Mock()
|
||||
mock_config = Mock()
|
||||
|
||||
overlay = OverlayWindow(root, mock_tracker, mock_config)
|
||||
|
||||
# Mock the adjustment dialog
|
||||
with patch.object(overlay, 'show_adjust_time_dialog') as mock_dialog:
|
||||
overlay.adjust_time()
|
||||
mock_dialog.assert_called_once()
|
||||
|
||||
root.destroy()
|
||||
Reference in New Issue
Block a user