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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user