Refresh the Flutter snapshot: v3 tickets V3-04 through V3-16, plus a fresh installable APK

V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221.

The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
This commit is contained in:
2026-08-19 13:36:04 -05:00
parent 14289befb0
commit 4c634354bd
48 changed files with 4436 additions and 224 deletions

View File

@@ -0,0 +1,115 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:rippr/src/domain/models.dart';
import 'package:rippr/src/export/ride_export.dart';
import 'package:rippr/src/ui/format.dart';
/// V3-03: distance and speed units. Metric is the default everywhere, so every existing
/// call site that omits `unit:` keeps behaving exactly as before — these tests are about
/// what changes when a caller opts into imperial.
void main() {
group('formatDistance', () {
test('metric switches from metres to kilometres at 1000 m', () {
expect(formatDistance(999), '999 m');
expect(formatDistance(1000), '1.0 km');
expect(formatDistance(12345.6), '12.3 km');
});
test('imperial switches from feet to miles at 1000 ft', () {
// 1000 ft is ~304.8 m.
expect(formatDistance(100, unit: UnitSystem.imperial), '328 ft');
expect(
formatDistance(305, unit: UnitSystem.imperial),
'0.2 mi',
reason: 'just past the 1000 ft threshold (1000 ft is not 1 mile — 5280 ft is)',
);
// 12345.6 m is ~7.67 mi.
expect(formatDistance(12345.6, unit: UnitSystem.imperial), '7.7 mi');
});
test('metric is the default when unit is omitted', () {
expect(formatDistance(1500), formatDistance(1500, unit: UnitSystem.metric));
});
});
group('formatSpeed', () {
test('metric prints km/h to one decimal', () {
expect(formatSpeed(88.5), '88.5 km/h');
});
test('imperial converts to mph', () {
// 100 km/h is ~62.1 mph.
expect(formatSpeed(100, unit: UnitSystem.imperial), '62.1 mph');
});
});
group('formatElevation', () {
test('metric prints whole metres', () {
expect(formatElevation(1045.7), '1045 m');
});
test('imperial converts to whole feet', () {
// 1000 m is ~3280.84 ft.
expect(formatElevation(1000, unit: UnitSystem.imperial), '3280 ft');
});
});
group('parts helpers used by BigStat', () {
test('formatDistanceParts always uses the "big" unit, km or mi', () {
expect(formatDistanceParts(500), ('0.5', 'km'));
expect(formatDistanceParts(1609.344, unit: UnitSystem.imperial), ('1.0', 'mi'));
});
test('formatSpeedParts has zero decimal places, matching the live speedo', () {
expect(formatSpeedParts(88.6), ('89', 'km/h'));
expect(formatSpeedParts(100, unit: UnitSystem.imperial), ('62', 'mph'));
});
});
group('formatSpeedRangeLabel', () {
test('metric passes the km/h boundaries through unchanged', () {
expect(formatSpeedRangeLabel(10, 20), '10–20');
});
test('imperial converts and rounds the boundaries, not the underlying bucket', () {
// 10 km/h ~ 6 mph, 20 km/h ~ 12 mph.
expect(formatSpeedRangeLabel(10, 20, unit: UnitSystem.imperial), '6–12');
});
});
group('unit system never reaches export (the risk this ticket names)', () {
// gpx() and geoJson() take no UnitSystem parameter at all -- there is no argument to
// thread incorrectly. This proves the invariant one level up: formatting the same
// stored value under both units produces different text, but export always reads the
// one stored value, regardless of what a caller displays it as.
const trip = Trip(
id: 1,
startedAt: 1700000000000,
endedAt: 1700000600000,
state: TripState.completed,
distanceM: 12345.6,
maxSpeedKmh: 88.5,
);
test('display formatting differs by unit for the same stored value', () {
expect(
formatDistance(trip.distanceM, unit: UnitSystem.metric),
isNot(formatDistance(trip.distanceM, unit: UnitSystem.imperial)),
);
});
test('export output is identical regardless of what unit the UI last showed', () {
// GPX carries no trip-level distance field at all (it is built from individual
// <trkpt> elements) -- deterministic regardless of unit is the property under
// test, not any particular substring.
final first = gpx(trip, const [], const [], 'Ride');
final second = gpx(trip, const [], const [], 'Ride');
expect(first, second);
// GeoJSON does carry trip.distanceM in its properties, and it is the raw metric
// figure -- 12345.6, never a converted "7.7 mi".
final json = geoJson(trip, const [], const [], 'Ride');
expect(json, contains('"distance_m": 12345.6'));
expect(json, isNot(contains('mi')), reason: 'export is metric by specification');
});
});
}