import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/data/accepted_store.dart'; import 'package:miler/data/route_order.dart'; import 'package:miler/data/service_day.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/home/trip.dart'; /// ───────────────────────────────────────────────────────────────────────── /// ACTIVITY IS TODAY, IN THREE TRIPS /// /// The page answered "everything you have ever finished, newest first", cut /// into date bands. The rider's question is narrower and shaped differently: /// **how did Trip 2 go.** He was given three runs this morning; he wants the /// record back in the same three. /// /// These pin the four properties that makes it that record rather than a feed: /// the grouping is the hub's, the order inside a trip is the hub's, the day is /// today's, and nothing internal leaks onto the screen. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { SharedPreferences.setMockInitialValues({'userid': 38}); ServiceProfile.setActive(ServiceProfile.milkMan); }); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); Map done( String id, { int step = 0, DateTime? at, String status = 'delivered', String? tripId, String? day, }) => { 'orderid': id, 'pickupid': int.tryParse(id) ?? id.hashCode.abs(), 'orderstatus': status, 'step': step, 'pickupcustomer': 'Customer $id', if (tripId != null) 'tripid': tripId, if (at != null) 'expected_pickup_time': at.toIso8601String(), if (at != null) 'completedat': at.toIso8601String(), 'completedday': day ?? ServiceDay.today, }; List ids(List> s) => [ for (final x in s) x['orderid'] as String, ]; group('grouped into the rider’s three trips', () { test('a three-run day produces Trip 1, Trip 2 and Trip 3', () { final trips = Trip.groupIntoTrips([ done('a', at: DateTime(2026, 8, 21, 9)), done('b', at: DateTime(2026, 8, 21, 14)), done('c', at: DateTime(2026, 8, 21, 19)), ]); expect(trips.map((t) => t.tripNumber), [1, 2, 3]); }); test('a completed stop stays under the trip it belonged to', () { final trips = Trip.groupIntoTrips([ done('morning-1', at: DateTime(2026, 8, 21, 8)), done('morning-2', at: DateTime(2026, 8, 21, 11)), done('evening-1', at: DateTime(2026, 8, 21, 18)), ]); final slots = trips.slots; expect(ids(slots[0]!.stops), ['morning-1', 'morning-2']); expect(slots[1], isNull, reason: 'nothing was done in the afternoon'); expect(ids(slots[2]!.stops), ['evening-1']); }); test('the hub’s own trip id beats the clock', () { // Two stops eleven hours apart that the hub calls one run. The day-part // rule is a fallback for when it says nothing — never an override. final trips = Trip.groupIntoTrips([ done('a', at: DateTime(2026, 8, 21, 8), tripId: 'T7'), done('b', at: DateTime(2026, 8, 21, 19), tripId: 'T7'), ]); expect(trips.length, 1); expect(ids(trips.single.stops), ['a', 'b']); }); test('a trip with no dates at all still lands in a slot', () { // No timestamps anywhere. It must not vanish, and it must not be // labelled with an internal data-quality word. final trips = Trip.groupIntoTrips([done('a'), done('b')]); expect(trips.single.stops, hasLength(2)); expect(trips.slots[0], isNotNull); }); }); group('the admin sequence survives into the record', () { test('a trip shows its stops in the hub’s order, not by finish time', () { // Finished out of order — c first, a last — but the run was planned // a → b → c, and the record of a planned run is the plan. final trips = Trip.groupIntoTrips([ done('c', step: 3, at: DateTime(2026, 8, 21, 9, 5)), done('a', step: 1, at: DateTime(2026, 8, 21, 9, 40)), done('b', step: 2, at: DateTime(2026, 8, 21, 9, 20)), ]); expect(ids(trips.single.stops), ['a', 'b', 'c']); expect(RouteOrder.sequenceOf(trips.single.stops.first), 1); }); test('grouping never re-bases the sequence per trip', () { final trips = Trip.groupIntoTrips([ done('a5', step: 5, at: DateTime(2026, 8, 21, 15)), done('a4', step: 4, at: DateTime(2026, 8, 21, 14)), done('m1', step: 1, at: DateTime(2026, 8, 21, 9)), ]); expect(ids(trips.slots[1]!.stops), ['a4', 'a5']); expect(RouteOrder.sequenceOf(trips.slots[1]!.stops.first), 4); }); }); group('today only, without a timer', () { test('yesterday is not today’s activity', () { final yesterday = ServiceDay.stamp( DateTime.now().subtract(const Duration(days: 1)), ); expect(ServiceDay.belongsToToday(done('a', day: yesterday)), isFalse); expect(ServiceDay.belongsToToday(done('b')), isTrue); }); test('the rollover needs nothing to run at midnight', () { // The same record, asked about on two different days. Nothing was // deleted and no timer fired — the answer is computed at read time, // which is the only thing that survives the app being closed at 23:59. final row = done('a', day: '2026-08-21'); expect(ServiceDay.belongsToToday(row, now: '2026-08-21'), isTrue); expect(ServiceDay.belongsToToday(row, now: '2026-08-22'), isFalse); }); test('a record with no date is kept, quietly', () { // A payload defect is not evidence the work happened yesterday, and it // is not something to print on the rider's screen either. final row = {'orderid': 'x', 'orderstatus': 'delivered'}; expect(ServiceDay.belongsToToday(row), isTrue); expect(ServiceDay.of(row), ''); }); test('a timestamp is used when no day stamp was written', () { final row = { 'orderid': 'x', 'completedat': DateTime.now().toIso8601String(), }; expect(ServiceDay.belongsToToday(row), isTrue); }); test('the store and the reader agree on what a day is', () async { await addCompletedBookings([done('a')], terminalStatus: 'delivered'); final filed = (await getCompletedBookings()).single; expect( ServiceDay.of(filed), ServiceDay.today, reason: 'two implementations of a date format is how a shift ends up ' 'half in one day and half in the next', ); }); }); group('nothing internal reaches the rider', () { test('the work-type mark never says MILK', () { // The service profile stays — it is what separates one rider's records // from another's. What goes is printing its name on his screen. expect(ServiceProfile.milkMan.workTypeLabel, isNot('MILK')); expect(ServiceProfile.milkMan.workTypeLabel, 'DELIVERY'); // Logistics never had a single-type mark and still does not — its stops // genuinely differ, so each one names itself. expect(ServiceProfile.parcel.workTypeLabel, isNull); expect(ServiceProfile.parcel.usesSingleWorkType, isFalse); }); test('records are still isolated by owner, in the data', () async { await addCompletedBookings([done('a')], terminalStatus: 'delivered'); expect(await getCompletedOrderIds(), contains('a')); SharedPreferences.setMockInitialValues({'userid': 99}); expect( await getCompletedOrderIds(), isEmpty, reason: 'separation is an ownership rule, not a label on a card', ); }); test('the other line’s records are not this one’s', () async { await addCompletedBookings([done('a')], terminalStatus: 'delivered'); ServiceProfile.setActive(ServiceProfile.parcel); expect(await getCompletedOrderIds(), isEmpty); }); }); group('delivered and skipped stay different things', () { test('a skip is filed as a skip, never as a cancellation', () async { await addCompletedBookings( [done('a', status: 'outfordelivery')], cancelled: true, terminalStatus: 'skipped', ); final filed = (await getCompletedBookings()).single; expect(filed['orderstatus'], 'skipped'); expect(filed['orderstatus'], isNot('cancelled')); }); test('a skip keeps the reason the rider gave', () async { await addSkippedBooking(done('a'), reason: 'Shutter down'); final parked = (await getSkippedBookings()).single; expect(parked['skipreason'], 'Shutter down'); expect(parked['orderstatus'], 'skipped'); }); }); }