import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:rippr/src/data/database.dart'; import 'package:rippr/src/data/trip_repository.dart'; import 'package:rippr/src/domain/models.dart'; import 'package:rippr/src/notification/ride_notification_controller.dart'; import 'package:rippr/src/notification/ride_notification_coordinator.dart'; import 'package:rippr/src/notification/ride_notification_text.dart'; import 'package:rippr/src/recording/location_source.dart'; import 'package:rippr/src/recording/recording_engine.dart'; /// V3-06: the plugin call itself needs a device (see the ticket's Tests section), but the /// text it is given, and the routing from a tapped action back to the engine, do not. The /// coordinator tests check the database after an action, not the UI -- the project's /// standing rule for anything touching recording state. void main() { Trip trip({ TripState state = TripState.recording, double distanceM = 12345, int movingMillis = 45 * 60 * 1000 + 12 * 1000, }) => Trip( id: 1, startedAt: 0, state: state, distanceM: distanceM, movingMillis: movingMillis, ); group('rideNotificationText', () { test('recording: distance and elapsed, no prefix', () { expect(rideNotificationText(trip()), '12.3 km · 00:45:12'); }); test('paused: prefixed so the shade reads correctly without opening the app', () { expect( rideNotificationText(trip(state: TripState.paused)), 'Paused · 12.3 km · 00:45:12', ); }); test('honours the unit system, exactly like every other display surface', () { expect( rideNotificationText(trip(), unit: UnitSystem.imperial), contains('mi'), ); }); }); group('RideNotificationCoordinator', () { late AppDatabase db; late TripRepository repo; late FakeLocationSource source; late RecordingEngine engine; late FakeRideNotificationController controller; setUp(() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; db = AppDatabase(NativeDatabase.memory()); repo = TripRepository(db); source = FakeLocationSource(); engine = RecordingEngine(repository: repo, locationSource: source); controller = FakeRideNotificationController(); }); tearDown(() async { await source.dispose(); await db.close(); }); test('shows the notification when a trip starts and cancels it when it ends', () async { final coordinator = RideNotificationCoordinator( controller: controller, engine: engine, tripStream: repo.watchActiveTrip(), ); await repo.startTrip(1000); await pumpEventQueue(); expect(controller.visible, isTrue); expect(controller.lastText, isNotNull); await repo.completeTrip(2000); await pumpEventQueue(); expect(controller.visible, isFalse); expect(controller.cancelCalls, greaterThan(0)); await coordinator.dispose(); }); test('a tapped Pause action actually pauses the ride -- checked in the ' 'database, not the notification', () async { final coordinator = RideNotificationCoordinator( controller: controller, engine: engine, tripStream: repo.watchActiveTrip(), ); await repo.startTrip(1000); await pumpEventQueue(); controller.simulateAction(RideNotificationAction.pause); await pumpEventQueue(); final active = await repo.activeTrip(); expect(active?.state, TripState.paused); await coordinator.dispose(); }); test('a tapped Resume action only resumes an actually-paused trip', () async { final coordinator = RideNotificationCoordinator( controller: controller, engine: engine, tripStream: repo.watchActiveTrip(), ); await repo.startTrip(1000); await pumpEventQueue(); // Recording, not paused: a Resume action here would be bogus. controller.simulateAction(RideNotificationAction.resume); await pumpEventQueue(); expect((await repo.activeTrip())?.state, TripState.recording); await repo.pauseTrip(2000); await pumpEventQueue(); controller.simulateAction(RideNotificationAction.resume); await pumpEventQueue(); expect((await repo.activeTrip())?.state, TripState.recording); await coordinator.dispose(); }); }); }