"""Workout rendering and HTMX lifecycle checks, using an isolated database."""
import unittest
from datetime import date
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
class WorkoutViewTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Import the application without opening a real database connection.
with patch('psycopg_pool.ConnectionPool'):
import app
cls.module = app
cls.app = app.app
cls.app.config.update(TESTING=True)
def setUp(self):
import routes.workout as workout
self.workout = workout
self.db = MagicMock()
for module in (self.module, workout):
patcher = patch.object(module, 'db', self.db)
patcher.start()
self.addCleanup(patcher.stop)
self.user = SimpleNamespace(id=1, name='Peter', is_authenticated=True,
is_active=True, is_anonymous=False,
is_admin=False, get_id=lambda: '1')
loader = patch.object(self.module.login_manager, '_user_callback', lambda _: self.user)
loader.start()
self.addCleanup(loader.stop)
self.client = self.app.test_client()
with self.client.session_transaction() as session:
session['_user_id'] = '1'
session['_fresh'] = True
self.top_sets = [
dict(topset_id=i, exercise_id=i, exercise_name=name, repetitions=reps, weight=weight)
for i, (name, reps, weight) in enumerate([
('30 degrees Smith Press', 7, 93.5),
('Seated Smith machine BTN press', 5, 61.5),
('Smith machine JM press', 12, 62.0),
('DB Lateral Raises', 20, 10.5),
('Smith machine Bulgarian split squat', 12, 96.0),
], 1)
]
self.distribution = [
dict(attribute_name=name, percentage=percent, color=color, count=1)
for name, percent, color in [('Shoulders', 40, '#477db5'), ('Chest', 20, '#299e9a'),
('Quads', 20, '#8b75bb'), ('Triceps', 20, '#c78565')]
]
self.view_model = dict(person_id=1, workout_id=1, person_name='Peter',
start_date=date(2026, 9, 5), note='', tags=[], top_sets=self.top_sets,
muscle_distribution=self.distribution, equipment_distribution=[], movement_distribution=[])
model = patch.object(workout, '_get_workout_view_model', lambda *args: self.view_model)
model.start()
self.addCleanup(model.stop)
self.db.execute.return_value = self.top_sets
self.db.get_topset.return_value = self.top_sets[0]
self.db.get_exercise.return_value = {'name': self.top_sets[0]['exercise_name']}
self.db.get_all_exercises.return_value = [{'exercise_id': 1, 'name': 'Smith Press'}]
self.db.create_topset.return_value = 6
self.db.get_topset_achievements.return_value = dict(is_pr_reps=True, rep_increase=1, weight_increase=0, stalled_sessions=0)
self.db.exercises.get_workout_attribute_distribution.return_value = self.distribution
self.base = '/person/1/workout/1'
def test_full_page_and_fragment_render(self):
for headers in ({}, {'HX-Request': 'true'}):
response = self.client.get(self.base, headers=headers)
self.assertEqual(response.status_code, 200)
self.assertIn(b'workout-layout', response.data)
self.assertIn(b'Session exercises', response.data)
self.assertEqual(response.data.count(b'id=extra-target-1'), 1)
def test_summary_counts_unique_exercises_and_refreshes(self):
self.db.execute.return_value = self.top_sets + [self.top_sets[0]]
response = self.client.get(self.base + '/summary')
self.assertEqual(response.status_code, 200)
self.assertIn(b'
Exercises5', response.data)
self.assertIn(b'Top sets6', response.data)
self.assertIn(b'Reps63', response.data)
self.assertIn(b'topsetChanged from:body', response.data)
def test_empty_distribution_keeps_refresh_target(self):
from flask import render_template
with self.app.test_request_context():
html = render_template('partials/workout_breakdown.html', person_id=1, workout_id=1,
distribution=[], category_name='Muscle Group')
self.assertIn('id="muscle-group-breakdown"', html)
self.assertIn('No categorized sets yet.', html)
self.assertIn('topsetAdded from:body', html)
def test_previous_performance_is_loaded_on_expansion(self):
self.db.get_most_recent_topset_for_exercise.return_value = (7, 93.5, 'Smith Press')
self.db.get_recent_topsets_for_exercise.return_value = [self.top_sets[0]]
response = self.client.get(self.base + '/exercise/most_recent_topset_for_exercise?exercise_id=1')
self.assertEqual(response.status_code, 200)
self.assertIn(b'toggle once from:closest details', response.data)
self.assertNotIn(b'hx-trigger=load', response.data)
self.assertIn(b'Previous performance', response.data)
self.assertNotIn(b'workout-stall-badge', response.data)
def test_previous_performance_shows_consecutive_stall_count(self):
self.db.get_most_recent_topset_for_exercise.return_value = (7, 93.5, 'Smith Press')
self.db.get_recent_topsets_for_exercise.return_value = [self.top_sets[0]]
for count, label in ((1, b'Stalled 1 time'), (3, b'Stalled 3 times')):
self.db.get_topset_achievements.return_value = {'stalled_sessions': count}
response = self.client.get(self.base + '/exercise/most_recent_topset_for_exercise?exercise_id=1')
self.assertEqual(response.status_code, 200)
self.assertIn(label, response.data)
self.assertIn('7 × 93.5 kg', response.get_data(as_text=True))
self.db.get_topset_achievements.assert_called_with(1)
def test_previous_performance_without_history_hides_stall_badge(self):
self.db.get_most_recent_topset_for_exercise.return_value = (None, None, 'Smith Press')
response = self.client.get(self.base + '/exercise/most_recent_topset_for_exercise?exercise_id=1')
self.assertEqual(response.status_code, 200)
self.assertNotIn(b'workout-stall-badge', response.data)
self.db.get_recent_topsets_for_exercise.assert_not_called()
def test_create_edit_cancel_update_and_delete_targets(self):
data = dict(exercise_id=1, repetitions=8, weight=93.5)
created = self.client.post(self.base + '/topset', data=data)
self.assertEqual(created.headers['HX-Trigger'], 'topsetAdded')
self.assertNotIn(b'hx-swap-oob', created.data)
for suffix in ('/topset/1/edit_form', '/topset/1'):
response = self.client.get(self.base + suffix)
self.assertEqual(response.status_code, 200)
self.assertIn(b'hx-swap-oob=outerHTML', response.data)
updated = self.client.put(self.base + '/topset/1', data=data)
self.assertEqual(updated.headers['HX-Trigger'], 'topsetChanged')
self.assertIn(b'hx-swap-oob=outerHTML', updated.data)
deleted = self.client.delete(self.base + '/topset/1/delete')
self.assertEqual(deleted.headers['HX-Trigger'], 'topsetChanged')
self.assertIn(b'hx-swap-oob=delete', deleted.data)
def test_summary_requires_owner(self):
with self.client.session_transaction() as session:
session.clear()
response = self.client.get(self.base + '/summary')
self.assertEqual(response.status_code, 302)
self.db.execute.assert_not_called()
def test_session_note_editor_round_trip_keeps_integrated_card(self):
import routes.notes as notes
text = 'Good energy\nKeep simple'
with patch.object(notes, 'db', self.db), patch.object(notes, '_fetch_workout_note', return_value=text), patch.object(notes, '_update_workout_note_for_person') as save:
edit = self.client.get(self.base + '/note/edit?view=session')
self.assertIn(b'workout-note-editor', edit.data)
# The HTML minifier can unescape RCDATA inside a textarea.
self.assertIn(b'Keep simple', edit.data)
self.assertIn(b'Save note', edit.data)
saved = self.client.put(self.base + '/note?view=session', data={'note': text})
save.assert_called_once_with(1, 1, text)
self.assertIn(b'workout-note-trigger', saved.data)
self.assertIn(b'Good energy
Keep <cues> simple', saved.data)
self.assertNotIn(b'