Full UI redesign pass complete: persistent tab shell with an always-visible background map, Modern Professional Dark theme, monochrome dark map tiles, offline skeleton map, a shared GlassPanel/FloatingPill component kit, customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning / Rides History screen rebuilds. 374 tests passing, up from 316. The APK is a fresh release build (debug-signed, no release signing config exists yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
185 lines
6.3 KiB
Dart
185 lines
6.3 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/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');
|
|
});
|
|
});
|
|
}
|