237 lines
10 KiB
Dart
237 lines
10 KiB
Dart
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');
|
||
});
|
||
});
|
||
}
|