mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-06 11:40:52 -06:00
- Normalize line endings from CRLF to LF across all files to match .editorconfig - Standardize quote style from single quotes to double quotes - Normalize whitespace and formatting throughout codebase - Apply consistent code style across 372 files including: * Application code (models, routes, services, utils) * Test files * Configuration files * CI/CD workflows This ensures consistency with the project's .editorconfig settings and improves code maintainability.
357 lines
14 KiB
Python
357 lines
14 KiB
Python
"""
|
|
Smoke tests for Project Dashboard feature.
|
|
Quick validation tests to ensure the dashboard is working at a basic level.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta, date
|
|
from decimal import Decimal
|
|
from app import create_app, db
|
|
from app.models import User, Project, Client, Task, TimeEntry, Activity
|
|
from app.models.kanban_column import KanbanColumn
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create and configure a test application instance."""
|
|
app = create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", "WTF_CSRF_ENABLED": False})
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
yield app
|
|
db.session.remove()
|
|
db.drop_all()
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create a test Flask client."""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def user(app):
|
|
"""Create a test user."""
|
|
with app.app_context():
|
|
user = User(username="testuser", role="user", email="test@example.com")
|
|
user.set_password("testpass123")
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
yield user
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client_obj(app):
|
|
"""Create a test client."""
|
|
with app.app_context():
|
|
client = Client(name="Test Client", description="A test client")
|
|
db.session.add(client)
|
|
db.session.commit()
|
|
yield client
|
|
|
|
|
|
@pytest.fixture
|
|
def project_with_data(app, test_client_obj, user):
|
|
"""Create a project with some sample data."""
|
|
with app.app_context():
|
|
# Avoid kanban default initialization during requests to prevent SQLite PK conflicts in tests
|
|
try:
|
|
import app.routes.projects as projects_routes
|
|
|
|
projects_routes.KanbanColumn.initialize_default_columns = staticmethod(lambda project_id=None: True)
|
|
except Exception:
|
|
pass
|
|
# Create project
|
|
project = Project(
|
|
name="Dashboard Test Project",
|
|
client_id=test_client_obj.id,
|
|
description="A test project",
|
|
billable=True,
|
|
hourly_rate=Decimal("100.00"),
|
|
budget_amount=Decimal("5000.00"),
|
|
)
|
|
project.estimated_hours = 50.0
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
# Add some tasks
|
|
task1 = Task(
|
|
project_id=project.id,
|
|
name="Test Task 1",
|
|
status="todo",
|
|
priority="high",
|
|
created_by=user.id,
|
|
assigned_to=user.id,
|
|
)
|
|
task2 = Task(
|
|
project_id=project.id,
|
|
name="Test Task 2",
|
|
status="done",
|
|
priority="medium",
|
|
created_by=user.id,
|
|
assigned_to=user.id,
|
|
)
|
|
db.session.add_all([task1, task2])
|
|
|
|
# Add time entries
|
|
now = datetime.now()
|
|
entry = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
task_id=task1.id,
|
|
start_time=now - timedelta(hours=4),
|
|
end_time=now,
|
|
duration_seconds=14400, # 4 hours
|
|
billable=True,
|
|
)
|
|
db.session.add(entry)
|
|
|
|
# Add activity
|
|
Activity.log(
|
|
user_id=user.id,
|
|
action="created",
|
|
entity_type="project",
|
|
entity_id=project.id,
|
|
entity_name=project.name,
|
|
description=f'Created project "{project.name}"',
|
|
)
|
|
|
|
db.session.commit()
|
|
yield project
|
|
|
|
|
|
def login(client, username="testuser", password="testpass123"):
|
|
"""Helper function to log in a user."""
|
|
return client.post("/auth/login", data={"username": username, "password": password}, follow_redirects=True)
|
|
|
|
|
|
class TestProjectDashboardSmoke:
|
|
"""Smoke tests for project dashboard functionality."""
|
|
|
|
def test_dashboard_page_loads(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard page loads without errors"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200, "Dashboard page should load successfully"
|
|
assert b"Dashboard" in response.data or b"dashboard" in response.data.lower()
|
|
|
|
def test_dashboard_requires_authentication(self, client, project_with_data):
|
|
"""Smoke test: Dashboard requires user to be logged in"""
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 302, "Should redirect to login"
|
|
|
|
def test_dashboard_shows_project_name(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard displays the project name"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert project_with_data.name.encode() in response.data
|
|
|
|
def test_dashboard_shows_key_metrics(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard displays key metrics cards"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Check for key metrics
|
|
assert b"Total Hours" in response.data or b"total hours" in response.data.lower()
|
|
assert b"Budget" in response.data or b"budget" in response.data.lower()
|
|
assert b"Tasks" in response.data or b"tasks" in response.data.lower()
|
|
assert b"Team" in response.data or b"team" in response.data.lower()
|
|
|
|
def test_dashboard_shows_charts(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard includes chart canvases"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Check for chart elements
|
|
assert b"canvas" in response.data or b"Chart" in response.data
|
|
|
|
def test_dashboard_shows_budget_visualization(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard shows budget vs actual section"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"Budget vs. Actual" in response.data or b"Budget" in response.data
|
|
|
|
def test_dashboard_shows_task_statistics(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard shows task statistics"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"Task" in response.data
|
|
# Should show task counts
|
|
assert b"2" in response.data # We created 2 tasks
|
|
|
|
def test_dashboard_shows_team_contributions(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard shows team member contributions"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"Team Member" in response.data or b"Contributions" in response.data
|
|
|
|
def test_dashboard_shows_recent_activity(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard shows recent activity section"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"Recent Activity" in response.data or b"Activity" in response.data
|
|
|
|
def test_dashboard_has_back_link(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard has link back to project view"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"Back to Project" in response.data
|
|
assert f"/projects/{project_with_data.id}".encode() in response.data
|
|
|
|
def test_dashboard_period_filter_works(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard period filter functions"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
# Test each period filter
|
|
for period in ["all", "week", "month", "3months", "year"]:
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard?period={period}")
|
|
assert response.status_code == 200, f"Dashboard should load with period={period}"
|
|
|
|
def test_dashboard_period_filter_dropdown(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard has period filter dropdown"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"periodFilter" in response.data or b"All Time" in response.data
|
|
|
|
def test_project_view_has_dashboard_link(self, client, user, project_with_data):
|
|
"""Smoke test: Project view page has link to dashboard"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}")
|
|
assert response.status_code == 200
|
|
# Be resilient to routing differences; check presence of dashboard link or text
|
|
page_text = response.get_data(as_text=True).lower()
|
|
assert ("dashboard" in page_text) or ("/dashboard" in page_text)
|
|
|
|
def test_dashboard_handles_no_data_gracefully(self, client, user, test_client_obj):
|
|
"""Smoke test: Dashboard handles project with no data"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
# Create empty project
|
|
empty_project = Project(name="Empty Project", client_id=test_client_obj.id)
|
|
db.session.add(empty_project)
|
|
db.session.commit()
|
|
|
|
response = client.get(f"/projects/{empty_project.id}/dashboard")
|
|
assert response.status_code == 200, "Dashboard should load even with no data"
|
|
|
|
def test_dashboard_shows_hours_worked(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard displays hours worked"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
# Should show 4.0 hours (from our test data)
|
|
assert b"4.0" in response.data
|
|
|
|
def test_dashboard_shows_budget_amount(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard displays budget amount"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
# Should show budget of 5000
|
|
assert b"5000" in response.data
|
|
|
|
def test_dashboard_calculates_completion_rate(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard calculates task completion rate"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
# With 1 done out of 2 tasks, should show 50%
|
|
assert b"50" in response.data or b"completion" in response.data.lower()
|
|
|
|
def test_dashboard_shows_team_member_name(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard shows team member username"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert user.username.encode() in response.data
|
|
|
|
def test_dashboard_handles_invalid_period(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard handles invalid period parameter gracefully"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard?period=invalid")
|
|
assert response.status_code == 200, "Should still load with invalid period"
|
|
|
|
def test_dashboard_404_for_nonexistent_project(self, client, user):
|
|
"""Smoke test: Dashboard returns 404 for non-existent project"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get("/projects/99999/dashboard")
|
|
assert response.status_code == 404
|
|
|
|
def test_dashboard_chart_js_loaded(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard loads Chart.js library"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"chart.js" in response.data.lower() or b"Chart" in response.data
|
|
|
|
def test_dashboard_responsive_layout(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard uses responsive grid layout"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
# Check for responsive grid classes
|
|
assert b"grid" in response.data or b"lg:grid-cols" in response.data
|
|
|
|
def test_dashboard_dark_mode_compatible(self, client, user, project_with_data):
|
|
"""Smoke test: Dashboard has dark mode styling"""
|
|
with client.session_transaction() as sess:
|
|
sess["_user_id"] = str(user.id)
|
|
|
|
response = client.get(f"/projects/{project_with_data.id}/dashboard")
|
|
assert response.status_code == 200
|
|
# Check for dark mode classes
|
|
assert b"dark:" in response.data
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|