Merge FB-06: always-interactive map with recenter control

# Conflicts:
#	docs/feedback/FB-06-always-interactive-map-recenter.md
This commit is contained in:
2026-08-25 00:14:11 -05:00
3 changed files with 279 additions and 7 deletions

View File

@@ -367,4 +367,192 @@ void main() {
expect(find.byKey(const Key('location-marker')), findsNothing);
});
});
group('always-interactive map + recenter (FB-06)', () {
testWidgets('pan/zoom is allowed even with no recorded points (idle map)',
(tester) async {
await tester.pumpWidget(MaterialApp(
theme: ripprTheme(),
home: const Scaffold(
body: RideMap(points: [], segments: [], showEmptyLabel: false),
),
));
await tester.pump();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
expect(
map.options.interactionOptions.flags & InteractiveFlag.drag,
InteractiveFlag.drag,
);
expect(
map.options.interactionOptions.flags & InteractiveFlag.pinchZoom,
InteractiveFlag.pinchZoom,
);
});
testWidgets('pan/zoom is allowed while recording (has points)', (tester) async {
final points = [for (var i = 0; i < 4; i++) p(1, i)];
const segments = [Segment(id: 1, tripId: 1, startedAt: 0, endedAt: 1)];
await tester.pumpWidget(MaterialApp(
theme: ripprTheme(),
home: Scaffold(body: RideMap(points: points, segments: segments)),
));
await tester.pump();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
expect(
map.options.interactionOptions.flags & InteractiveFlag.drag,
InteractiveFlag.drag,
);
expect(
map.options.interactionOptions.flags & InteractiveFlag.pinchZoom,
InteractiveFlag.pinchZoom,
);
});
testWidgets('no recenter button before any manual pan', (tester) async {
const fix = ll.LatLng(51.0, -114.0);
await tester.pumpWidget(MaterialApp(
theme: ripprTheme(),
home: const Scaffold(
body: RideMap(
points: [],
segments: [],
showEmptyLabel: false,
follow: true,
ambientPosition: fix,
),
),
));
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsNothing);
});
testWidgets('no recenter button when follow is false, even after a pan',
(tester) async {
const fix = ll.LatLng(51.0, -114.0);
await tester.pumpWidget(MaterialApp(
theme: ripprTheme(),
home: const Scaffold(
body: RideMap(
points: [],
segments: [],
showEmptyLabel: false,
ambientPosition: fix,
),
),
));
await tester.pump();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
map.options.onPositionChanged?.call(map.mapController!.camera, true);
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsNothing);
});
testWidgets('a manual pan while following shows the recenter button',
(tester) async {
const fix = ll.LatLng(51.0, -114.0);
await tester.pumpWidget(MaterialApp(
theme: ripprTheme(),
home: const Scaffold(
body: RideMap(
points: [],
segments: [],
showEmptyLabel: false,
follow: true,
ambientPosition: fix,
),
),
));
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsNothing);
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
map.options.onPositionChanged!(map.mapController!.camera, true);
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsOneWidget);
});
testWidgets(
'tapping recenter moves the camera back to the ambient position and '
'hides the button again (following resumed)', (tester) async {
const fix1 = ll.LatLng(51.0, -114.0);
const fix2 = ll.LatLng(52.0, -115.0);
await tester.pumpWidget(MaterialApp(
theme: ripprTheme(),
home: const Scaffold(
body: RideMap(
points: [],
segments: [],
showEmptyLabel: false,
follow: true,
ambientPosition: fix1,
),
),
));
await tester.pump();
var map = tester.widget<FlutterMap>(find.byType(FlutterMap));
// Simulate the rider manually panning away, then a fresh ambient fix
// arriving while following is off (so the camera does not auto-chase it).
map.options.onPositionChanged!(map.mapController!.camera, true);
map.mapController!.move(fix2, map.mapController!.camera.zoom);
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsOneWidget);
await tester.tap(find.byKey(const Key('recenter-button')));
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsNothing);
map = tester.widget<FlutterMap>(find.byType(FlutterMap));
final center = map.mapController!.camera.center;
expect(center.latitude, closeTo(fix1.latitude, 1e-9),
reason: 'recenter should move back to the latest known ambient '
'position, not stay at the panned-to location');
expect(center.longitude, closeTo(fix1.longitude, 1e-9));
});
testWidgets('tapping recenter moves the camera to the latest recorded '
'point when recording', (tester) async {
final points = [for (var i = 0; i < 4; i++) p(1, i)];
const segments = [Segment(id: 1, tripId: 1, startedAt: 0, endedAt: 1)];
await tester.pumpWidget(MaterialApp(
theme: ripprTheme(),
home: Scaffold(
body: RideMap(
points: points,
segments: segments,
follow: true,
),
),
));
await tester.pump();
var map = tester.widget<FlutterMap>(find.byType(FlutterMap));
map.options.onPositionChanged!(map.mapController!.camera, true);
map.mapController!.move(const ll.LatLng(0, 0), map.mapController!.camera.zoom);
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsOneWidget);
await tester.tap(find.byKey(const Key('recenter-button')));
await tester.pump();
expect(find.byKey(const Key('recenter-button')), findsNothing);
map = tester.widget<FlutterMap>(find.byType(FlutterMap));
final last = points.last;
final center = map.mapController!.camera.center;
expect(center.latitude, closeTo(last.latitude, 1e-9));
expect(center.longitude, closeTo(last.longitude, 1e-9));
});
});
}