From 3d992ef71ba8808937b3fe7740773f3f320798d2 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 15 Aug 2026 21:32:58 +0200 Subject: [PATCH] Split manual_workout_test.dart (350) into three by group 402 Dart tests before and after. The cross-language wire-format group moves with its siblings and still pins the same literal keys the Python side emits. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y --- .../models/manual_workout_budget_test.dart | 165 ++++++++++++++++++ .../test/models/manual_workout_test.dart | 103 ----------- .../manual_workout_validation_test.dart | 134 ++++++++++++++ 3 files changed, 299 insertions(+), 103 deletions(-) create mode 100644 stronglift_replacement/workout_app/test/models/manual_workout_budget_test.dart create mode 100644 stronglift_replacement/workout_app/test/models/manual_workout_validation_test.dart diff --git a/stronglift_replacement/workout_app/test/models/manual_workout_budget_test.dart b/stronglift_replacement/workout_app/test/models/manual_workout_budget_test.dart new file mode 100644 index 0000000..e5e0b09 --- /dev/null +++ b/stronglift_replacement/workout_app/test/models/manual_workout_budget_test.dart @@ -0,0 +1,165 @@ +import 'package:crdt_sync/crdt_sync.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:workout_app/models/manual_workout.dart'; + +// Builds a VALID table-tennis draft; override individual fields per test. +ManualWorkoutDraft _tt({ + String sport = kSportTableTennis, + String startTime = '18:00', + String endTime = '19:30', + String locationName = 'Solec', + String transportMethod = 'bike', + String cost = '40 PLN', + int rpe = 6, + String wentWell = 'Served consistently and moved feet well', + String toImprove = 'Backhand topspin needs more consistency', + String overallFeeling = 'Felt strong focused and in good rhythm', + int matchesWon = 3, + int matchesLost = 1, + int setsWon = 7, + int setsLost = 4, + String racket = 'Butterfly', + String balls = 'Nittaku 3-star', +}) => ManualWorkoutDraft( + sport: sport, + startTime: startTime, + endTime: endTime, + locationName: locationName, + transportMethod: transportMethod, + cost: cost, + rpe: rpe, + wentWell: wentWell, + toImprove: toImprove, + overallFeeling: overallFeeling, + matchesWon: matchesWon, + matchesLost: matchesLost, + setsWon: setsWon, + setsLost: setsLost, + racket: racket, + balls: balls, +); + +// Builds a VALID "other sport" draft; override individual fields per test. +ManualWorkoutDraft _other({ + String activityTypeOther = 'squash', + String activityDetails = + 'Full-court squash drills and three practice games total', + String equipment = 'racket, goggles', +}) => ManualWorkoutDraft( + sport: kSportOther, + startTime: '09:00', + endTime: '10:00', + locationName: 'Squash club', + transportMethod: 'car', + cost: '30 PLN', + rpe: 5, + wentWell: 'Moved well and kept long rallies going', + toImprove: 'Need to volley earlier off the back wall', + overallFeeling: 'Tired but satisfied with the effort', + activityTypeOther: activityTypeOther, + activityDetails: activityDetails, + equipment: equipment, +); + +// The cross-language wire fixture — an IDENTICAL literal lives in the Python +// suite (test_manual_workout.py::TestSyncWireFormat). Neither side's own +// round-trip test catches a key-name/format drift from the other; this shared +// literal on both sides does. Change one, change the other. +const _wireDate = '2026-07-13'; +const _wirePayload = { + 'type': 'manual_workout', + 'source': 'table tennis at Solec', + 'sport': 'table_tennis', + 'activity_type': 'table tennis', + 'start_time': '18:00', + 'end_time': '19:30', + 'duration_minutes': '90.0', + 'location_name': 'Solec', + 'transport_method': 'bike', + 'cost': '40 PLN', + 'reservation_phone': '600100200', + 'rpe': 6, + 'techniques_practiced': 'topspin serve', + 'warm_up_minutes': '10', + 'pain_or_injury': 'none', + 'went_well': 'Served consistently and moved feet well', + 'to_improve': 'Backhand topspin needs more consistency', + 'overall_feeling': 'Felt strong focused and in good rhythm', + 'matches_won': 3, + 'matches_lost': 1, + 'sets_won': 7, + 'sets_lost': 4, + 'racket': 'Butterfly', + 'balls': 'Nittaku 3-star', + 'kind': 'manual_workout', + 'date': '2026-07-13', +}; + +void main() { + group('countManualBudget (per-entry)', () { + Map payload(String date, String start) => { + 'date': date, + 'start_time': start, + }; + final now = DateTime(2026, 7, 16); + + // Was "count as a single day" until 2026-08-09, which disagreed with the + // PC's count_in_window ("Counted per entry, not per day"): three same-day + // workouts read 3/2 there — over the 7-day cap — but 1/2 here, so the + // phone kept accepting what the PC had already refused. + test('multiple workouts on one day each consume a slot', () { + final budget = countManualBudget([ + payload('2026-07-13', '14:00'), + payload('2026-07-13', '23:07'), + ], now); + expect(budget.week, 2); + expect(budget.month, 2); + }); + + test('three on one day exhaust the 7-day cap, matching the PC', () { + final budget = countManualBudget([ + payload('2026-07-13', '09:00'), + payload('2026-07-13', '14:00'), + payload('2026-07-13', '19:00'), + ], now); + expect(budget.week, 3); + expect(budget.week >= kManualWorkoutBudgetPer7Days, isTrue); + }); + + test('workouts on distinct days each count', () { + final budget = countManualBudget([ + payload('2026-07-13', '14:00'), + payload('2026-07-15', '09:00'), + ], now); + expect(budget.week, 2); + }); + + test('same-day pair plus another day counts three entries', () { + final budget = countManualBudget([ + payload('2026-07-13', '14:00'), + payload('2026-07-13', '23:07'), + payload('2026-07-15', '09:00'), + ], now); + expect(budget.week, 3); + }); + + test('ignores dates outside the 30-day window and future dates', () { + final budget = countManualBudget([ + payload('2026-05-01', '10:00'), + payload('2026-07-20', '10:00'), + ], now); + expect(budget.week, 0); + expect(budget.month, 0); + }); + + test('skips payloads with missing or unparsable date', () { + final budget = countManualBudget([ + {'start_time': '10:00'}, + {'date': 42}, + {'date': 'not-a-date'}, + ], now); + expect(budget.week, 0); + expect(budget.month, 0); + }); + }); +} diff --git a/stronglift_replacement/workout_app/test/models/manual_workout_test.dart b/stronglift_replacement/workout_app/test/models/manual_workout_test.dart index 4a31642..a3e60cb 100644 --- a/stronglift_replacement/workout_app/test/models/manual_workout_test.dart +++ b/stronglift_replacement/workout_app/test/models/manual_workout_test.dart @@ -244,107 +244,4 @@ void main() { expect(validateManualWorkout(_other(activityDetails: 'short')), isNotNull); }); }); - - group('buildEntry', () { - test('table tennis carries score fields and a table-tennis label', () { - final entry = buildEntry(_tt()); - expect(entry['activity_type'], 'table tennis'); - expect(entry['matches_lost'], 1); - expect(entry['source'], 'table tennis at Solec'); - }); - - test('other sport carries description/equipment and its own label', () { - final entry = buildEntry(_other()); - expect(entry['activity_type'], 'squash'); - expect(entry['activity_details'], contains('squash')); - expect(entry['equipment'], 'racket, goggles'); - }); - - test('duration is empty for an unparsable time', () { - expect(buildEntry(_tt(startTime: 'bogus'))['duration_minutes'], ''); - }); - - test('strips whitespace from fields', () { - final entry = buildEntry(_tt(locationName: ' Solec ', racket: ' pro ')); - expect(entry['location_name'], 'Solec'); - expect(entry['racket'], 'pro'); - }); - }); - - group('durationMinutes', () { - test('returns minutes for a valid range', () { - expect(durationMinutes(_tt(startTime: '12:00', endTime: '13:30')), 90.0); - }); - - test('returns null for an unparsable time', () { - expect(durationMinutes(_tt(startTime: 'noon')), isNull); - }); - }); - - group('countManualBudget (per-entry)', () { - Map payload(String date, String start) => { - 'date': date, - 'start_time': start, - }; - final now = DateTime(2026, 7, 16); - - // Was "count as a single day" until 2026-08-09, which disagreed with the - // PC's count_in_window ("Counted per entry, not per day"): three same-day - // workouts read 3/2 there — over the 7-day cap — but 1/2 here, so the - // phone kept accepting what the PC had already refused. - test('multiple workouts on one day each consume a slot', () { - final budget = countManualBudget([ - payload('2026-07-13', '14:00'), - payload('2026-07-13', '23:07'), - ], now); - expect(budget.week, 2); - expect(budget.month, 2); - }); - - test('three on one day exhaust the 7-day cap, matching the PC', () { - final budget = countManualBudget([ - payload('2026-07-13', '09:00'), - payload('2026-07-13', '14:00'), - payload('2026-07-13', '19:00'), - ], now); - expect(budget.week, 3); - expect(budget.week >= kManualWorkoutBudgetPer7Days, isTrue); - }); - - test('workouts on distinct days each count', () { - final budget = countManualBudget([ - payload('2026-07-13', '14:00'), - payload('2026-07-15', '09:00'), - ], now); - expect(budget.week, 2); - }); - - test('same-day pair plus another day counts three entries', () { - final budget = countManualBudget([ - payload('2026-07-13', '14:00'), - payload('2026-07-13', '23:07'), - payload('2026-07-15', '09:00'), - ], now); - expect(budget.week, 3); - }); - - test('ignores dates outside the 30-day window and future dates', () { - final budget = countManualBudget([ - payload('2026-05-01', '10:00'), - payload('2026-07-20', '10:00'), - ], now); - expect(budget.week, 0); - expect(budget.month, 0); - }); - - test('skips payloads with missing or unparsable date', () { - final budget = countManualBudget([ - {'start_time': '10:00'}, - {'date': 42}, - {'date': 'not-a-date'}, - ], now); - expect(budget.week, 0); - expect(budget.month, 0); - }); - }); } diff --git a/stronglift_replacement/workout_app/test/models/manual_workout_validation_test.dart b/stronglift_replacement/workout_app/test/models/manual_workout_validation_test.dart new file mode 100644 index 0000000..2bd75ec --- /dev/null +++ b/stronglift_replacement/workout_app/test/models/manual_workout_validation_test.dart @@ -0,0 +1,134 @@ +import 'package:crdt_sync/crdt_sync.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:workout_app/models/manual_workout.dart'; + +// Builds a VALID table-tennis draft; override individual fields per test. +ManualWorkoutDraft _tt({ + String sport = kSportTableTennis, + String startTime = '18:00', + String endTime = '19:30', + String locationName = 'Solec', + String transportMethod = 'bike', + String cost = '40 PLN', + int rpe = 6, + String wentWell = 'Served consistently and moved feet well', + String toImprove = 'Backhand topspin needs more consistency', + String overallFeeling = 'Felt strong focused and in good rhythm', + int matchesWon = 3, + int matchesLost = 1, + int setsWon = 7, + int setsLost = 4, + String racket = 'Butterfly', + String balls = 'Nittaku 3-star', +}) => ManualWorkoutDraft( + sport: sport, + startTime: startTime, + endTime: endTime, + locationName: locationName, + transportMethod: transportMethod, + cost: cost, + rpe: rpe, + wentWell: wentWell, + toImprove: toImprove, + overallFeeling: overallFeeling, + matchesWon: matchesWon, + matchesLost: matchesLost, + setsWon: setsWon, + setsLost: setsLost, + racket: racket, + balls: balls, +); + +// Builds a VALID "other sport" draft; override individual fields per test. +ManualWorkoutDraft _other({ + String activityTypeOther = 'squash', + String activityDetails = + 'Full-court squash drills and three practice games total', + String equipment = 'racket, goggles', +}) => ManualWorkoutDraft( + sport: kSportOther, + startTime: '09:00', + endTime: '10:00', + locationName: 'Squash club', + transportMethod: 'car', + cost: '30 PLN', + rpe: 5, + wentWell: 'Moved well and kept long rallies going', + toImprove: 'Need to volley earlier off the back wall', + overallFeeling: 'Tired but satisfied with the effort', + activityTypeOther: activityTypeOther, + activityDetails: activityDetails, + equipment: equipment, +); + +// The cross-language wire fixture — an IDENTICAL literal lives in the Python +// suite (test_manual_workout.py::TestSyncWireFormat). Neither side's own +// round-trip test catches a key-name/format drift from the other; this shared +// literal on both sides does. Change one, change the other. +const _wireDate = '2026-07-13'; +const _wirePayload = { + 'type': 'manual_workout', + 'source': 'table tennis at Solec', + 'sport': 'table_tennis', + 'activity_type': 'table tennis', + 'start_time': '18:00', + 'end_time': '19:30', + 'duration_minutes': '90.0', + 'location_name': 'Solec', + 'transport_method': 'bike', + 'cost': '40 PLN', + 'reservation_phone': '600100200', + 'rpe': 6, + 'techniques_practiced': 'topspin serve', + 'warm_up_minutes': '10', + 'pain_or_injury': 'none', + 'went_well': 'Served consistently and moved feet well', + 'to_improve': 'Backhand topspin needs more consistency', + 'overall_feeling': 'Felt strong focused and in good rhythm', + 'matches_won': 3, + 'matches_lost': 1, + 'sets_won': 7, + 'sets_lost': 4, + 'racket': 'Butterfly', + 'balls': 'Nittaku 3-star', + 'kind': 'manual_workout', + 'date': '2026-07-13', +}; + +void main() { + group('buildEntry', () { + test('table tennis carries score fields and a table-tennis label', () { + final entry = buildEntry(_tt()); + expect(entry['activity_type'], 'table tennis'); + expect(entry['matches_lost'], 1); + expect(entry['source'], 'table tennis at Solec'); + }); + + test('other sport carries description/equipment and its own label', () { + final entry = buildEntry(_other()); + expect(entry['activity_type'], 'squash'); + expect(entry['activity_details'], contains('squash')); + expect(entry['equipment'], 'racket, goggles'); + }); + + test('duration is empty for an unparsable time', () { + expect(buildEntry(_tt(startTime: 'bogus'))['duration_minutes'], ''); + }); + + test('strips whitespace from fields', () { + final entry = buildEntry(_tt(locationName: ' Solec ', racket: ' pro ')); + expect(entry['location_name'], 'Solec'); + expect(entry['racket'], 'pro'); + }); + }); + + group('durationMinutes', () { + test('returns minutes for a valid range', () { + expect(durationMinutes(_tt(startTime: '12:00', endTime: '13:30')), 90.0); + }); + + test('returns null for an unparsable time', () { + expect(durationMinutes(_tt(startTime: 'noon')), isNull); + }); + }); +}