175 lines
9.0 KiB
Python
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 <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()
|