Files
workout/tests/test_workout_view.py
T

175 lines
9.0 KiB
Python

"""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'<dt>Exercises<dd>5', response.data)
self.assertIn(b'<dt>Top sets<dd>6', response.data)
self.assertIn(b'<dt>Reps<dd>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 <cues> 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 <cues> simple</textarea>', 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<br>Keep &lt;cues> simple', saved.data)
self.assertNotIn(b'<textarea', saved.data)
canceled = self.client.get(self.base + '/note?view=session')
self.assertIn(b'workout-note-preview', canceled.data)
legacy = self.client.get(self.base + '/note')
self.assertNotIn(b'workout-note-card', legacy.data)
def test_tags_return_updated_controls_after_select_clear_and_create(self):
import routes.tags as tags
tag = dict(tag_id=12, tag_name='Push', is_selected=True)
def execute(query, args=(), **kwargs):
if 'SELECT person_id' in query:
return {'person_id': 1}
if 'SELECT tag_id FROM Tag' in query:
return {'tag_id': 12}
if 'SELECT DISTINCT' in query:
return []
if 'FROM tag' in query:
return [tag]
return []
self.db.execute.side_effect = execute
with patch.object(tags, 'db', self.db):
for selected in ({'tag_id': '12'}, {}):
tag['is_selected'] = bool(selected)
response = self.client.post('/tag/workout/1/add?view=session', data=selected)
self.assertEqual(response.status_code, 200)
self.assertIn(b'workout-tag-editor-1', response.data)
self.assertIn(b'type=checkbox', response.data)
self.assertEqual(b' checked' in response.data, bool(selected))
tag['is_selected'] = True
response = self.client.post('/tag/workout/1/new?view=session', data={'tag_name': 'Push'})
self.assertEqual(response.status_code, 200)
self.assertIn(b'workout-tag-editor-1', response.data)
self.assertIn(b' checked', response.data)
if __name__ == '__main__':
unittest.main()