"""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'
Exercises
5', response.data) self.assertIn(b'
Top sets
6', response.data) self.assertIn(b'
Reps
63', 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') 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) 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'