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 // 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'); }); }); }