Refresh the Flutter snapshot: UI-01 through UI-09, plus a fresh installable APK

Full UI redesign pass complete: persistent tab shell with an always-visible
background map, Modern Professional Dark theme, monochrome dark map tiles,
offline skeleton map, a shared GlassPanel/FloatingPill component kit,
customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning /
Rides History screen rebuilds. 374 tests passing, up from 316.

The APK is a fresh release build (debug-signed, no release signing config
exists yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
2026-08-24 14:41:35 -05:00
parent 4c634354bd
commit 587f75eab4
42 changed files with 4401 additions and 89 deletions

View File

@@ -12,6 +12,9 @@ import 'package:rippr/src/domain/models.dart';
import 'package:rippr/src/ui/activity_display.dart';
import 'package:rippr/src/recording/location_source.dart';
import 'package:rippr/src/recording/wakelock_controller.dart';
import 'package:rippr/src/ui/app_shell.dart';
import 'package:rippr/src/ui/components/glass_panel.dart';
import 'package:rippr/src/ui/components/ride_map.dart';
import 'package:rippr/src/ui/detail/trip_detail_screen.dart';
import 'package:rippr/src/ui/format.dart';
import 'package:rippr/src/ui/record/record_screen.dart';
@@ -184,6 +187,51 @@ void main() {
reason: 'cancelling must not delete the ride');
});
screenTest('tapping START actually starts a trip (UI-05)', (tester) async {
await tester.pumpWidget(host(const RecordScreen(), map: false));
await tester.pumpAndSettle();
expect(await repo.activeTrip(), isNull);
await tester.tap(find.byKey(const Key('start')));
await tester.pump();
expect(await repo.activeTrip(), isNotNull,
reason: 'the new icon-only control bar must still drive the same engine '
'call the old text button did');
});
screenTest('tapping PAUSE and then STOP actually pauses and completes the trip '
'(UI-05)', (tester) async {
await repo.startTrip(1000);
await pumpLive(tester, host(const RecordScreen(), map: false));
await tester.tap(find.byKey(const Key('pause')));
await tester.pump();
final paused = await repo.activeTrip();
expect(paused?.state, TripState.paused);
await tester.tap(find.byKey(const Key('stop')));
await tester.pump();
expect(await repo.activeTrip(), isNull,
reason: 'stopping completes the trip -- it is no longer the active one');
});
screenTest('HUD widgets render over the map, not replacing it (UI-05)',
(tester) async {
await repo.startTrip(1000);
await pumpLive(
tester,
host(
ShellScaffold(currentIndex: 0, onDestinationSelected: (_) {}, child: const RecordScreen()),
),
);
expect(find.byKey(const Key('shell-background-map')), findsOneWidget,
reason: 'the map is still there, underneath the HUD');
expect(find.text('SPEED'), findsOneWidget);
expect(find.text('DISTANCE'), findsOneWidget);
});
screenTest('text is legible against the dark ground', (tester) async {
// The regression this exists for: removing Compose's Surface left LocalContentColor
// black, and a 64sp figure rendered invisibly on a near-black background. No logic
@@ -203,58 +251,37 @@ void main() {
expect(colour, isNot(Colors.black));
});
screenTest('a settings entry point exists and is wired (V3-02)', (tester) async {
var opened = false;
await tester.pumpWidget(
host(RecordScreen(onOpenSettings: () => opened = true)),
);
await tester.pumpAndSettle();
// UI-01: Settings/Routes/Rides entry points are no longer per-screen callback
// buttons on RecordScreen -- they're destinations on the shell's persistent bottom
// nav bar. See the `AppShell`/`ShellScaffold` group below for their coverage.
expect(find.byKey(const Key('open-settings')), findsOneWidget);
await tester.tap(find.byKey(const Key('open-settings')));
await tester.pumpAndSettle();
expect(opened, isTrue,
reason: 'the button must actually invoke the callback that navigates');
});
screenTest('a routes entry point exists and is wired (V3-07)', (tester) async {
var opened = false;
await tester.pumpWidget(
host(RecordScreen(onOpenRoutes: () => opened = true)),
);
await tester.pumpAndSettle();
expect(find.byKey(const Key('open-routes')), findsOneWidget);
await tester.tap(find.byKey(const Key('open-routes')));
await tester.pumpAndSettle();
expect(opened, isTrue);
});
screenTest('the live map appears only while recording and the toggle is on '
'(V3-04)', (tester) async {
// Idle, toggle on: no trip to draw, so no map at all.
await tester.pumpWidget(host(const RecordScreen()));
await tester.pumpAndSettle();
expect(find.byKey(const Key('live-map')), findsNothing);
// Recording, toggle off: RecordingEngine has produced a trip, but the map must
// not be constructed at all -- not just hidden.
screenTest('the background map appears only while the map toggle is on '
'(V3-04, moved to the shell by UI-01)', (tester) async {
// Toggle off: the shell must not construct the map at all -- not just hidden.
await repo.startTrip(1000);
await tester.pumpWidget(const SizedBox.shrink());
await pumpLive(tester, host(const RecordScreen(), map: false));
expect(find.byKey(const Key('live-map')), findsNothing);
await pumpLive(
tester,
host(
ShellScaffold(currentIndex: 0, onDestinationSelected: (_) {}, child: const RecordScreen()),
map: false,
),
);
expect(find.byKey(const Key('shell-background-map')), findsNothing);
expect(find.byType(FlutterMap), findsNothing);
// Recording, toggle on: the map is drawn.
// Toggle on: the map is drawn.
await tester.pumpWidget(const SizedBox.shrink());
await pumpLive(tester, host(const RecordScreen()));
expect(find.byKey(const Key('live-map')), findsOneWidget);
await pumpLive(
tester,
host(
ShellScaffold(currentIndex: 0, onDestinationSelected: (_) {}, child: const RecordScreen()),
),
);
expect(find.byKey(const Key('shell-background-map')), findsOneWidget);
});
screenTest('backgrounding the app drops the tile layer (V3-04)',
(tester) async {
screenTest('backgrounding the app drops the tile layer (V3-04, moved to the '
'shell by UI-01)', (tester) async {
final h = await repo.startTrip(1000);
await repo.appendPoints([
TrackPoint(
@@ -267,7 +294,12 @@ void main() {
altitudeM: 1000.0,
),
]);
await pumpLive(tester, host(const RecordScreen()));
await pumpLive(
tester,
host(
ShellScaffold(currentIndex: 0, onDestinationSelected: (_) {}, child: const RecordScreen()),
),
);
expect(find.byType(TileLayer), findsOneWidget,
reason: 'foregrounded: tiles render normally');
@@ -278,6 +310,7 @@ void main() {
await tester.binding.defaultBinaryMessenger
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
await tester.pump();
await tester.pump();
expect(find.byType(TileLayer), findsNothing,
reason: 'backgrounded: no tile layer means no tile request can fire');
@@ -288,6 +321,7 @@ void main() {
await tester.binding.defaultBinaryMessenger
.handlePlatformMessage('flutter/lifecycle', resumed, (_) {});
await tester.pump();
await tester.pump();
expect(find.byType(TileLayer), findsOneWidget,
reason: 'foregrounding again must resume tiles');
});
@@ -362,6 +396,69 @@ void main() {
expect(start.height, 96,
reason: 'V3-05: 72dp is not enough at speed, with gloves');
});
screenTest('the mounted-mode speed digit is legible against GlassPanel\'s own '
'translucent surface, not just different from the dark ground (UI-05)',
(tester) async {
// UI-05's own named risk: mounted mode was built and tested against an opaque
// Card, not a blurred, translucent GlassPanel -- a light theme through blurred
// content behaves differently than the dark-on-dark case V3-05 originally
// guarded against. This asserts the real contrast ratio, not just "differs from
// the wrong ground" the way the test above does.
await tester.pumpWidget(host(const RecordScreen(), mountedMode: true));
await tester.pumpAndSettle();
final speed = tester.widget<Text>(
find.descendant(of: find.byKey(const Key('speed')), matching: find.text('0')),
);
final panel = tester.widget<GlassPanel>(find.byType(GlassPanel).first);
final mountedColors = ripprMountedTheme().colorScheme;
// GlassPanel fills with `colors.surface` at `GlassPanel.fillOpacity` -- since it
// is a solid, near-opaque fill (not a transparency composited over unknown
// content), the panel's own surface colour is what the text is actually read
// against in practice.
expect(panel.child, isNotNull);
expect(
contrastRatio(speed.style!.color!, mountedColors.surface),
greaterThanOrEqualTo(4.5),
reason: 'AA normal text against GlassPanel\'s mounted-theme surface',
);
});
});
group('shell nav bar', () {
// UI-01: Settings/Routes/Rides are no longer per-screen callback buttons on
// RecordScreen -- they're destinations on the shell's persistent bottom nav bar.
// This replaces the old "a settings/routes entry point exists and is wired"
// per-screen tests.
screenTest('all four destinations are present and switching tabs calls back '
'with the tapped index', (tester) async {
var lastIndex = -1;
await tester.pumpWidget(host(
ShellScaffold(
currentIndex: 0,
onDestinationSelected: (i) => lastIndex = i,
child: const RecordScreen(),
),
map: false,
));
await tester.pumpAndSettle();
expect(find.byKey(const Key('shell-nav-bar')), findsOneWidget);
expect(find.byKey(const Key('nav-map')), findsOneWidget);
expect(find.byKey(const Key('nav-rides')), findsOneWidget);
expect(find.byKey(const Key('nav-plan')), findsOneWidget);
expect(find.byKey(const Key('nav-settings')), findsOneWidget);
await tester.tap(find.byKey(const Key('nav-settings')));
expect(lastIndex, 3);
await tester.tap(find.byKey(const Key('nav-rides')));
expect(lastIndex, 1);
await tester.tap(find.byKey(const Key('nav-plan')));
expect(lastIndex, 2);
});
});
group('trips list', () {
@@ -397,8 +494,86 @@ void main() {
expect(find.byKey(const Key('trip-1')), findsOneWidget);
expect(find.text('Finished'), findsOneWidget);
// Only the completed one.
expect(find.byType(Card), findsOneWidget);
// Only the completed one. UI-07: trip cards are GlassPanel-based, not Card --
// RideMap's thumbnail is unique to a trip card (unlike GlassPanel/Material, which
// other chrome on this screen also uses).
expect(find.byType(RideMap), findsOneWidget);
});
screenTest('search narrows the visible list to matching rides (UI-07)',
(tester) async {
await seedCompletedTrip(
startedAt: 1000, endedAt: 5000, name: 'Coast loop');
await seedCompletedTrip(
startedAt: 100000, endedAt: 200000, name: 'Mountain climb');
await tester.pumpWidget(host(const TripsScreen()));
await tester.pumpAndSettle();
expect(find.text('Coast loop'), findsOneWidget);
expect(find.text('Mountain climb'), findsOneWidget);
await tester.enterText(find.byKey(const Key('ride-search')), 'coast');
await tester.pump();
expect(find.text('Coast loop'), findsOneWidget);
expect(find.text('Mountain climb'), findsNothing);
});
screenTest('filtering by activity narrows the visible list (UI-07)',
(tester) async {
await repo.startTrip(1000, activity: Activity.bicycle).then(
(h) async {
await repo.appendPoints([
TrackPoint(
tripId: h.tripId,
segmentId: h.segmentId,
timestamp: 1000,
latitude: 51.0,
longitude: -114.0,
speedKmh: 20,
altitudeM: 1000,
),
]);
await repo.renameTrip(h.tripId, 'Bike ride');
await repo.completeTrip(5000);
await repo.recomputeAggregates(h.tripId);
});
await repo.startTrip(100000, activity: Activity.motorcycle).then(
(h) async {
await repo.appendPoints([
TrackPoint(
tripId: h.tripId,
segmentId: h.segmentId,
timestamp: 100000,
latitude: 51.0,
longitude: -114.0,
speedKmh: 60,
altitudeM: 1000,
),
]);
await repo.renameTrip(h.tripId, 'Moto ride');
await repo.completeTrip(200000);
await repo.recomputeAggregates(h.tripId);
});
await tester.pumpWidget(host(const TripsScreen()));
await tester.pumpAndSettle();
expect(find.text('Bike ride'), findsOneWidget);
expect(find.text('Moto ride'), findsOneWidget);
await tester.tap(find.byKey(const Key('activity-filter')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
await tester.pump();
await tester.tap(find.byKey(const Key('activity-filter-bicycle')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
await tester.pump();
expect(find.text('Bike ride'), findsOneWidget);
expect(find.text('Moto ride'), findsNothing);
});
screenTest('Merge enables at exactly two selections', (tester) async {