The port runs on Android and iOS and is feature-complete; the native Android app is superseded but kept, since it is still the only version that has recorded real rides. rippr-flutter-1.0-debug.apk is package com.rippr.port, deliberately different from the native com.rippr so both install side by side. Recording the same ride on both at once is the strongest available check that the port is faithful. Added INSTALL.md covering both platforms. Android is a one-line adb install; iOS has no APK equivalent and must be built and signed through Xcode with a free Apple ID, which gives a 7-day profile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
4.2 KiB
Dart
121 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:rippr/src/domain/models.dart';
|
|
import 'package:rippr/src/ui/components/ride_map.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');
|
|
});
|
|
}
|