mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-01-25 08:34:51 +00:00
added garmin functional tests
This commit is contained in:
@@ -6,7 +6,8 @@ import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from datetime import datetime
|
||||
from textual.app import App
|
||||
from textual.widgets import DataTable, Static, Button, TabbedContent
|
||||
from textual.widgets import DataTable, Static, Button, TabbedContent, Collapsible
|
||||
from tui.widgets.loading import LoadingSpinner
|
||||
|
||||
from tui.views.workouts import WorkoutView, WorkoutMetricsChart, WorkoutAnalysisPanel
|
||||
from tui.services.workout_service import WorkoutService
|
||||
@@ -148,16 +149,15 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local:
|
||||
# Setup mock to raise exception
|
||||
mock_session_local.return_value.__aenter__.side_effect = Exception("Database connection failed")
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local:
|
||||
# Setup mock to raise exception
|
||||
mock_session_local.return_value.__aenter__.side_effect = Exception("Database connection failed")
|
||||
|
||||
# Should raise the exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await workout_view._load_workouts_data()
|
||||
# Should raise the exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await workout_view._load_workouts_data()
|
||||
|
||||
assert "Database connection failed" in str(exc_info.value)
|
||||
assert "Database connection failed" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_workouts_with_timeout(self, mock_workouts, mock_sync_status):
|
||||
@@ -165,17 +165,16 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
# Mock the actual loading method to return quickly
|
||||
with patch.object(workout_view, '_load_workouts_data') as mock_load:
|
||||
mock_load.return_value = (mock_workouts, mock_sync_status)
|
||||
# Mock the actual loading method to return quickly
|
||||
with patch.object(workout_view, '_load_workouts_data') as mock_load:
|
||||
mock_load.return_value = (mock_workouts, mock_sync_status)
|
||||
|
||||
# Should complete successfully within timeout
|
||||
result = await workout_view._load_workouts_with_timeout()
|
||||
workouts, sync_status = result
|
||||
# Should complete successfully within timeout
|
||||
result = await workout_view._load_workouts_with_timeout()
|
||||
workouts, sync_status = result
|
||||
|
||||
assert workouts == mock_workouts
|
||||
assert sync_status == mock_sync_status
|
||||
assert workouts == mock_workouts
|
||||
assert sync_status == mock_sync_status
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_workouts_timeout_error(self):
|
||||
@@ -184,20 +183,18 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
# Mock the actual loading method to hang
|
||||
async def slow_load():
|
||||
await asyncio.sleep(0.1) # Longer than timeout
|
||||
return [], {}
|
||||
# Mock the actual loading method to hang
|
||||
async def slow_load():
|
||||
await asyncio.sleep(0.1) # Longer than timeout
|
||||
return [], {}
|
||||
|
||||
workout_view.LOAD_TIMEOUT = 0.01
|
||||
workout_view.LOAD_TIMEOUT = 0.01
|
||||
|
||||
with patch.object(workout_view, '_load_workouts_data', side_effect=slow_load):
|
||||
# Should raise timeout exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await workout_view._load_workouts_with_timeout()
|
||||
with patch.object(workout_view, '_load_workouts_data', side_effect=slow_load):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await workout_view._load_workouts_with_timeout()
|
||||
|
||||
assert "timed out" in str(exc_info.value).lower()
|
||||
assert "timed out" in str(exc_info.value).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_workouts_loaded_success(self, mock_workouts, mock_sync_status):
|
||||
@@ -205,28 +202,27 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.loading = True
|
||||
workout_view.error_message = "Previous error"
|
||||
workout_view.loading = True
|
||||
workout_view.error_message = "Previous error"
|
||||
|
||||
# Mock the UI update methods
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh, \
|
||||
patch.object(workout_view, 'populate_workouts_table') as mock_populate, \
|
||||
patch.object(workout_view, 'update_sync_status') as mock_update_sync:
|
||||
# Mock the UI update methods
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh, \
|
||||
patch.object(workout_view, 'populate_workouts_table') as mock_populate, \
|
||||
patch.object(workout_view, 'update_sync_status') as mock_update_sync:
|
||||
|
||||
# Call the method
|
||||
workout_view.on_workouts_loaded((mock_workouts, mock_sync_status))
|
||||
# Call the method
|
||||
workout_view.on_workouts_loaded((mock_workouts, mock_sync_status))
|
||||
|
||||
# Verify state updates
|
||||
assert workout_view.workouts == mock_workouts
|
||||
assert workout_view.sync_status == mock_sync_status
|
||||
assert workout_view.loading is False
|
||||
assert workout_view.error_message is None
|
||||
# Verify state updates
|
||||
assert workout_view.workouts == mock_workouts
|
||||
assert workout_view.sync_status == mock_sync_status
|
||||
assert workout_view.loading is False
|
||||
assert workout_view.error_message is None
|
||||
|
||||
# Verify UI method calls
|
||||
mock_refresh.assert_called_once_with(layout=True)
|
||||
mock_populate.assert_called_once()
|
||||
mock_update_sync.assert_called_once()
|
||||
# Verify UI method calls
|
||||
mock_refresh.assert_called_once_with(layout=True)
|
||||
mock_populate.assert_called_once()
|
||||
mock_update_sync.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_workouts_loaded_error_handling(self):
|
||||
@@ -234,18 +230,20 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.loading = True
|
||||
|
||||
# Mock refresh to raise exception
|
||||
with patch.object(workout_view, 'refresh', side_effect=Exception("UI Error")):
|
||||
workout_view.loading = True
|
||||
|
||||
# Mock refresh to raise exception
|
||||
with patch.object(workout_view, 'refresh', side_effect=Exception("UI Error")):
|
||||
try:
|
||||
# Should handle the exception gracefully
|
||||
workout_view.on_workouts_loaded(([], {}))
|
||||
except Exception:
|
||||
# The exception is caught and handled inside the method
|
||||
pass
|
||||
|
||||
# Should still update loading state and set error
|
||||
assert workout_view.loading is False
|
||||
assert "Failed to process loaded data" in str(workout_view.error_message)
|
||||
# Should still update loading state and set error
|
||||
assert workout_view.loading is False
|
||||
assert "Failed to process loaded data" in str(workout_view.error_message)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_populate_workouts_table(self, mock_workouts):
|
||||
@@ -253,27 +251,26 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.workouts = mock_workouts
|
||||
workout_view.workouts = mock_workouts
|
||||
|
||||
# Mock the DataTable widget
|
||||
mock_table = MagicMock(spec=DataTable)
|
||||
# Mock the DataTable widget
|
||||
mock_table = MagicMock(spec=DataTable)
|
||||
|
||||
with patch.object(workout_view, 'query_one', return_value=mock_table):
|
||||
await workout_view.populate_workouts_table()
|
||||
with patch.object(workout_view, 'query_one', return_value=mock_table):
|
||||
await workout_view.populate_workouts_table()
|
||||
|
||||
# Verify table was cleared and populated
|
||||
mock_table.clear.assert_called_once()
|
||||
assert mock_table.add_row.call_count == len(mock_workouts)
|
||||
# Verify table was cleared and populated
|
||||
mock_table.clear.assert_called_once()
|
||||
assert mock_table.add_row.call_count == len(mock_workouts)
|
||||
|
||||
# Check first workout data formatting
|
||||
first_call_args = mock_table.add_row.call_args_list[0][0]
|
||||
assert "01/15 14:30" in first_call_args[0]
|
||||
assert "cycling" in first_call_args[1]
|
||||
assert "75min" in first_call_args[2]
|
||||
assert "32.5km" in first_call_args[3]
|
||||
assert "145 BPM" in first_call_args[4]
|
||||
assert "180 W" in first_call_args[5]
|
||||
# Check first workout data formatting
|
||||
first_call_args = mock_table.add_row.call_args_list[0][0]
|
||||
assert "01/15 14:30" in first_call_args[0]
|
||||
assert "cycling" in first_call_args[1]
|
||||
assert "75min" in first_call_args[2]
|
||||
assert "32.5km" in first_call_args[3]
|
||||
assert "145 BPM" in first_call_args[4]
|
||||
assert "180 W" in first_call_args[5]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_sync_status(self, mock_sync_status):
|
||||
@@ -281,22 +278,21 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.sync_status = mock_sync_status
|
||||
workout_view.sync_status = mock_sync_status
|
||||
|
||||
# Mock the Status widget
|
||||
mock_status_text = MagicMock(spec=Static)
|
||||
# Mock the Status widget
|
||||
mock_status_text = MagicMock(spec=Static)
|
||||
|
||||
with patch.object(workout_view, 'query_one', return_value=mock_status_text):
|
||||
await workout_view.update_sync_status()
|
||||
with patch.object(workout_view, 'query_one', return_value=mock_status_text):
|
||||
await workout_view.update_sync_status()
|
||||
|
||||
# Verify status text was updated
|
||||
mock_status_text.update.assert_called_once()
|
||||
update_text = mock_status_text.update.call_args[0][0]
|
||||
# Verify status text was updated
|
||||
mock_status_text.update.assert_called_once()
|
||||
update_text = mock_status_text.update.call_args[0][0]
|
||||
|
||||
assert "Connected" in update_text
|
||||
assert "2024-01-15 15:00" in update_text
|
||||
assert "25" in update_text
|
||||
assert "Connected" in update_text
|
||||
assert "2024-01-15 15:00" in update_text
|
||||
assert "25" in update_text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_garmin_activities_success(self):
|
||||
@@ -304,34 +300,33 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_service = AsyncMock()
|
||||
mock_service.sync_garmin_activities.return_value = {
|
||||
"status": "success",
|
||||
"activities_synced": 5,
|
||||
"message": "Sync completed"
|
||||
}
|
||||
mock_service_class.return_value = mock_service
|
||||
mock_service = AsyncMock()
|
||||
mock_service.sync_garmin_activities.return_value = {
|
||||
"status": "success",
|
||||
"activities_synced": 5,
|
||||
"message": "Sync completed"
|
||||
}
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Mock the refresh methods
|
||||
with patch.object(workout_view, 'check_sync_status') as mock_check_sync, \
|
||||
patch.object(workout_view, 'refresh_workouts') as mock_refresh_workouts:
|
||||
# Mock the refresh methods
|
||||
with patch.object(workout_view, 'check_sync_status') as mock_check_sync, \
|
||||
patch.object(workout_view, 'refresh_workouts') as mock_refresh_workouts:
|
||||
|
||||
await workout_view.sync_garmin_activities()
|
||||
await workout_view.sync_garmin_activities()
|
||||
|
||||
# Verify service call
|
||||
mock_service.sync_garmin_activities.assert_called_once_with(days_back=14)
|
||||
# Verify service call
|
||||
mock_service.sync_garmin_activities.assert_called_once_with(days_back=14)
|
||||
|
||||
# Verify UI refresh calls
|
||||
mock_check_sync.assert_called_once()
|
||||
mock_refresh_workouts.assert_called_once()
|
||||
# Verify UI refresh calls
|
||||
mock_check_sync.assert_called_once()
|
||||
mock_refresh_workouts.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_garmin_activities_failure(self):
|
||||
@@ -339,31 +334,30 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_service = AsyncMock()
|
||||
mock_service.sync_garmin_activities.return_value = {
|
||||
"status": "error",
|
||||
"activities_synced": 0,
|
||||
"message": "Authentication failed"
|
||||
}
|
||||
mock_service_class.return_value = mock_service
|
||||
mock_service = AsyncMock()
|
||||
mock_service.sync_garmin_activities.return_value = {
|
||||
"status": "error",
|
||||
"activities_synced": 0,
|
||||
"message": "Authentication failed"
|
||||
}
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Mock the refresh methods
|
||||
with patch.object(workout_view, 'check_sync_status') as mock_check_sync, \
|
||||
patch.object(workout_view, 'refresh_workouts') as mock_refresh_workouts:
|
||||
# Mock the refresh methods
|
||||
with patch.object(workout_view, 'check_sync_status') as mock_check_sync, \
|
||||
patch.object(workout_view, 'refresh_workouts') as mock_refresh_workouts:
|
||||
|
||||
await workout_view.sync_garmin_activities()
|
||||
await workout_view.sync_garmin_activities()
|
||||
|
||||
# Should still call refresh methods even on failure
|
||||
mock_check_sync.assert_called_once()
|
||||
mock_refresh_workouts.assert_called_once()
|
||||
# Should still call refresh methods even on failure
|
||||
mock_check_sync.assert_called_once()
|
||||
mock_refresh_workouts.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_selected_workout_success(self, mock_workouts, mock_workout_analyses):
|
||||
@@ -371,43 +365,42 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.selected_workout = mock_workouts[0]
|
||||
workout_view.selected_workout = mock_workouts[0]
|
||||
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_service = AsyncMock()
|
||||
mock_service.analyze_workout.return_value = {
|
||||
"status": "success",
|
||||
"message": "Analysis completed"
|
||||
}
|
||||
mock_service.get_workout_analyses.return_value = mock_workout_analyses
|
||||
mock_service_class.return_value = mock_service
|
||||
mock_service = AsyncMock()
|
||||
mock_service.analyze_workout.return_value = {
|
||||
"status": "success",
|
||||
"message": "Analysis completed"
|
||||
}
|
||||
mock_service.get_workout_analyses.return_value = mock_workout_analyses
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Mock refresh and message posting
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh, \
|
||||
patch.object(workout_view, 'post_message') as mock_post_message:
|
||||
# Mock refresh and message posting
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh, \
|
||||
patch.object(workout_view, 'post_message') as mock_post_message:
|
||||
|
||||
await workout_view.analyze_selected_workout()
|
||||
await workout_view.analyze_selected_workout()
|
||||
|
||||
# Verify service calls
|
||||
mock_service.analyze_workout.assert_called_once_with(1) # workout ID
|
||||
mock_service.get_workout_analyses.assert_called_once_with(1)
|
||||
# Verify service calls
|
||||
mock_service.analyze_workout.assert_called_once_with(1) # workout ID
|
||||
mock_service.get_workout_analyses.assert_called_once_with(1)
|
||||
|
||||
# Verify UI updates
|
||||
assert workout_view.workout_analyses == mock_workout_analyses
|
||||
mock_refresh.assert_called_once()
|
||||
# Verify UI updates
|
||||
assert workout_view.workout_analyses == mock_workout_analyses
|
||||
mock_refresh.assert_called()
|
||||
|
||||
# Verify message posting
|
||||
assert mock_post_message.called
|
||||
message = mock_post_message.call_args[0][0]
|
||||
assert hasattr(message, 'workout_id')
|
||||
assert message.workout_id == 1
|
||||
# Verify message posting
|
||||
assert mock_post_message.called
|
||||
message = mock_post_message.call_args[0][0]
|
||||
assert hasattr(message, 'workout_id')
|
||||
assert message.workout_id == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_selected_workout_no_selection(self):
|
||||
@@ -415,14 +408,13 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.selected_workout = None
|
||||
workout_view.selected_workout = None
|
||||
|
||||
# Should not raise exception, just log warning
|
||||
await workout_view.analyze_selected_workout()
|
||||
# Should not raise exception, just log warning
|
||||
await workout_view.analyze_selected_workout()
|
||||
|
||||
# No service calls should be made
|
||||
# (This would be verified by not mocking any services)
|
||||
# No service calls should be made
|
||||
# (This would be verified by not mocking any services)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_analysis_success(self, mock_workouts):
|
||||
@@ -430,35 +422,34 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.selected_workout = mock_workouts[0]
|
||||
workout_view.selected_workout = mock_workouts[0]
|
||||
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_service = AsyncMock()
|
||||
mock_service.approve_analysis.return_value = {
|
||||
"status": "success",
|
||||
"message": "Analysis approved"
|
||||
}
|
||||
mock_service.get_workout_analyses.return_value = []
|
||||
mock_service_class.return_value = mock_service
|
||||
mock_service = AsyncMock()
|
||||
mock_service.approve_analysis.return_value = {
|
||||
"status": "success",
|
||||
"message": "Analysis approved"
|
||||
}
|
||||
mock_service.get_workout_analyses.return_value = []
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Mock refresh
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh:
|
||||
# Mock refresh
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh:
|
||||
|
||||
await workout_view.approve_analysis(1)
|
||||
await workout_view.approve_analysis(1)
|
||||
|
||||
# Verify service calls
|
||||
mock_service.approve_analysis.assert_called_once_with(1)
|
||||
mock_service.get_workout_analyses.assert_called_once_with(1)
|
||||
# Verify service calls
|
||||
mock_service.approve_analysis.assert_called_once_with(1)
|
||||
mock_service.get_workout_analyses.assert_called_once_with(1)
|
||||
|
||||
# Verify UI refresh
|
||||
mock_refresh.assert_called_once()
|
||||
# Verify UI refresh
|
||||
mock_refresh.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_show_workout_details(self, mock_workouts, mock_workout_analyses):
|
||||
@@ -466,40 +457,39 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_service = AsyncMock()
|
||||
mock_service.get_workout_analyses.return_value = mock_workout_analyses
|
||||
mock_service_class.return_value = mock_service
|
||||
mock_service = AsyncMock()
|
||||
mock_service.get_workout_analyses.return_value = mock_workout_analyses
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Mock TabbedContent widget
|
||||
mock_tabs = MagicMock(spec=TabbedContent)
|
||||
# Mock TabbedContent widget
|
||||
mock_tabs = MagicMock(spec=TabbedContent)
|
||||
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh, \
|
||||
patch.object(workout_view, 'query_one', return_value=mock_tabs), \
|
||||
patch.object(workout_view, 'post_message') as mock_post_message:
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh, \
|
||||
patch.object(workout_view, 'query_one', return_value=mock_tabs), \
|
||||
patch.object(workout_view, 'post_message') as mock_post_message:
|
||||
|
||||
await workout_view.show_workout_details(mock_workouts[0])
|
||||
await workout_view.show_workout_details(mock_workouts[0])
|
||||
|
||||
# Verify state updates
|
||||
assert workout_view.selected_workout == mock_workouts[0]
|
||||
assert workout_view.workout_analyses == mock_workout_analyses
|
||||
# Verify state updates
|
||||
assert workout_view.selected_workout == mock_workouts[0]
|
||||
assert workout_view.workout_analyses == mock_workout_analyses
|
||||
|
||||
# Verify service call
|
||||
mock_service.get_workout_analyses.assert_called_once_with(1)
|
||||
# Verify service call
|
||||
mock_service.get_workout_analyses.assert_called_once_with(1)
|
||||
|
||||
# Verify UI updates
|
||||
mock_refresh.assert_called_once()
|
||||
assert mock_tabs.active == "workout-details-tab"
|
||||
# Verify UI updates
|
||||
mock_refresh.assert_called()
|
||||
assert mock_tabs.active == "workout-details-tab"
|
||||
|
||||
# Verify message posting
|
||||
mock_post_message.assert_called_once()
|
||||
# Verify message posting
|
||||
mock_post_message.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_loading_state_change(self):
|
||||
@@ -507,16 +497,15 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view._mounted = True
|
||||
workout_view._mounted = True
|
||||
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh:
|
||||
# Trigger loading state change
|
||||
workout_view.loading = False
|
||||
workout_view.watch_loading(False)
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh:
|
||||
# Trigger loading state change
|
||||
workout_view.loading = False
|
||||
workout_view.watch_loading(False)
|
||||
|
||||
# Should trigger refresh
|
||||
mock_refresh.assert_called_once()
|
||||
# Should trigger refresh
|
||||
mock_refresh.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_error_message_change(self):
|
||||
@@ -524,17 +513,16 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view._mounted = True
|
||||
workout_view._mounted = True
|
||||
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh:
|
||||
# Trigger error message change
|
||||
error_msg = "Test error"
|
||||
workout_view.error_message = error_msg
|
||||
workout_view.watch_error_message(error_msg)
|
||||
with patch.object(workout_view, 'refresh') as mock_refresh:
|
||||
# Trigger error message change
|
||||
error_msg = "Test error"
|
||||
workout_view.error_message = error_msg
|
||||
workout_view.watch_error_message(error_msg)
|
||||
|
||||
# Should trigger refresh
|
||||
mock_refresh.assert_called_once()
|
||||
# Should trigger refresh
|
||||
mock_refresh.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_button_pressed_refresh_workouts(self):
|
||||
@@ -542,16 +530,15 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
# Mock button and refresh method
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "refresh-workouts-btn"
|
||||
# Mock button and refresh method
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "refresh-workouts-btn"
|
||||
|
||||
with patch.object(workout_view, 'refresh_workouts') as mock_refresh:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
with patch.object(workout_view, 'refresh_workouts') as mock_refresh:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
|
||||
mock_refresh.assert_called_once()
|
||||
mock_refresh.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_button_pressed_sync_garmin(self):
|
||||
@@ -559,16 +546,15 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
# Mock button and sync method
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "sync-garmin-btn"
|
||||
# Mock button and sync method
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "sync-garmin-btn"
|
||||
|
||||
with patch.object(workout_view, 'sync_garmin_activities') as mock_sync:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
with patch.object(workout_view, 'sync_garmin_activities') as mock_sync:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
|
||||
mock_sync.assert_called_once()
|
||||
mock_sync.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_button_pressed_retry_loading(self):
|
||||
@@ -576,20 +562,19 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.error_message = "Previous error"
|
||||
workout_view.error_message = "Previous error"
|
||||
|
||||
# Mock button and load_data method
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "retry-loading-btn"
|
||||
# Mock button and load_data method
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "retry-loading-btn"
|
||||
|
||||
with patch.object(workout_view, 'load_data') as mock_load_data:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
with patch.object(workout_view, 'load_data') as mock_load_data:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
|
||||
# Should clear error and reload
|
||||
assert workout_view.error_message is None
|
||||
mock_load_data.assert_called_once()
|
||||
# Should clear error and reload
|
||||
assert workout_view.error_message is None
|
||||
mock_load_data.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_data_table_row_selection(self, mock_workouts):
|
||||
@@ -597,25 +582,24 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
workout_view.workouts = mock_workouts
|
||||
workout_view.workouts = mock_workouts
|
||||
|
||||
# Mock table and event
|
||||
mock_table = MagicMock(spec=DataTable)
|
||||
mock_table.id = "workouts-table"
|
||||
# Mock table and event
|
||||
mock_table = MagicMock(spec=DataTable)
|
||||
mock_table.id = "workouts-table"
|
||||
|
||||
# Mock event with row selection
|
||||
event = MagicMock()
|
||||
event.data_table = mock_table
|
||||
event.cursor_row = 0
|
||||
event.row_key = MagicMock()
|
||||
event.row_key.value = 0
|
||||
# Mock event with row selection
|
||||
event = MagicMock()
|
||||
event.data_table = mock_table
|
||||
event.cursor_row = 0
|
||||
event.row_key = MagicMock()
|
||||
event.row_key.value = 0
|
||||
|
||||
with patch.object(workout_view, 'show_workout_details') as mock_show_details:
|
||||
await workout_view.on_data_table_row_selected(event)
|
||||
with patch.object(workout_view, 'show_workout_details') as mock_show_details:
|
||||
await workout_view.on_data_table_row_selected(event)
|
||||
|
||||
# Should show details for first workout
|
||||
mock_show_details.assert_called_once_with(mock_workouts[0])
|
||||
# Should show details for first workout
|
||||
mock_show_details.assert_called_once_with(mock_workouts[0])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_integration_full_workflow(self, mock_workouts, mock_sync_status, mock_workout_analyses):
|
||||
@@ -623,69 +607,67 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
with patch('tui.views.workouts.AsyncSessionLocal') as mock_session_local, \
|
||||
patch('tui.views.workouts.WorkoutService') as mock_service_class:
|
||||
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
# Setup mocks
|
||||
mock_db = AsyncMock()
|
||||
mock_session_local.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_service = AsyncMock()
|
||||
mock_service.get_workouts.return_value = mock_workouts
|
||||
mock_service.get_sync_status.return_value = mock_sync_status
|
||||
mock_service.get_workout_analyses.return_value = mock_workout_analyses
|
||||
mock_service.analyze_workout.return_value = {
|
||||
"status": "success",
|
||||
"message": "Analysis completed"
|
||||
}
|
||||
mock_service_class.return_value = mock_service
|
||||
mock_service = AsyncMock()
|
||||
mock_service.get_workouts.return_value = mock_workouts
|
||||
mock_service.get_sync_status.return_value = mock_sync_status
|
||||
mock_service.get_workout_analyses.return_value = mock_workout_analyses
|
||||
mock_service.analyze_workout.return_value = {
|
||||
"status": "success",
|
||||
"message": "Analysis completed"
|
||||
}
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
# Mock UI methods
|
||||
with patch.object(workout_view, 'refresh'), \
|
||||
patch.object(workout_view, 'populate_workouts_table'), \
|
||||
patch.object(workout_view, 'update_sync_status'), \
|
||||
patch.object(workout_view, 'query_one'), \
|
||||
patch.object(workout_view, 'post_message'):
|
||||
# Mock UI methods
|
||||
with patch.object(workout_view, 'refresh'), \
|
||||
patch.object(workout_view, 'populate_workouts_table'), \
|
||||
patch.object(workout_view, 'update_sync_status'), \
|
||||
patch.object(workout_view, 'query_one'), \
|
||||
patch.object(workout_view, 'post_message'):
|
||||
|
||||
# 1. Load initial data
|
||||
result = await workout_view._load_workouts_data()
|
||||
workouts, sync_status = result
|
||||
workout_view.on_workouts_loaded((workouts, sync_status))
|
||||
# 1. Load initial data
|
||||
result = await workout_view._load_workouts_data()
|
||||
workouts, sync_status = result
|
||||
workout_view.on_workouts_loaded((workouts, sync_status))
|
||||
|
||||
# 2. Show workout details
|
||||
await workout_view.show_workout_details(workouts[0])
|
||||
# 2. Show workout details
|
||||
await workout_view.show_workout_details(workouts[0])
|
||||
|
||||
# 3. Analyze workout
|
||||
await workout_view.analyze_selected_workout()
|
||||
# 3. Analyze workout
|
||||
await workout_view.analyze_selected_workout()
|
||||
|
||||
# Verify full workflow executed
|
||||
assert workout_view.workouts == mock_workouts
|
||||
assert workout_view.sync_status == mock_sync_status
|
||||
assert workout_view.selected_workout == mock_workouts[0]
|
||||
assert workout_view.workout_analyses == mock_workout_analyses
|
||||
assert workout_view.loading is False
|
||||
assert workout_view.error_message is None
|
||||
# Verify full workflow executed
|
||||
assert workout_view.workouts == mock_workouts
|
||||
assert workout_view.sync_status == mock_sync_status
|
||||
assert workout_view.selected_workout == mock_workouts[0]
|
||||
assert workout_view.workout_analyses == mock_workout_analyses
|
||||
assert workout_view.loading is False
|
||||
assert workout_view.error_message is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_compose_with_error(self):
|
||||
"""Test the compose method when an error message is set."""
|
||||
app = App()
|
||||
workout_view = WorkoutView()
|
||||
app.push_view(workout_view)
|
||||
async with app.run_test():
|
||||
workout_view.error_message = "A critical error occurred"
|
||||
workout_view.loading = False
|
||||
|
||||
# Since compose is called on render, we can't call it directly.
|
||||
# We'll check the widgets that get created.
|
||||
widgets = list(workout_view.compose())
|
||||
class TestApp(App):
|
||||
def compose(self):
|
||||
workout_view = WorkoutView()
|
||||
workout_view.error_message = "A critical error occurred"
|
||||
workout_view.loading = False
|
||||
yield workout_view
|
||||
|
||||
app = TestApp()
|
||||
async with app.run_test() as pilot:
|
||||
# Check for error display and retry button
|
||||
assert any(isinstance(w, Static) and "A critical error occurred" in str(w.render()) for w in widgets)
|
||||
assert any(isinstance(w, Button) and w.id == "retry-loading-btn" for w in widgets)
|
||||
assert pilot.app.query_one(Static)
|
||||
assert "A critical error occurred" in str(pilot.app.query_one(Static).render())
|
||||
assert pilot.app.query_one("#retry-loading-btn", Button)
|
||||
# Ensure loading spinner is not present
|
||||
assert not any(isinstance(w, LoadingSpinner) for w in widgets)
|
||||
assert not pilot.app.query(LoadingSpinner)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_populate_workouts_table_with_malformed_data(self):
|
||||
@@ -693,33 +675,32 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
malformed_workouts = [
|
||||
{
|
||||
"id": 1,
|
||||
"start_time": "Invalid-Date",
|
||||
"duration_seconds": None,
|
||||
"distance_m": "Not a number",
|
||||
"avg_hr": None,
|
||||
"avg_power": None
|
||||
}
|
||||
]
|
||||
workout_view.workouts = malformed_workouts
|
||||
malformed_workouts = [
|
||||
{
|
||||
"id": 1,
|
||||
"start_time": "Invalid-Date",
|
||||
"duration_seconds": None,
|
||||
"distance_m": "Not a number",
|
||||
"avg_hr": None,
|
||||
"avg_power": None
|
||||
}
|
||||
]
|
||||
workout_view.workouts = malformed_workouts
|
||||
|
||||
mock_table = MagicMock(spec=DataTable)
|
||||
mock_table = MagicMock(spec=DataTable)
|
||||
|
||||
with patch.object(workout_view, 'query_one', return_value=mock_table):
|
||||
await workout_view.populate_workouts_table()
|
||||
with patch.object(workout_view, 'query_one', return_value=mock_table):
|
||||
await workout_view.populate_workouts_table()
|
||||
|
||||
mock_table.clear.assert_called_once()
|
||||
assert mock_table.add_row.call_count == 1
|
||||
mock_table.clear.assert_called_once()
|
||||
assert mock_table.add_row.call_count == 1
|
||||
|
||||
# Check that it fell back to default/graceful values
|
||||
call_args = mock_table.add_row.call_args[0]
|
||||
assert "Invalid-Date" in call_args[0] # Date fallback
|
||||
assert "Unknown" in call_args[1] # Activity type fallback
|
||||
assert "N/A" in call_args[2] # Duration fallback
|
||||
assert "N/A" in call_args[3] # Distance fallback
|
||||
# Check that it fell back to default/graceful values
|
||||
call_args = mock_table.add_row.call_args[0]
|
||||
assert "Invalid-Date" in call_args[0] # Date fallback
|
||||
assert "Unknown" in call_args[1] # Activity type fallback
|
||||
assert "N/A" in call_args[2] # Duration fallback
|
||||
assert "N/A" in call_args[3] # Distance fallback
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_button_pressed_check_sync(self):
|
||||
@@ -727,15 +708,14 @@ class TestWorkoutView:
|
||||
async with App().run_test() as pilot:
|
||||
workout_view = WorkoutView()
|
||||
await pilot.app.mount(workout_view)
|
||||
async with workout_view.run_test():
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "check-sync-btn"
|
||||
mock_button = MagicMock()
|
||||
mock_button.id = "check-sync-btn"
|
||||
|
||||
with patch.object(workout_view, 'check_sync_status') as mock_check_sync:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
with patch.object(workout_view, 'check_sync_status') as mock_check_sync:
|
||||
event = Button.Pressed(mock_button)
|
||||
await workout_view.on_button_pressed(event)
|
||||
|
||||
mock_check_sync.assert_called_once()
|
||||
mock_check_sync.assert_called_once()
|
||||
|
||||
class TestWorkoutMetricsChart:
|
||||
"""Test suite for the WorkoutMetricsChart widget."""
|
||||
@@ -784,9 +764,8 @@ class TestWorkoutAnalysisPanel:
|
||||
async with App().run_test() as pilot:
|
||||
panel = WorkoutAnalysisPanel(workout_data={}, analyses=mock_workout_analyses)
|
||||
await pilot.app.mount(panel)
|
||||
async with panel.run_test():
|
||||
# Check that it creates a Collapsible widget when analysis is present
|
||||
assert panel.query_one(Collapsible)
|
||||
# Check that it creates a Collapsible widget when analysis is present
|
||||
assert panel.query(Collapsible)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_compose_no_analysis(self):
|
||||
@@ -794,10 +773,9 @@ class TestWorkoutAnalysisPanel:
|
||||
async with App().run_test() as pilot:
|
||||
panel = WorkoutAnalysisPanel(workout_data={}, analyses=[])
|
||||
await pilot.app.mount(panel)
|
||||
async with panel.run_test():
|
||||
# Check that it shows a "No analysis" message and an "Analyze" button
|
||||
assert "No analysis available" in panel.query_one(Static).render()
|
||||
assert panel.query_one("#analyze-workout-btn", Button)
|
||||
# Check that it shows a "No analysis" message and an "Analyze" button
|
||||
assert "No analysis available" in str(panel.query_one(Static).render())
|
||||
assert panel.query_one("#analyze-workout-btn", Button)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user