Files
TimeTracker/tests/test_timezone.py
Dries Peeters 1e11ffec7f Fix keyboard shortcuts triggering in text input fields
- Fix keyboard shortcuts (like 'g r' for Go to Reports) incorrectly triggering
  while typing in input fields, textareas, and rich text editors
- Enhance detection for popular rich text editors:
  * Toast UI Editor (used in project descriptions)
  * TinyMCE, Quill, CodeMirror, Summernote
  * All contenteditable elements
- Allow specific global shortcuts even in input fields:
  * Ctrl+K / Cmd+K: Open command palette
  * Shift+?: Show keyboard shortcuts help
  * Ctrl+/: Focus search
- Clear key sequences when user starts typing to prevent partial matches
- Add debug logging for troubleshooting keyboard shortcut issues
- Update JavaScript cache busting version numbers (v=2.0, v=2.2)

Test improvements:
- Add comprehensive test suite for keyboard shortcuts input fix
  * Test typing 'gr' in 'program' doesn't trigger navigation
  * Test rich text editor detection logic
  * Test allowed shortcuts in inputs
- Refactor smoke tests to use admin_authenticated_client fixture
  instead of manual login (DRY principle)
- Fix Windows PermissionError in test cleanup for temporary files
- Add SESSION_COOKIE_HTTPONLY to test config for security
- Update test secret key length to meet requirements
- Remove duplicate admin user fixtures

Resolves issue where typing words like 'program' or 'graphics' in forms
would trigger unintended navigation shortcuts.
2025-10-29 18:17:04 +01:00

231 lines
7.7 KiB
Python

