V3-04: live map on the recording screen
Feeds RideMap from watchPointsForTrip/watchSegmentsForTrip via autoDispose.family providers. RecordingEngine stays map-free (enforced by architecture_test.dart). RideMap gained lifecycle-aware tile teardown and follow-mode, both shared with the trip-detail map.
This commit is contained in:
15
test/architecture_test.dart
Normal file
15
test/architecture_test.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// A structural invariant from V3-04, checked directly rather than trusted: rendering
|
||||
/// belongs to a visible screen's widget lifecycle, never to the recording engine. If this
|
||||
/// ever starts failing, a map reference has leaked into code that keeps running while the
|
||||
/// phone is pocketed and the screen is off.
|
||||
void main() {
|
||||
test('RecordingEngine never imports the map', () {
|
||||
final source = File('lib/src/recording/recording_engine.dart').readAsStringSync();
|
||||
expect(source.contains('ride_map'), isFalse);
|
||||
expect(source.contains('flutter_map'), isFalse);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:drift/drift.dart' show driftRuntimeOptions;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/app/providers.dart';
|
||||
@@ -137,7 +139,7 @@ void main() {
|
||||
screenTest('recording swaps to PAUSE and STOP, with no DISCARD',
|
||||
(tester) async {
|
||||
await repo.startTrip(1000);
|
||||
await pumpLive(tester, host(const RecordScreen()));
|
||||
await pumpLive(tester, host(const RecordScreen(), map: false));
|
||||
|
||||
expect(find.byKey(const Key('pause')), findsOneWidget);
|
||||
expect(find.byKey(const Key('stop')), findsOneWidget);
|
||||
@@ -150,7 +152,7 @@ void main() {
|
||||
screenTest('paused offers RESUME, STOP and DISCARD', (tester) async {
|
||||
await repo.startTrip(1000);
|
||||
await repo.pauseTrip(2000);
|
||||
await pumpLive(tester, host(const RecordScreen()));
|
||||
await pumpLive(tester, host(const RecordScreen(), map: false));
|
||||
|
||||
expect(find.byKey(const Key('resume')), findsOneWidget);
|
||||
expect(find.byKey(const Key('stop')), findsOneWidget);
|
||||
@@ -161,7 +163,7 @@ void main() {
|
||||
screenTest('discard asks before destroying anything', (tester) async {
|
||||
await repo.startTrip(1000);
|
||||
await repo.pauseTrip(2000);
|
||||
await pumpLive(tester, host(const RecordScreen()));
|
||||
await pumpLive(tester, host(const RecordScreen(), map: false));
|
||||
|
||||
await tester.tap(find.byKey(const Key('discard')));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -208,6 +210,66 @@ void main() {
|
||||
expect(opened, isTrue,
|
||||
reason: 'the button must actually invoke the callback that navigates');
|
||||
});
|
||||
|
||||
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.
|
||||
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);
|
||||
expect(find.byType(FlutterMap), findsNothing);
|
||||
|
||||
// Recording, 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);
|
||||
});
|
||||
|
||||
screenTest('backgrounding the app drops the tile layer (V3-04)',
|
||||
(tester) async {
|
||||
final h = await repo.startTrip(1000);
|
||||
await repo.appendPoints([
|
||||
TrackPoint(
|
||||
tripId: h.tripId,
|
||||
segmentId: h.segmentId,
|
||||
timestamp: 1000,
|
||||
latitude: 51.0,
|
||||
longitude: -114.0,
|
||||
speedKmh: 20.0,
|
||||
altitudeM: 1000.0,
|
||||
),
|
||||
]);
|
||||
await pumpLive(tester, host(const RecordScreen()));
|
||||
|
||||
expect(find.byType(TileLayer), findsOneWidget,
|
||||
reason: 'foregrounded: tiles render normally');
|
||||
|
||||
// Simulates the platform lifecycle message a real backgrounding sends -- this is
|
||||
// the standard way to drive AppLifecycleState changes in a widget test.
|
||||
final message = const StringCodec().encodeMessage('AppLifecycleState.paused');
|
||||
await tester.binding.defaultBinaryMessenger
|
||||
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(TileLayer), findsNothing,
|
||||
reason: 'backgrounded: no tile layer means no tile request can fire');
|
||||
expect(find.byType(PolylineLayer), findsOneWidget,
|
||||
reason: 'the drawn path itself is not removed, only tile fetching');
|
||||
|
||||
final resumed = const StringCodec().encodeMessage('AppLifecycleState.resumed');
|
||||
await tester.binding.defaultBinaryMessenger
|
||||
.handlePlatformMessage('flutter/lifecycle', resumed, (_) {});
|
||||
await tester.pump();
|
||||
expect(find.byType(TileLayer), findsOneWidget,
|
||||
reason: 'foregrounding again must resume tiles');
|
||||
});
|
||||
});
|
||||
|
||||
group('trips list', () {
|
||||
|
||||
Reference in New Issue
Block a user