feat(train): Profile opens on your body, the last 12 weeks and this year (ADR 0030, D60)

Above the days: a card with your body's weight and resting and maximal
heart rate, tapped opening Your body; the last 12 weeks' time trained as
bars, a week tapped showing its time, distance and workouts; and this
calendar year by activity. Home's week card reads its time as the days do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-31 18:46:26 +01:00
commit 5cfab59bd2
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 713 additions and 46 deletions

View file

@ -29,6 +29,7 @@ import 'package:halkyon_train_flutter/src/feature/plan/model/plan.dart';
import 'package:halkyon_train_flutter/src/feature/plan/model/stored_plan.dart';
import 'package:halkyon_train_flutter/src/feature/profile/data/profile_repository.dart';
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
import 'package:halkyon_train_flutter/src/feature/profile/widget/body_card.dart';
import 'package:halkyon_train_flutter/src/feature/profile/widget/profile_screen.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/announce_store.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/device_store.dart';
@ -44,6 +45,7 @@ import 'package:halkyon_train_flutter/src/feature/route/data/routing_gateway.dar
import 'package:halkyon_train_flutter/src/feature/route/model/route.dart';
import 'package:halkyon_train_flutter/src/feature/workout/data/workout_repository.dart';
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
import 'package:halkyon_train_flutter/src/feature/workout/widget/progress_cards.dart';
import 'package:halkyon_train_flutter/src/feature/workout/widget/week_card.dart';
import 'package:halkyon_train_flutter/src/tabs/maps_tab.dart';
import 'package:halkyon_train_flutter/src/tabs/record_tab.dart';
@ -661,6 +663,65 @@ void main() {
expect(find.text('52'), findsOneWidget);
expect(find.text('Female'), findsOneWidget);
});
testWidgets("Profile's card shows the figures given, opens Your body, and "
'follows a change (D60)', (tester) async {
await tester.runAsync(
() => profile.save(
const ProfileContent(
weightKg: 72.5,
restingHeartRate: 52,
maxHeartRate: 188,
),
),
);
await start(tester, usedBefore: true, profile: profile);
await goTab(tester, 'Profile');
await tester.runAsync(pumpEventQueue);
await tester.pump();
final card = find.byType(BodyCard);
expect(
find.descendant(
of: card,
matching: find.text('72.5 kg · resting 52 bpm · max 188 bpm'),
),
findsOneWidget,
);
await tester.runAsync(
() => profile.save(const ProfileContent(restingHeartRate: 50)),
);
await tester.runAsync(pumpEventQueue);
await tester.pump();
expect(
find.descendant(of: card, matching: find.text('resting 50 bpm')),
findsOneWidget,
);
await tester.tap(card);
await tester.pumpAndSettle();
expect(find.text('50'), findsOneWidget);
});
testWidgets("without figures, Profile's card says what they're for", (
tester,
) async {
await start(tester, usedBefore: true, profile: profile);
await goTab(tester, 'Profile');
await tester.runAsync(pumpEventQueue);
await tester.pump();
expect(
find.descendant(
of: find.byType(BodyCard),
matching: find.text(
'What your band and your heart-rate zones work from',
),
),
findsOneWidget,
);
});
});
group('the Polar Loop (ADR 0030, D34, D35)', () {
@ -3143,6 +3204,52 @@ void main() {
});
});
testWidgets('Profile charts the last 12 weeks, this week chosen, a week '
'tapped showing its own, and sums up the year by activity (D60)', (
tester,
) async {
tester.platformDispatcher.localesTestValue = const [Locale('en', 'GB')];
addTearDown(tester.platformDispatcher.clearLocalesTestValue);
// Saturday, 31 October 2026.
await withClock(Clock.fixed(DateTime(2026, 10, 31, 15)), () async {
await saveRun(DateTime(2026, 10, 27, 7, 30));
await saveRow();
await saveRun(DateTime(2026, 9, 3, 18));
await startWith(tester);
await goTab(tester, 'Profile');
final weeks = find.byType(WeeksCard);
Finder inWeeks(String text) =>
find.descendant(of: weeks, matching: find.text(text));
expect(inWeeks('Last 12 weeks'), findsOneWidget);
expect(inWeeks('This week'), findsOneWidget);
// 12:30 running and 30:00 rowing.
expect(inWeeks('42 min · 2.50 km · 2 workouts'), findsOneWidget);
await tester.tap(find.bySemanticsLabel('Week of 31 Aug'));
await tester.pump();
expect(inWeeks('Week of 31 Aug'), findsOneWidget);
expect(inWeeks('12 min · 2.50 km · 1 workout'), findsOneWidget);
final year = find.byType(YearCard);
expect(
find.descendant(of: year, matching: find.text('2026')),
findsOneWidget,
);
double top(String text) => tester
.getTopLeft(find.descendant(of: year, matching: find.text(text)))
.dy;
expect(top('Rowing indoors'), lessThan(top('Running')));
expect(
find.descendant(
of: year,
matching: find.text('2 workouts · 25 min · 5.00 km'),
),
findsOneWidget,
);
});
});
testWidgets("a workout's page shows its map, totals, splits and charts, "
'and where the map comes from', (tester) async {
await saveRun(DateTime(2026, 10, 27, 7, 30));

View file

@ -109,4 +109,84 @@ void main() {
expect(week.longestDay, const Duration(hours: 1));
});
group('the weeks Profile shows (D60)', () {
test('twelve, oldest first, the last this one', () {
final weeks = weeksBack(
[
workout(DateTime(2026, 10, 27, 7), moving: const Duration(hours: 1)),
workout(DateTime(2026, 8, 12, 7), moving: const Duration(hours: 2)),
// Thirteen weeks back: out.
workout(DateTime(2026, 7, 29, 7)),
],
now: now,
firstDay: DateTime.monday,
);
expect(weeks, hasLength(12));
expect(weeks.last.start, DateTime(2026, 10, 26));
expect(weeks.first.start, DateTime(2026, 8, 10));
expect(weeks.last.time, const Duration(hours: 1));
expect(weeks.first.time, const Duration(hours: 2));
expect(weeks.map((week) => week.count).reduce((a, b) => a + b), 2);
});
test('by the calendar across a change of the clocks', () {
// Clocks went back on 25 October 2026 in Europe.
final weeks = weeksBack(
const [],
now: now,
firstDay: DateTime.monday,
);
for (final week in weeks) {
expect(week.start.weekday, DateTime.monday);
expect(week.start.hour, 0);
}
});
});
group('the year Profile shows (D60)', () {
test('per activity, the most trained first, this calendar year alone', () {
final year = yearSummary([
workout(
DateTime(2026, 3, 1, 7),
moving: const Duration(hours: 1),
metres: 10000,
),
workout(
DateTime(2026, 10, 30, 7),
moving: const Duration(hours: 1),
metres: 12000,
),
WorkoutContent(
activity: Activity.cycling,
startedAt: DateTime(2026, 6, 1, 9).toUtc(),
endedAt: DateTime(2026, 6, 1, 12).toUtc(),
distanceMetres: 80000,
),
// Last year's.
workout(DateTime(2025, 12, 31, 20), metres: 5000),
], now: now);
expect(year.year, 2026);
expect(year.activities, [
(
activity: Activity.cycling,
count: 1,
time: const Duration(hours: 3),
metres: 80000,
),
(
activity: Activity.running,
count: 2,
time: const Duration(hours: 2),
metres: 22000,
),
]);
});
test('a year without workouts lists none', () {
expect(yearSummary(const [], now: now).activities, isEmpty);
});
});
}