Files
doormile_milderapp/test/activity_trips_test.dart
2026-08-28 11:13:15 +05:30

237 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, dynamic> 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<String> ids(List<Map<String, dynamic>> 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 excluded, and reported', () {
// ── This assertion was inverted, deliberately ──
//
// It used to read `isTrue`: a payload defect was held not to be evidence
// the work happened yesterday, so an undated row was admitted to today.
// That reasoning had the burden of proof backwards. `GET /miler/bookings`
// returns the rider's whole open set, so "the API sent it" says nothing
// about *when* — and this branch turned out to be the hole yesterday's
// finished bookings came through, on a screen whose entire premise is
// that everything on it happened today.
//
// A row that cannot prove it is today's is not shown today. Nothing is
// deleted: [ServiceDay.of] still answers, both local stores keep their
// records, and the backend is untouched. It is a view filter.
//
// Still not something to print on the rider's screen — he can do nothing
// about a backend row with no timestamp — so it goes to the console
// instead of to a badge. See [ServiceDay.logUndated].
final row = {'orderid': 'x', 'orderstatus': 'delivered'};
expect(ServiceDay.belongsToToday(row), isFalse);
expect(ServiceDay.of(row), '');
final reported = <Map<String, dynamic>>[];
ServiceDay.belongsToToday(row, onUndated: reported.add);
expect(reported, [row], reason: 'excluded, but never in silence');
});
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');
});
});
}