Files
rippr/test/ride_notification_test.dart
uhryniuk 5013b7002f V3-06: live stats in the notification
RideNotificationController seam over flutter_local_notifications; RideNotificationCoordinator wires TripRepository.watchActiveTrip() to it and routes Pause/Resume actions back into RecordingEngine. Instantiated eagerly at app root rather than screen-owned, since a pocketed ride has no visible widget tree. Documents an unresolved risk: geolocator's own foreground-service notification cannot be suppressed, so two notifications may be visible until verified on a device.
2026-08-17 15:04:34 -05:00

136 lines
4.4 KiB
Dart

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();
});
});
}