import pytest
from datetime import datetime, timedelta
from app import create_app, db
from app.models import Settings, TimeEntry, User, Project
from app.utils.timezone import get_app_timezone, utc_to_local, local_to_utc, now_in_app_timezone
@pytest.fixture
def app():
"""Create application for testing"""
app = create_app({
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
'WTF_CSRF_ENABLED': False,
'SECRET_KEY': 'test-secret-key-for-testing-at-least-32-chars',
'FLASK_ENV': 'testing'
})
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Create test client"""
return app.test_client()
@pytest.fixture
def user(app):
"""Create test user"""
with app.app_context():
user = User(username='testuser', role='user')
db.session.add(user)
db.session.commit()
return user
@pytest.fixture
def project(app):
"""Create test project"""
with app.app_context():
project = Project(name='Test Project', client='Test Client', billable=True, hourly_rate=50.0)
db.session.add(project)
db.session.commit()
return project
def test_timezone_default_from_environment(app):
"""Test that timezone defaults to environment variable when no database settings exist"""
with app.app_context():
# Clear any existing settings
Settings.query.delete()
db.session.commit()
# Test default timezone
timezone = get_app_timezone()
assert timezone == 'Europe/Rome' # Default from config
def test_timezone_from_database_settings(app):
"""Test that timezone is read from database settings"""
with app.app_context():
# Create settings with custom timezone
settings = Settings(timezone='America/New_York')
db.session.add(settings)
db.session.commit()
# Test that timezone is read from database
timezone = get_app_timezone()
assert timezone == 'America/New_York'
@pytest.mark.xfail(reason="Timezone display test needs adjustment - comparing timezone-aware datetimes")
def test_timezone_change_affects_display(app, user, project):
"""Test that changing timezone affects how times are displayed"""
with app.app_context():
# Create settings with Europe/Rome timezone
settings = Settings(timezone='Europe/Rome')
db.session.add(settings)
db.session.commit()
# Create a time entry at a specific UTC time
utc_time = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0)
# Refresh the user and project objects to ensure they're attached to the session
user = db.session.merge(user)
project = db.session.merge(project)
entry = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=utc_time,
end_time=utc_time + timedelta(hours=2),
source='manual'
)
db.session.add(entry)
db.session.commit()
# Get time in Rome timezone (UTC+1 or UTC+2 depending on DST)
rome_time = utc_to_local(entry.start_time)
# Change timezone to America/New_York
settings.timezone = 'America/New_York'
db.session.commit()
# Get time in New York timezone (UTC-5 or UTC-4 depending on DST)
ny_time = utc_to_local(entry.start_time)
# Times should be different
assert rome_time != ny_time
# New York time should be earlier than Rome time (behind UTC)
# This is a basic check - actual difference depends on DST
assert ny_time.hour != rome_time.hour or abs(ny_time.hour - rome_time.hour) > 1
@pytest.mark.xfail(reason="Timezone offset calculation needs adjustment - allows for timezone differences")
def test_timezone_aware_current_time(app):
"""Test that current time is returned in the configured timezone"""
with app.app_context():
# Set timezone to Europe/Rome
settings = Settings(timezone='Europe/Rome')
db.session.add(settings)
db.session.commit()
# Get current time in app timezone
app_time = now_in_app_timezone()
utc_time = datetime.now().replace(tzinfo=None)
# App time should be in Rome timezone
assert app_time.tzinfo is not None
assert 'Europe/Rome' in str(app_time.tzinfo)
# Times should be close (within a few seconds)
time_diff = abs((app_time.replace(tzinfo=None) - utc_time).total_seconds())
assert time_diff < 10
def test_timezone_conversion_utc_to_local(app):
"""Test UTC to local timezone conversion"""
with app.app_context():
# Set timezone to Asia/Tokyo
settings = Settings(timezone='Asia/Tokyo')
db.session.add(settings)
db.session.commit()
# Create a UTC time
utc_time = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0)
# Convert to Tokyo time
tokyo_time = utc_to_local(utc_time)
# Tokyo time should be ahead of UTC (UTC+9)
assert tokyo_time.tzinfo is not None
assert 'Asia/Tokyo' in str(tokyo_time.tzinfo)
# Tokyo time should be ahead of UTC time
# Tokyo is UTC+9, so 12:00 UTC should be 21:00 in Tokyo
assert tokyo_time.hour == 21
def test_timezone_conversion_local_to_utc(app):
"""Test local timezone to UTC conversion"""
with app.app_context():
# Set timezone to Europe/London
settings = Settings(timezone='Europe/London')
db.session.add(settings)
db.session.commit()
# Create a local time (assumed to be in London timezone)
local_time = datetime.now().replace(hour=15, minute=30, second=0, microsecond=0)
# Convert to UTC
utc_time = local_to_utc(local_time)
# UTC time should have timezone info
assert utc_time.tzinfo is not None
# Convert back to local to verify
back_to_local = utc_to_local(utc_time)
assert back_to_local.hour == 15
assert back_to_local.minute == 30
def test_invalid_timezone_fallback(app):
"""Test that invalid timezone falls back to UTC"""
with app.app_context():
# Set invalid timezone
settings = Settings(timezone='Invalid/Timezone')
db.session.add(settings)
db.session.commit()
# Should fall back to UTC
timezone = get_app_timezone()
assert timezone == 'Invalid/Timezone' # Still stored in database
# But timezone object should fall back to UTC
from app.utils.timezone import get_timezone_obj
tz_obj = get_timezone_obj()
assert 'UTC' in str(tz_obj)
def test_timezone_settings_update(app):
"""Test that updating timezone settings takes effect immediately"""
with app.app_context():
# Create initial settings
settings = Settings(timezone='Europe/Rome')
db.session.add(settings)
db.session.commit()
# Verify initial timezone
assert get_app_timezone() == 'Europe/Rome'
# Update timezone
settings.timezone = 'America/Los_Angeles'
db.session.commit()
# Verify timezone change takes effect
assert get_app_timezone() == 'America/Los_Angeles'
# Test that time conversion uses new timezone
utc_time = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0)
la_time = utc_to_local(utc_time)
# LA time should be in Pacific timezone
assert la_time.tzinfo is not None
assert 'America/Los_Angeles' in str(la_time.tzinfo)