Redesign BP tracker with lightweight views and unobtrusive save confirmations

This commit is contained in:
Peter Stockings
2026-09-08 15:07:57 +10:00
parent 25d1774e53
commit 87b60aac9b
40 changed files with 2693 additions and 2553 deletions
+197
View File
@@ -0,0 +1,197 @@
import csv
import gzip
import io
from datetime import datetime, timedelta
from pathlib import Path
import pytest
from pytz import timezone
from app import db
from app.insights import parse_range, readings_query, chart_data, summary
from app.models import Reading, Profile
def add(timestamp, systolic=120, user_id=1, diastolic=75, heart_rate=65):
item = Reading(user_id=user_id, timestamp=timestamp, systolic=systolic,
diastolic=diastolic, heart_rate=heart_rate)
db.session.add(item)
db.session.commit()
return item
@pytest.mark.parametrize('url', ['/', '/dashboard', '/dashboard?view=history', '/dashboard?view=trends',
'/dashboard?view=history&mode=calendar', '/reading/add', '/user/profile', '/data/',
'/dashboard/report', '/auth/login', '/auth/signup'])
def test_pages_render(client, url):
response = client.get(url, follow_redirects=True)
assert response.status_code == 200
assert 'BP Tracker' in response.text
assert 'private, no-store' == response.headers['Cache-Control']
def test_empty_state(client):
response = client.get('/dashboard')
assert 'No readings yet' in response.text
assert '0 / 0' not in response.text
assert 'hx-trigger' not in response.text
@pytest.mark.parametrize('day,hours', [('2026-10-04', 23), ('2026-04-05', 25)])
def test_range_includes_entire_local_day_excludes_next_midnight(app, day, hours):
selected = parse_range({'start_date': day, 'end_date': day}, timezone('Australia/Sydney'))
assert (selected.end_utc - selected.start_utc).total_seconds() == hours * 3600
add(selected.start_utc - timedelta(seconds=1))
add(selected.start_utc, 110)
add(selected.end_utc - timedelta(microseconds=1), 130)
add(selected.end_utc, 160)
add(selected.start_utc, 200, user_id=2)
stats = summary(readings_query(1, selected))
assert stats['count'] == 2
assert stats['systolic'] == 120
@pytest.mark.parametrize('query', ['start_date=no&end_date=no', 'start_date=2026-09-01',
'start_date=2026-09-08&end_date=2026-09-01', 'days=all', 'days=-1',
'start_date=0001-01-01&end_date=9999-12-31'])
def test_invalid_range_is_actionable(client, query):
assert client.get('/dashboard?' + query).status_code == 400
assert client.get('/data/export?' + query).status_code == 400
def test_save_another_and_validation_preserve_timestamp(client):
values = dict(timestamp='2026-09-08T08:15', systolic='121', diastolic='77', heart_rate='67', action='another')
result = client.post('/reading/add', data=values, follow_redirects=True)
assert result.status_code == 200
assert result.request.path == '/reading/add'
assert 'Reading added successfully.' in result.text
assert 'class="notifications"' in result.text
assert 'value="121"' not in result.text
saved = Reading.query.one()
assert saved.timestamp == datetime(2026, 9, 7, 22, 15)
values['systolic'] = '999'
result = client.post('/reading/add', data=values)
assert result.status_code == 200
assert '2026-09-08T08:15' in result.text
assert 'value="999"' in result.text
assert Reading.query.count() == 1
def test_edit_timestamp_and_delete(client):
item = add(datetime(2026, 9, 7, 22))
result = client.post(f'/reading/{item.id}/edit', data=dict(timestamp='2026-09-08T09:20', systolic=125, diastolic=80, heart_rate=70))
assert result.status_code == 302
assert db.session.get(Reading, item.id).timestamp == datetime(2026, 9, 7, 23, 20)
assert client.get(f'/reading/{item.id}/confirm_delete').status_code == 200
result = client.post(f'/reading/{item.id}/delete')
assert result.status_code == 302
assert Reading.query.count() == 0
def test_user_isolation_and_export(client):
add(datetime(2026, 9, 7, 22), 123)
other = add(datetime(2026, 9, 7, 22), 234, user_id=2)
add(datetime(2026, 9, 8, 14), 190) # Midnight the following day in Sydney.
query = 'start_date=2026-09-08&end_date=2026-09-08'
response = client.get('/data/export?' + query)
assert response.status_code == 200
rows = list(csv.reader(io.StringIO(response.text)))
assert len(rows) == 2
assert rows[1] == ['2026-09-07 22:00:00', '123', '75', '65']
assert 'attachment' in response.headers['Content-Disposition']
report = client.get('/dashboard/report?' + query)
assert report.status_code == 200
assert '234' not in report.text and '190</td>' not in report.text
for suffix in ('edit', 'confirm_delete'):
assert client.get(f'/reading/{other.id}/{suffix}').status_code == 404
assert client.post(f'/reading/{other.id}/delete').status_code == 404
def test_saved_preferences(client):
profile = Profile.query.filter_by(user_id=1).one()
profile.systolic_threshold = 150
profile.diastolic_threshold = 100
profile.dark_mode = True
db.session.commit()
add(datetime(2026, 9, 7, 22), 140, diastolic=90)
response = client.get('/dashboard?view=history&start_date=2026-09-08&end_date=2026-09-08')
assert 'data-theme="dark"' in response.text
assert 'Below threshold' in response.text
assert '150/100' in response.text
def test_large_history_and_payload_budgets(client):
start = datetime(2020, 1, 1)
db.session.bulk_save_objects([Reading(user_id=1, timestamp=start + timedelta(hours=i * 5),
systolic=110 + i % 40, diastolic=65 + i % 25, heart_rate=60 + i % 30) for i in range(10_000)])
db.session.commit()
selected = parse_range({'start_date': '2020-01-01', 'end_date': '2026-09-08'}, timezone('Australia/Sydney'))
query = readings_query(1, selected)
chart = chart_data(query, selected)
assert len(chart['points']) <= 180
assert sum(p['count'] for p in chart['points']) == 10_000
assert summary(query)['count'] == 10_000
history = client.get('/dashboard?view=history&start_date=2020-01-01&end_date=2026-09-08')
assert history.text.count('class="edit-link"') == 25
assert 'Page 1 of 400' in history.text
urls = ['/dashboard', '/dashboard?view=trends&start_date=2020-01-01&end_date=2026-09-08',
'/dashboard?view=history&mode=calendar&anchor=2020-01-01']
for url in urls:
response = client.get(url)
assert response.status_code == 200
assert len(gzip.compress(response.data)) <= 20_000
static = Path(__file__).resolve().parents[1] / 'app' / 'static'
css_size = len(gzip.compress((static / 'css/tailwind.css').read_bytes()))
js_size = len(gzip.compress((static / 'js/diy-turbo.min.js').read_bytes()))
assert css_size <= 8_000
assert js_size <= 5_000
assert len(gzip.compress(client.get('/dashboard').data)) + css_size + js_size + 276 <= 40_000
def test_compression_and_static_cache(client):
response = client.get('/dashboard', headers={'Accept-Encoding': 'gzip'})
assert response.headers['Content-Encoding'] == 'gzip'
assert b'Overview' in gzip.decompress(response.data)
import re
css = re.search(r'href="([^"]+tailwind.css[^"]+)"', client.get('/dashboard').text).group(1)
response = client.get(css.replace('&amp;', '&'))
assert 'immutable' in response.headers['Cache-Control']
def test_import_validation_and_atomicity(client):
payload = b'Timestamp,Systolic,Diastolic,Heart Rate\n2026-09-07 22:00:00,120,75,65\n2026-09-07 23:00:00,999,75,65\n'
response = client.post('/data/', data={'file': (io.BytesIO(payload), 'readings.csv')}, follow_redirects=True)
assert 'Check the measurement values on row 3' in response.text
assert Reading.query.count() == 0
payload = payload.split(b'2026-09-07 23:00:00')[0]
response = client.post('/data/', data={'file': (io.BytesIO(payload), 'readings.csv')}, follow_redirects=True)
assert 'Data imported successfully' in response.text
assert Reading.query.count() == 1
@pytest.mark.parametrize('local_time', ['2026-10-04T02:30', '2026-04-05T02:30'])
def test_ambiguous_or_nonexistent_entry_time(client, local_time):
response = client.post('/reading/add', data=dict(timestamp=local_time, systolic=120, diastolic=75, heart_rate=65))
assert response.status_code == 200
assert 'daylight saving' in response.text
assert Reading.query.count() == 0
def test_csrf_required_for_saves_deletes_and_imports(app, client):
app.config['WTF_CSRF_ENABLED'] = True
item = add(datetime(2026, 9, 7, 22))
response = client.post('/reading/add', data=dict(timestamp='2026-09-08T09:00', systolic=120, diastolic=75, heart_rate=65))
assert 'CSRF token is missing' in response.text
client.post(f'/reading/{item.id}/delete')
client.post('/data/', data={'file': (io.BytesIO(b'Timestamp,Systolic,Diastolic,Heart Rate\n2026-09-07 22:00:00,120,75,65\n'), 'readings.csv')})
assert Reading.query.count() == 1
@pytest.mark.parametrize('url,fragment', [('/dashboard/list', 'view=history'),
('/dashboard/table', 'view=history'), ('/dashboard/graph', 'view=trends'),
('/dashboard/weekly', 'unit=week'), ('/dashboard/monthly', 'unit=month')])
def test_old_bookmarks_still_work(client, url, fragment):
response = client.get(url)
assert response.status_code == 302
assert fragment in response.location
assert client.get(response.location).status_code == 200