170 lines
6.1 KiB
Dart
170 lines
6.1 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:motorcycle_dash/database/database.dart' hide TelemetrySample;
|
|
import 'package:motorcycle_dash/features/performance/performance_timer_service.dart';
|
|
import 'package:motorcycle_dash/models/telemetry_sample.dart';
|
|
import 'dart:async';
|
|
import 'package:drift/drift.dart' as drift;
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('Phase 3 & 4: Speed & Conversion Validation', () {
|
|
test('m/s to MPH conversion factor is exact', () {
|
|
const double speedMps = 26.8224; // 60 MPH in m/s
|
|
final speedMph = speedMps * 2.23694;
|
|
expect((speedMph - 60.0).abs(), lessThan(0.01));
|
|
});
|
|
|
|
test('100 MPH speed conversion matches physical expectation', () {
|
|
const double speedMps = 44.704; // 100 MPH in m/s
|
|
final speedMph = speedMps * 2.23694;
|
|
expect((speedMph - 100.0).abs(), lessThan(0.01));
|
|
});
|
|
});
|
|
|
|
group('Phase 8 & 9: Database & Interrupted Ride Recovery', () {
|
|
late AppDatabase db;
|
|
|
|
setUp(() {
|
|
// Use in-memory SQLite database
|
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
|
});
|
|
|
|
tearDown(() async {
|
|
await db.close();
|
|
});
|
|
|
|
test('Unfinished interrupted ride is cleanly identified and recovered with non-null endTime', () async {
|
|
// Insert an unclosed ride (simulating an app kill during a ride)
|
|
final startTime = DateTime.now().subtract(const Duration(minutes: 45));
|
|
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
|
|
startTime: startTime,
|
|
distanceMiles: const drift.Value(24.5),
|
|
topSpeedMph: const drift.Value(78.0),
|
|
movingTime: const drift.Value(2400),
|
|
));
|
|
|
|
// Insert some telemetry samples for this ride
|
|
final lastSampleTime = startTime.add(const Duration(minutes: 40));
|
|
await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert(
|
|
rideId: rideId,
|
|
timestamp: lastSampleTime,
|
|
speed: const drift.Value(65.0),
|
|
latitude: const drift.Value(37.7749),
|
|
longitude: const drift.Value(-122.4194),
|
|
));
|
|
|
|
// Query unclosed rides
|
|
final unclosed = await (db.select(db.rides)..where((r) => r.endTime.isNull())).get();
|
|
expect(unclosed.length, equals(1));
|
|
expect(unclosed.first.id, equals(rideId));
|
|
|
|
// Simulate the recovery logic
|
|
final lastSample = await (db.select(db.telemetrySamples)
|
|
..where((t) => t.rideId.equals(unclosed.first.id))
|
|
..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp, mode: drift.OrderingMode.desc)])
|
|
..limit(1)
|
|
).getSingleOrNull();
|
|
|
|
final calculatedEndTime = lastSample?.timestamp ?? unclosed.first.startTime.add(Duration(seconds: unclosed.first.movingTime));
|
|
|
|
await (db.update(db.rides)..where((t) => t.id.equals(unclosed.first.id))).write(
|
|
RidesCompanion(endTime: drift.Value(calculatedEndTime)),
|
|
);
|
|
|
|
// Verify ride is now cleanly closed
|
|
final closedRide = await (db.select(db.rides)..where((r) => r.id.equals(rideId))).getSingle();
|
|
expect(closedRide.endTime, isNotNull);
|
|
expect(closedRide.endTime!.difference(lastSampleTime).inSeconds.abs(), lessThanOrEqualTo(1));
|
|
});
|
|
});
|
|
|
|
group('Phase 10: Performance Timer Interpolation', () {
|
|
late AppDatabase db;
|
|
|
|
setUp(() {
|
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
|
});
|
|
|
|
tearDown(() async {
|
|
await db.close();
|
|
});
|
|
|
|
test('Performance Timer reaches completed state on 0-60 threshold cross', () async {
|
|
final controller = StreamController<TelemetrySample>.broadcast();
|
|
final timerService = PerformanceTimerService(db, controller.stream);
|
|
|
|
timerService.setTarget(PerformanceTarget.sixty);
|
|
expect(timerService.state.state, equals(PerformanceState.ready));
|
|
|
|
final baseTime = DateTime.now();
|
|
|
|
// Send stationary sample
|
|
controller.add(TelemetrySample(
|
|
timestamp: baseTime,
|
|
speed: 0.0,
|
|
longitudinalG: 0.0,
|
|
));
|
|
await Future.delayed(const Duration(milliseconds: 10));
|
|
|
|
// Send launch sample (speed > 1.0, forward accel > 0.2G)
|
|
controller.add(TelemetrySample(
|
|
timestamp: baseTime.add(const Duration(milliseconds: 200)),
|
|
speed: 5.0,
|
|
longitudinalG: 0.8,
|
|
));
|
|
await Future.delayed(const Duration(milliseconds: 10));
|
|
expect(timerService.state.state, equals(PerformanceState.timing));
|
|
|
|
// Send intermediate speed sample
|
|
controller.add(TelemetrySample(
|
|
timestamp: baseTime.add(const Duration(milliseconds: 1500)),
|
|
speed: 35.0,
|
|
longitudinalG: 0.6,
|
|
));
|
|
await Future.delayed(const Duration(milliseconds: 10));
|
|
|
|
// Send crossing speed sample (62 MPH at 2800ms)
|
|
controller.add(TelemetrySample(
|
|
timestamp: baseTime.add(const Duration(milliseconds: 2800)),
|
|
speed: 62.0,
|
|
longitudinalG: 0.5,
|
|
));
|
|
await Future.delayed(const Duration(milliseconds: 20));
|
|
|
|
expect(timerService.state.state, equals(PerformanceState.completed));
|
|
expect(timerService.state.lastResult, isNotNull);
|
|
expect(timerService.state.lastResult!.target, equals(PerformanceTarget.sixty));
|
|
expect(timerService.state.lastResult!.timeSeconds, greaterThan(0.0));
|
|
expect(timerService.state.lastResult!.timeSeconds, lessThan(3.0));
|
|
|
|
timerService.dispose();
|
|
await controller.close();
|
|
});
|
|
});
|
|
|
|
group('Phase 24: GPX & CSV Export Validation', () {
|
|
test('GPX output contains valid XML structure and telemetry extensions', () {
|
|
final samples = [
|
|
TelemetrySample(
|
|
timestamp: DateTime.utc(2026, 8, 27, 10, 0, 0),
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
altitude: 15.0,
|
|
speed: 45.0,
|
|
heading: 180.0,
|
|
leanAngle: 12.5,
|
|
longitudinalG: 0.2,
|
|
lateralG: 0.1,
|
|
),
|
|
];
|
|
|
|
// Verify telemetry fields for export formatting
|
|
expect(samples.first.latitude, equals(37.7749));
|
|
expect(samples.first.longitude, equals(-122.4194));
|
|
expect(samples.first.speed, equals(45.0));
|
|
});
|
|
});
|
|
}
|