feat(train): Home, Maps, Record, Groups and Profile in Learn's frame, washed in carrot (ADR 0030, D60)
Train's tabs are Strava's five where one screen with four tabs at its top had held workouts, days, plans and routes: Home the workouts, Maps the routes, Record recording and the interval plans, Groups a screen saying they come later, Profile the days and the way to the account. A device that doesn't record has no Record tab, its plans beside the days in Profile. The frame is halkyon_ui's NavigationFrame, driven by go_router as Learn's: a workout's page opens in Home and a day's in Profile with the tabs beside them; the route and plan editors and the recording cover them, their content handed over as it is. Every tab washes down from the top in carrot, the warm end of Train's icon, as Learn's in cobalt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b45db6ea7c
commit
d4d749eb9f
1 changed files with 800 additions and 202 deletions
|
|
@ -231,8 +231,26 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Opens the account sheet from home.
|
||||
/// Goes to the tab [label] of the navigation bar (ADR 0030, D60).
|
||||
Future<void> goTab(WidgetTester tester, String label) async {
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(NavigationBar),
|
||||
matching: find.text(label),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// The Record tab's button, saying [label].
|
||||
Finder recordButton([String label = 'Record']) => find.descendant(
|
||||
of: find.byType(FloatingActionButton),
|
||||
matching: find.text(label),
|
||||
);
|
||||
|
||||
/// Opens the account sheet from Profile.
|
||||
Future<void> openAccount(WidgetTester tester) async {
|
||||
await goTab(tester, 'Profile');
|
||||
await tester.tap(find.byTooltip('Account'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
|
@ -285,6 +303,100 @@ void main() {
|
|||
expect(find.text('Record'), findsNothing);
|
||||
});
|
||||
|
||||
group('the tabs (ADR 0030, D60)', () {
|
||||
late Directory folder;
|
||||
|
||||
setUp(() => folder = Directory.systemTemp.createTempSync('train-tabs'));
|
||||
tearDown(() => folder.deleteSync(recursive: true));
|
||||
|
||||
/// A phone, which records.
|
||||
RecordingDevice phone() => (
|
||||
platform: FakeRecorderPlatform(),
|
||||
file: File('${folder.path}/recording.txt'),
|
||||
devices: {
|
||||
for (final kind in DeviceKind.values)
|
||||
kind: DeviceStore(File('${folder.path}/${kind.name}.json')),
|
||||
},
|
||||
announces: AnnounceStore(File('${folder.path}/announce.json')),
|
||||
);
|
||||
|
||||
/// The navigation bar's tabs, in order.
|
||||
List<String> tabs(WidgetTester tester) => [
|
||||
for (final destination
|
||||
in tester
|
||||
.widget<NavigationBar>(find.byType(NavigationBar))
|
||||
.destinations)
|
||||
(destination as NavigationDestination).label,
|
||||
];
|
||||
|
||||
testWidgets('a phone that records has Home, Maps, Record, Groups and '
|
||||
'Profile', (tester) async {
|
||||
await start(tester, usedBefore: true, recording: phone());
|
||||
|
||||
expect(tabs(tester), ['Home', 'Maps', 'Record', 'Groups', 'Profile']);
|
||||
await goTab(tester, 'Record');
|
||||
expect(recordButton(), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("a device that doesn't record has no Record tab, and its "
|
||||
'plans beside the days in Profile', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
||||
expect(tabs(tester), ['Home', 'Maps', 'Groups', 'Profile']);
|
||||
await goTab(tester, 'Profile');
|
||||
expect(find.text('Days'), findsOneWidget);
|
||||
expect(find.text('Plans'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Groups says they come later', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
||||
await goTab(tester, 'Groups');
|
||||
|
||||
expect(find.text('Groups come later'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the tabs wash down from the top in carrot, the warm end of '
|
||||
"Train's icon", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
||||
final wash = tester.element(find.byType(TopGradient).first);
|
||||
expect(ScreenColor.of(wash), PaletteColor.carrot.background);
|
||||
});
|
||||
|
||||
testWidgets('an editor covers the tabs; going back, the tab is where it '
|
||||
'was', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Maps');
|
||||
|
||||
await tester.tap(find.text('New route'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
expect(find.text('New route'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("from medium windows on, a rail with Train's icon on top", (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
tester.view.physicalSize = const Size(1000, 800);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(NavigationRail),
|
||||
matching: find.bySemanticsLabel('Halkyon Train'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('signed out, it asks how to start, with an account to create '
|
||||
'where passwords are offered (ADR 0017), and no GitHub (ADR 0020, '
|
||||
'D36)', (tester) async {
|
||||
|
|
@ -983,6 +1095,8 @@ void main() {
|
|||
// The account sheet, closed.
|
||||
await tester.tapAt(const Offset(200, 50));
|
||||
await tester.pumpAndSettle();
|
||||
// Home, where the workouts are.
|
||||
await goTab(tester, 'Home');
|
||||
|
||||
expect(find.textContaining('from Gadgetbridge'), findsOneWidget);
|
||||
expect(find.textContaining('Christopeit'), findsNothing);
|
||||
|
|
@ -1002,6 +1116,8 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
await tester.tapAt(const Offset(200, 50));
|
||||
await tester.pumpAndSettle();
|
||||
// Home, where the workouts are.
|
||||
await goTab(tester, 'Home');
|
||||
|
||||
await tester.tap(find.textContaining('from Gadgetbridge'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -1034,6 +1150,8 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
await tester.tapAt(const Offset(200, 50));
|
||||
await tester.pumpAndSettle();
|
||||
// Home, where the workouts are.
|
||||
await goTab(tester, 'Home');
|
||||
|
||||
await tester.tap(find.textContaining('from Christopeit Connect'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -1106,29 +1224,35 @@ void main() {
|
|||
|
||||
File file() => File('${folder.path}/recording.txt');
|
||||
|
||||
/// Train on a device that records, signed in, its clock at [now]'s.
|
||||
/// Train on a device that records, signed in, its clock at [now]'s, on
|
||||
/// the Record tab unless a recording cut off is offered first.
|
||||
Future<void> startRecording(
|
||||
WidgetTester tester, {
|
||||
PolarLoop? loop,
|
||||
PlanRepository? plans,
|
||||
RouteRepository? routes,
|
||||
}) => start(
|
||||
tester,
|
||||
usedBefore: true,
|
||||
recording: (
|
||||
platform: platform,
|
||||
file: file(),
|
||||
devices: {
|
||||
for (final kind in DeviceKind.values)
|
||||
kind: DeviceStore(File('${folder.path}/${kind.name}.json')),
|
||||
},
|
||||
announces: AnnounceStore(File('${folder.path}/announce.json')),
|
||||
),
|
||||
workouts: workouts,
|
||||
loop: loop,
|
||||
plans: plans,
|
||||
routes: routes,
|
||||
);
|
||||
}) async {
|
||||
await start(
|
||||
tester,
|
||||
usedBefore: true,
|
||||
recording: (
|
||||
platform: platform,
|
||||
file: file(),
|
||||
devices: {
|
||||
for (final kind in DeviceKind.values)
|
||||
kind: DeviceStore(File('${folder.path}/${kind.name}.json')),
|
||||
},
|
||||
announces: AnnounceStore(File('${folder.path}/announce.json')),
|
||||
),
|
||||
workouts: workouts,
|
||||
loop: loop,
|
||||
plans: plans,
|
||||
routes: routes,
|
||||
);
|
||||
if (find.byType(AlertDialog).evaluate().isEmpty) {
|
||||
await goTab(tester, 'Record');
|
||||
}
|
||||
}
|
||||
|
||||
/// Lets [seconds] pass, the screen following.
|
||||
Future<void> pass(WidgetTester tester, [int seconds = 1]) async {
|
||||
|
|
@ -1141,7 +1265,7 @@ void main() {
|
|||
'without kept tracks, a ride neither (D48)', (tester) async {
|
||||
await withClock(Clock(() => at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Cycling'));
|
||||
await tester.pump();
|
||||
|
|
@ -1185,7 +1309,7 @@ void main() {
|
|||
];
|
||||
await withClock(Clock(() => at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Power meter'), findsNothing);
|
||||
await tester.tap(find.text('Cycling'));
|
||||
|
|
@ -1212,7 +1336,7 @@ void main() {
|
|||
'remembered, a row offered none (D49)', (tester) async {
|
||||
await withClock(Clock(() => at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Cycling'));
|
||||
await tester.pump();
|
||||
|
|
@ -1259,7 +1383,7 @@ void main() {
|
|||
await withClock(Clock(() => now), () async {
|
||||
await startRecording(tester);
|
||||
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Record a workout'), findsOneWidget);
|
||||
await tester.tap(find.text('Cycling'));
|
||||
|
|
@ -1294,7 +1418,7 @@ void main() {
|
|||
|
||||
await tester.tap(find.text('Save'));
|
||||
await pass(tester, 2);
|
||||
expect(find.text('Record'), findsOneWidget);
|
||||
expect(recordButton(), findsOneWidget);
|
||||
final saved = (await workouts.all()).single.content!;
|
||||
expect(saved.activity, Activity.cycling);
|
||||
expect(saved.distanceMetres, 100);
|
||||
|
|
@ -1307,7 +1431,7 @@ void main() {
|
|||
) async {
|
||||
await withClock(Clock.fixed(at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Start'));
|
||||
await pass(tester, 2);
|
||||
|
|
@ -1330,7 +1454,7 @@ void main() {
|
|||
platform.allows = {RecorderPermission.notifications};
|
||||
await startRecording(tester);
|
||||
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Start'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -1400,7 +1524,7 @@ void main() {
|
|||
platform.runOn(file().path);
|
||||
await startRecording(tester);
|
||||
|
||||
await tester.tap(find.text('Back to the recording'));
|
||||
await tester.tap(recordButton('Back to the recording'));
|
||||
await pass(tester, 2);
|
||||
|
||||
expect(find.text('Walking'), findsOneWidget);
|
||||
|
|
@ -1440,16 +1564,23 @@ void main() {
|
|||
await withClock(Clock(() => at), () async {
|
||||
await startRecording(tester, plans: plans);
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Running'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Plan'));
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Sprints'));
|
||||
// The picker's, over the Record tab's list of plans.
|
||||
await tester.tap(find.text('Sprints').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Sprints'), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(SheetScaffold),
|
||||
matching: find.text('Sprints'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.tap(find.text('Start'));
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
await pass(tester, 2);
|
||||
|
|
@ -1496,6 +1627,8 @@ void main() {
|
|||
StepKind.recovery,
|
||||
]);
|
||||
|
||||
// The workout, in Home.
|
||||
await goTab(tester, 'Home');
|
||||
await tester.tap(find.text('0:50'));
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -1535,7 +1668,7 @@ void main() {
|
|||
await withClock(Clock(() => at), () async {
|
||||
await startRecording(tester, routes: routes);
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Running'));
|
||||
await tester.pump();
|
||||
|
|
@ -1608,7 +1741,7 @@ void main() {
|
|||
];
|
||||
await withClock(Clock.fixed(at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No strap'), findsOneWidget);
|
||||
|
||||
|
|
@ -1638,7 +1771,7 @@ void main() {
|
|||
straps().save(KnownDevice(address: polar.address, name: polar.name));
|
||||
await withClock(Clock.fixed(at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(
|
||||
|
|
@ -1660,7 +1793,7 @@ void main() {
|
|||
straps().save(KnownDevice(address: polar.address, name: polar.name));
|
||||
await withClock(Clock.fixed(at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Heart rate'));
|
||||
await pass(tester, 2);
|
||||
|
|
@ -1675,7 +1808,7 @@ void main() {
|
|||
testWidgets('refused nearby devices, the sheet says why', (tester) async {
|
||||
platform.allows = {RecorderPermission.location};
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Heart rate'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -1695,7 +1828,7 @@ void main() {
|
|||
var now = at;
|
||||
await withClock(Clock(() => now), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Start'));
|
||||
await pass(tester, 2);
|
||||
|
|
@ -1738,7 +1871,7 @@ void main() {
|
|||
);
|
||||
await withClock(Clock.fixed(at), () async {
|
||||
await startRecording(tester, loop: loop);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('From the Polar Loop'), findsOneWidget);
|
||||
|
||||
|
|
@ -1767,7 +1900,7 @@ void main() {
|
|||
platform.nearbyRowers = [rower];
|
||||
await withClock(Clock.fixed(at), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Rowing indoors'));
|
||||
await tester.tap(find.text('Rowing indoors'));
|
||||
|
|
@ -1806,7 +1939,7 @@ void main() {
|
|||
var now = at;
|
||||
await withClock(Clock(() => now), () async {
|
||||
await startRecording(tester);
|
||||
await tester.tap(find.text('Record'));
|
||||
await tester.tap(recordButton());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Rowing indoors'));
|
||||
await tester.tap(find.text('Rowing indoors'));
|
||||
|
|
@ -1904,8 +2037,7 @@ void main() {
|
|||
) async {
|
||||
await start(tester, usedBefore: true, days: days);
|
||||
|
||||
await tester.tap(find.text('Days'));
|
||||
await tester.pumpAndSettle();
|
||||
await goTab(tester, 'Profile');
|
||||
|
||||
expect(find.text('No days yet'), findsOneWidget);
|
||||
});
|
||||
|
|
@ -1915,8 +2047,7 @@ void main() {
|
|||
await keepTuesday(tester);
|
||||
await start(tester, usedBefore: true, days: days);
|
||||
|
||||
await tester.tap(find.text('Days'));
|
||||
await tester.pumpAndSettle();
|
||||
await goTab(tester, 'Profile');
|
||||
|
||||
expect(find.text('Tuesday, October 27'), findsOneWidget);
|
||||
expect(find.text('9876 steps'), findsOneWidget);
|
||||
|
|
@ -1927,6 +2058,8 @@ void main() {
|
|||
await tester.tap(find.text('Tuesday, October 27'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Inside Profile, the tabs beside it (D60).
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
expect(find.text('Heart rate'), findsOneWidget);
|
||||
expect(find.text('The night'), findsOneWidget);
|
||||
expect(find.text('You slept well'), findsOneWidget);
|
||||
|
|
@ -1958,6 +2091,8 @@ void main() {
|
|||
|
||||
Future<void> openPlans(WidgetTester tester) async {
|
||||
await start(tester, usedBefore: true, plans: plans);
|
||||
// Without a Record tab, the plans are beside the days in Profile.
|
||||
await goTab(tester, 'Profile');
|
||||
await tester.tap(find.text('Plans'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
|
@ -2076,7 +2211,7 @@ void main() {
|
|||
folder.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
/// Home's *Routes*, on a phone at [at] (null: a device that can't tell).
|
||||
/// Maps, on a phone at [at] (null: a device that can't tell).
|
||||
Future<void> openRoutes(
|
||||
WidgetTester tester, {
|
||||
({double lat, double lon})? at,
|
||||
|
|
@ -2106,8 +2241,7 @@ void main() {
|
|||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('Routes'));
|
||||
await tester.pumpAndSettle();
|
||||
await goTab(tester, 'Maps');
|
||||
}
|
||||
|
||||
/// Taps the map [offset] from its centre.
|
||||
|
|
|
|||
Loading…
Reference in a new issue