UI-05: rebuild Record screen as the Map HUD (design north star)
Replaces the scrolling stats-card layout with the full-bleed Map HUD: UI-04's draggable/resizable HUD widgets float over UI-01's shared background map (full opacity on this tab), and the old Pause/Stop/Resume/ Discard buttons become full-bleed, icon-only segments matching the mockup's tertiary-container/error-container colors exactly. The idle state keeps its prior compact layout deliberately -- HUD widgets only appear once a ride exists, preserving the existing "a resting screen must not look like a ride going nowhere" guarantee rather than reinterpreting it. State-machine logic (ticker, wakelock, speed subscription, error handling) is untouched; every pre-existing record-screen test passed unchanged against the rebuilt screen. Added tests that actually tap Start/Pause/Stop and verify engine state changes, confirm HUD widgets render over the map rather than replacing it, and verify the mounted-mode speed digit's real contrast ratio against GlassPanel's translucent surface specifically (the ticket's own named risk). Corrects a UI-04 mistake found while implementing this ticket: the HUD's default four metrics were ordered Speed/Distance/Elapsed/Max Speed, a guess made before reading the actual Map HUD mockup HTML closely. The real fixed row is Speed/Avg Speed/Dist/Time -- reordered HudMetric to match and updated every test that asserted the old order. Adds PulsingLocationMarker (UI-03) to RideMap's live usage via a new showLocationMarker flag, and explicit tertiaryContainer/errorContainer tokens to ripprColors so the control bar matches the design system's literal values rather than an auto-derived tonal palette. Verified end-to-end on a real emulator: Start, Pause, Resume, and Stop all correctly drive the trip state machine with the full live map behind everything. A lengthy false alarm during this verification (taps appearing to do nothing) turned out to be a screenshot-scale mis-measurement on the verification side, not an app defect -- resolved by sampling pixel colors directly from the raw screenshot to find the control bar's true on-screen position. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
@@ -108,6 +108,6 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Speed'), findsOneWidget, reason: 'visible by default');
|
||||
expect(find.text('Average speed'), findsNothing, reason: 'hidden by default');
|
||||
expect(find.text('Max speed'), findsNothing, reason: 'hidden by default');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,15 +51,16 @@ void main() {
|
||||
reason: 'no two metrics should default to overlapping positions');
|
||||
});
|
||||
|
||||
test('the first four metrics (the mockup\'s fixed row) start visible', () {
|
||||
test('the first four metrics (the Map HUD mockup\'s fixed row: Speed/Avg Speed/'
|
||||
'Dist/Time) start visible', () {
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.speed).visible, isTrue);
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.avgSpeed).visible, isTrue);
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.distance).visible, isTrue);
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.elapsedTime).visible, isTrue);
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.maxSpeed).visible, isTrue);
|
||||
});
|
||||
|
||||
test('metrics beyond the fixed row start hidden', () {
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.avgSpeed).visible, isFalse);
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.maxSpeed).visible, isFalse);
|
||||
expect(HudWidgetLayout.defaultFor(HudMetric.elevationGain).visible, isFalse);
|
||||
});
|
||||
|
||||
|
||||
@@ -210,17 +210,17 @@ void main() {
|
||||
testWidgets('a metric switch reflects and writes through to Config', (tester) async {
|
||||
await tester.pumpWidget(host());
|
||||
await tester.pumpAndSettle();
|
||||
await scrollTo(tester, const Key('hud-visible-avgSpeed'));
|
||||
await scrollTo(tester, const Key('hud-visible-maxSpeed'));
|
||||
|
||||
final initial = tester
|
||||
.widget<SwitchListTile>(find.byKey(const Key('hud-visible-avgSpeed')))
|
||||
.widget<SwitchListTile>(find.byKey(const Key('hud-visible-maxSpeed')))
|
||||
.value;
|
||||
expect(initial, isFalse, reason: 'metrics beyond the fixed row start hidden');
|
||||
|
||||
await tester.tap(find.byKey(const Key('hud-visible-avgSpeed')));
|
||||
await tester.tap(find.byKey(const Key('hud-visible-maxSpeed')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(config.hudLayout[HudMetric.avgSpeed]!.visible, isTrue,
|
||||
expect(config.hudLayout[HudMetric.maxSpeed]!.visible, isTrue,
|
||||
reason: 'the switch must write through to Config immediately, not wait '
|
||||
'for a HUD edit-mode session to end');
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ 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/detail/trip_detail_screen.dart';
|
||||
import 'package:rippr/src/ui/format.dart';
|
||||
import 'package:rippr/src/ui/record/record_screen.dart';
|
||||
@@ -185,6 +186,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
|
||||
@@ -349,6 +395,34 @@ 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', () {
|
||||
|
||||
Reference in New Issue
Block a user