UI-01: persistent 4-tab shell with a shared background map
Replaces the push/pop stack rooted at Record with a StatefulShellRoute (Map/Rides/Plan/Settings), each tab keeping its own navigator so Trip Detail and the route planner push within their own branch. AppShell/ShellScaffold host one shared RideMap instance behind every tab -- full opacity and interactive on Map, dimmed and non-interactive elsewhere -- so the camera position and live path survive a tab switch instead of being refetched. Every tab's AppBar/header is removed per the redesign's no-chrome mandate. Fixes a bug this surfaced: RideMap rendered a structurally different tree for empty vs. non-empty points, which crashed once the map became a long-lived shell background instead of a fresh per-screen widget. Unified the background-map tree shape so it no longer remounts mid-recording. Verified on Medium_Phone_API_35: all four tabs, live recording surviving tab switches with the camera preserved, and the real HOME-key lifecycle tile-drop from V3-04 still firing correctly in the shell context. 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,7 @@ 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/detail/trip_detail_screen.dart';
|
||||
import 'package:rippr/src/ui/format.dart';
|
||||
import 'package:rippr/src/ui/record/record_screen.dart';
|
||||
@@ -203,58 +204,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 +247,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 +263,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 +274,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');
|
||||
});
|
||||
@@ -364,6 +351,41 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
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', () {
|
||||
screenTest('empty state explains what to do', (tester) async {
|
||||
await tester.pumpWidget(host(const TripsScreen()));
|
||||
|
||||
Reference in New Issue
Block a user