A pointer down/up Listener around the FlutterMap now tracks whether a real gesture is in progress, and didUpdateWidget's follow-recenter callbacks skip the camera move while one is -- closing the timing window where a once-per- second ambient GPS tick's postFrameCallback could land after a finger touched the map but before flutter_map reported hasGesture: true, snapping the camera back out from under an in-progress pan.
632 lines
22 KiB
Dart
632 lines
22 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:latlong2/latlong.dart' as ll;
|
|
import 'package:rippr/src/domain/models.dart';
|
|
import 'package:rippr/src/ui/components/ride_map.dart';
|
|
import 'package:rippr/src/ui/components/skeleton_map_layer.dart';
|
|
import 'package:rippr/src/ui/theme.dart';
|
|
|
|
/// Tiles are never fetched here — a widget test cannot serve them — but the polyline
|
|
/// geometry, which is where every real map bug in this project has lived, is fully
|
|
/// exercisable.
|
|
void main() {
|
|
TrackPoint p(int segmentId, int i, {double speed = 40, double lat = 51.0}) =>
|
|
TrackPoint(
|
|
id: i + 1,
|
|
tripId: 1,
|
|
segmentId: segmentId,
|
|
timestamp: 1000 + i * 1000,
|
|
latitude: lat + i * 0.0005,
|
|
longitude: -114.0,
|
|
speedKmh: speed,
|
|
altitudeM: 1000,
|
|
);
|
|
|
|
Future<List<Polyline>> render(
|
|
WidgetTester tester, {
|
|
required List<TrackPoint> points,
|
|
required List<Segment> segments,
|
|
}) async {
|
|
await tester.pumpWidget(MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: Scaffold(body: RideMap(points: points, segments: segments)),
|
|
));
|
|
await tester.pump();
|
|
final layers = tester
|
|
.widgetList<PolylineLayer>(find.byType(PolylineLayer))
|
|
.toList();
|
|
return layers.isEmpty ? const [] : layers.first.polylines.cast<Polyline>();
|
|
}
|
|
|
|
testWidgets('an empty ride says so instead of drawing a blank map',
|
|
(tester) async {
|
|
await tester.pumpWidget(MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: const Scaffold(
|
|
body: RideMap(points: [], segments: []),
|
|
),
|
|
));
|
|
await tester.pump();
|
|
expect(find.text('No path recorded'), findsOneWidget);
|
|
expect(find.byType(FlutterMap), findsNothing);
|
|
});
|
|
|
|
testWidgets('a pause leaves a gap: segments are never joined', (tester) async {
|
|
// Two segments a degree apart. If they were joined, one polyline would span ~111 km.
|
|
final points = [
|
|
for (var i = 0; i < 6; i++) p(1, i, lat: 51.0),
|
|
for (var i = 0; i < 6; i++) p(2, i, lat: 52.0),
|
|
];
|
|
const segments = [
|
|
Segment(id: 1, tripId: 1, startedAt: 0, endedAt: 1),
|
|
Segment(id: 2, tripId: 1, startedAt: 2, endedAt: 3),
|
|
];
|
|
|
|
final polylines = await render(tester, points: points, segments: segments);
|
|
|
|
expect(polylines, isNotEmpty);
|
|
for (final line in polylines) {
|
|
final lats = line.points.map((c) => c.latitude);
|
|
// No single polyline may straddle the gap between the two rides.
|
|
expect(lats.every((l) => l < 51.5) || lats.every((l) => l > 51.5), isTrue,
|
|
reason: 'a polyline spanned the pause');
|
|
}
|
|
});
|
|
|
|
testWidgets('decimation reduces rendered vertices but keeps the shape',
|
|
(tester) async {
|
|
// A dense straight run: Douglas-Peucker should collapse it hard.
|
|
final points = [for (var i = 0; i < 400; i++) p(1, i)];
|
|
const segments = [Segment(id: 1, tripId: 1, startedAt: 0, endedAt: 1)];
|
|
|
|
final polylines = await render(tester, points: points, segments: segments);
|
|
|
|
final rendered =
|
|
polylines.fold<int>(0, (a, line) => a + line.points.length);
|
|
expect(rendered, lessThan(points.length),
|
|
reason: 'render-only decimation should reduce vertex count');
|
|
expect(rendered, greaterThan(1));
|
|
});
|
|
|
|
testWidgets('a stationary ride still renders without zooming to infinity',
|
|
(tester) async {
|
|
// Every point at one spot — degenerate bounds. Fitting these would zoom past the
|
|
// tile server's maximum and render an empty grid, the v2.0 short-ride bug.
|
|
final points = [
|
|
for (var i = 0; i < 5; i++)
|
|
TrackPoint(
|
|
id: i + 1,
|
|
tripId: 1,
|
|
segmentId: 1,
|
|
timestamp: 1000 + i * 1000,
|
|
latitude: 51.0,
|
|
longitude: -114.0,
|
|
speedKmh: 0,
|
|
altitudeM: 1000,
|
|
),
|
|
];
|
|
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.initialZoom, shortRideZoom);
|
|
expect(map.options.maxZoom, maxTileZoom,
|
|
reason: 'exceeding OSM max tile zoom renders an empty grid');
|
|
});
|
|
|
|
group('skeleton mode (UI-02)', () {
|
|
testWidgets('shows the skeleton and no TileLayer when skeletonMode is true',
|
|
(tester) async {
|
|
await tester.pumpWidget(MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: const Scaffold(
|
|
body: RideMap(points: [], segments: [], showEmptyLabel: false, skeletonMode: true),
|
|
),
|
|
));
|
|
await tester.pump();
|
|
|
|
expect(find.byType(SkeletonMapLayer), findsOneWidget);
|
|
expect(find.byType(TileLayer), findsNothing);
|
|
});
|
|
|
|
testWidgets('shows the TileLayer and no skeleton when skeletonMode is false',
|
|
(tester) async {
|
|
await tester.pumpWidget(MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: const Scaffold(
|
|
body: RideMap(points: [], segments: [], showEmptyLabel: false),
|
|
),
|
|
));
|
|
await tester.pump();
|
|
|
|
expect(find.byType(TileLayer), findsOneWidget);
|
|
expect(find.byType(SkeletonMapLayer), findsNothing);
|
|
});
|
|
|
|
testWidgets('the recorded path still renders on top of the skeleton',
|
|
(tester) async {
|
|
final points = [
|
|
for (var i = 0; i < 4; i++)
|
|
TrackPoint(
|
|
id: i + 1,
|
|
tripId: 1,
|
|
segmentId: 1,
|
|
timestamp: 1000 + i * 1000,
|
|
latitude: 51.0 + i * 0.0005,
|
|
longitude: -114.0,
|
|
speedKmh: 40,
|
|
altitudeM: 1000,
|
|
),
|
|
];
|
|
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, skeletonMode: true),
|
|
),
|
|
));
|
|
await tester.pump();
|
|
|
|
expect(find.byType(SkeletonMapLayer), findsOneWidget);
|
|
expect(find.byType(TileLayer), findsNothing);
|
|
final layer = tester.widget<PolylineLayer>(find.byType(PolylineLayer));
|
|
expect(layer.polylines, isNotEmpty,
|
|
reason: 'the path comes from local data, not tiles -- it must not vanish '
|
|
'just because the tile fetch is failing');
|
|
});
|
|
});
|
|
|
|
group('ambient position (FB-01)', () {
|
|
testWidgets('with no recorded points, an ambient position centers the map at '
|
|
'street level', (tester) async {
|
|
const fix = ll.LatLng(51.05, -114.05);
|
|
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));
|
|
expect(map.options.initialCenter, fix);
|
|
expect(map.options.initialZoom, ambientZoom);
|
|
});
|
|
|
|
testWidgets('with neither recorded points nor an ambient fix, the map still '
|
|
'falls back to (0, 0) at zoom 2 (no regression, no crash)', (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.initialCenter, const ll.LatLng(0, 0));
|
|
expect(map.options.initialZoom, 2);
|
|
});
|
|
|
|
testWidgets('while following, a new ambient fix re-centers the map, exactly '
|
|
'like the recorded-path chase', (tester) async {
|
|
const fix1 = ll.LatLng(51.0, -114.0);
|
|
const fix2 = ll.LatLng(51.01, -114.01);
|
|
|
|
Widget build(ll.LatLng ambient) => MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: Scaffold(
|
|
body: RideMap(
|
|
points: const [],
|
|
segments: const [],
|
|
showEmptyLabel: false,
|
|
follow: true,
|
|
ambientPosition: ambient,
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(build(fix1));
|
|
await tester.pump();
|
|
|
|
await tester.pumpWidget(build(fix2));
|
|
await tester.pump();
|
|
|
|
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
|
|
final center = map.mapController!.camera.center;
|
|
expect(center.latitude, closeTo(fix2.latitude, 1e-9));
|
|
expect(center.longitude, closeTo(fix2.longitude, 1e-9));
|
|
});
|
|
|
|
testWidgets('the first ambient fix to arrive after construction corrects the '
|
|
'zoom, not just the center -- a plain re-center would otherwise leave the '
|
|
'map at the initial zoom-2 forever', (tester) async {
|
|
// Reproduces a real device bug found in emulator verification: the shell
|
|
// background map builds before any location permission/fix has resolved, so
|
|
// `ambientPosition` starts null and the initial camera locks at zoom 2 (the
|
|
// `bounds == null` fallback). The first fix to actually arrive must jump to
|
|
// `ambientZoom`, not just re-center at whatever zoom the map already happens
|
|
// to be sitting at.
|
|
const fix = ll.LatLng(51.0, -114.0);
|
|
|
|
Widget build(ll.LatLng? ambient) => MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: Scaffold(
|
|
body: RideMap(
|
|
points: const [],
|
|
segments: const [],
|
|
showEmptyLabel: false,
|
|
follow: true,
|
|
ambientPosition: ambient,
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(build(null));
|
|
await tester.pump();
|
|
|
|
var map = tester.widget<FlutterMap>(find.byType(FlutterMap));
|
|
expect(map.mapController!.camera.zoom, 2);
|
|
|
|
await tester.pumpWidget(build(fix));
|
|
await tester.pump();
|
|
|
|
map = tester.widget<FlutterMap>(find.byType(FlutterMap));
|
|
expect(map.mapController!.camera.zoom, ambientZoom);
|
|
expect(map.mapController!.camera.center.latitude, closeTo(fix.latitude, 1e-9));
|
|
expect(map.mapController!.camera.center.longitude, closeTo(fix.longitude, 1e-9));
|
|
});
|
|
|
|
testWidgets('a manual pan cancels ambient following the same way it cancels '
|
|
'recording-follow', (tester) async {
|
|
const fix1 = ll.LatLng(51.0, -114.0);
|
|
const fix2 = ll.LatLng(51.01, -114.01);
|
|
|
|
Widget build(ll.LatLng ambient) => MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: Scaffold(
|
|
body: RideMap(
|
|
points: const [],
|
|
segments: const [],
|
|
showEmptyLabel: false,
|
|
follow: true,
|
|
ambientPosition: ambient,
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(build(fix1));
|
|
await tester.pump();
|
|
|
|
// Simulate a real user gesture the same way flutter_map itself would report
|
|
// one to `onPositionChanged` -- calling the callback directly with
|
|
// `hasGesture: true` exercises the exact guard in `_RideMapState` without
|
|
// needing a real pointer gesture to get past `InteractionOptions`.
|
|
var map = tester.widget<FlutterMap>(find.byType(FlutterMap));
|
|
map.options.onPositionChanged!(map.mapController!.camera, true);
|
|
await tester.pump();
|
|
|
|
await tester.pumpWidget(build(fix2));
|
|
await tester.pump();
|
|
|
|
map = tester.widget<FlutterMap>(find.byType(FlutterMap));
|
|
final center = map.mapController!.camera.center;
|
|
expect(center.latitude, closeTo(fix1.latitude, 1e-9),
|
|
reason: 'following was cancelled by the manual pan; a later ambient fix '
|
|
'must not move the camera');
|
|
expect(center.longitude, closeTo(fix1.longitude, 1e-9));
|
|
});
|
|
|
|
testWidgets('the pulsing location marker falls back to the ambient position '
|
|
'when there are no recorded points', (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,
|
|
showLocationMarker: true,
|
|
ambientPosition: fix,
|
|
),
|
|
),
|
|
));
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(const Key('location-marker')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('no marker is shown when there is neither a recorded point nor an '
|
|
'ambient position', (tester) async {
|
|
await tester.pumpWidget(MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: const Scaffold(
|
|
body: RideMap(
|
|
points: [],
|
|
segments: [],
|
|
showEmptyLabel: false,
|
|
showLocationMarker: true,
|
|
),
|
|
),
|
|
));
|
|
await tester.pump();
|
|
|
|
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(
|
|
'a real drag beats an ambient tick that lands mid-drag (FB-10)',
|
|
(tester) async {
|
|
const fix1 = ll.LatLng(51.0, -114.0);
|
|
// The ambient tick that lands while the finger is down but before
|
|
// flutter_map's own gesture recognizer has reported `hasGesture: true` --
|
|
// this is the exact race the ticket describes.
|
|
const fix2 = ll.LatLng(51.5, -114.5);
|
|
|
|
Widget build(ll.LatLng ambient) => MaterialApp(
|
|
theme: ripprTheme(),
|
|
home: Scaffold(
|
|
body: RideMap(
|
|
points: const [],
|
|
segments: const [],
|
|
showEmptyLabel: false,
|
|
follow: true,
|
|
ambientPosition: ambient,
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(build(fix1));
|
|
await tester.pump();
|
|
|
|
// Put a real pointer down on the map -- this is what a rider's finger
|
|
// touching the screen looks like, well before flutter_map decides the
|
|
// movement counts as a drag.
|
|
final gesture =
|
|
await tester.startGesture(tester.getCenter(find.byType(FlutterMap)));
|
|
addTearDown(() => gesture.removePointer());
|
|
|
|
// Simulate a once-per-second ambient GPS tick landing while the pointer
|
|
// is already down but before it has moved -- exactly the race window
|
|
// the ticket describes: flutter_map has not yet reported `hasGesture:
|
|
// true`, so nothing has flipped `_following` off yet.
|
|
await tester.pumpWidget(build(fix2));
|
|
await tester.pump();
|
|
|
|
// The critical assertion: with the pointer still down and untouched by
|
|
// any real drag, the camera must not have snapped to the ambient tick's
|
|
// position. Without the fix, the race lets the scheduled
|
|
// `postFrameCallback` win here and the camera jumps to `fix2` before
|
|
// the rider's finger has moved at all.
|
|
var map = tester.widget<FlutterMap>(find.byType(FlutterMap));
|
|
var center = map.mapController!.camera.center;
|
|
expect(
|
|
center.latitude,
|
|
isNot(closeTo(fix2.latitude, 1e-6)),
|
|
reason: 'the mid-drag ambient tick must not win the race and snap the '
|
|
'camera back to the ambient position while the gesture is still '
|
|
'down',
|
|
);
|
|
expect(center.longitude, isNot(closeTo(fix2.longitude, 1e-6)));
|
|
|
|
// Now the finger actually moves and lifts -- the real drag completes
|
|
// normally, and the final position reflects it, not the ambient tick.
|
|
await gesture.moveBy(const Offset(-100, -100));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pump();
|
|
|
|
map = tester.widget<FlutterMap>(find.byType(FlutterMap));
|
|
center = map.mapController!.camera.center;
|
|
expect(
|
|
center.latitude,
|
|
isNot(closeTo(fix2.latitude, 1e-6)),
|
|
reason: 'the completed drag must still reflect the rider\'s own pan, '
|
|
'not the ambient position the mid-drag tick tried to recenter to',
|
|
);
|
|
expect(center.longitude, isNot(closeTo(fix2.longitude, 1e-6)));
|
|
});
|
|
|
|
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));
|
|
});
|
|
});
|
|
}
|