Files
doormile_milderapp/test/home_today_only_test.dart
2026-08-28 15:07:30 +05:30

306 lines
12 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:miler/data/api_config.dart';
import 'package:miler/data/service_day.dart';
import 'package:miler/views/Dashboard/home/trip.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// HOME IS TODAY, AND TODAY IS THREE TRIPS
///
/// Two bugs with one cause, and this is the file that holds the cause down.
///
/// ── What the rider saw ──
///
/// 1. **Yesterday's assignments were on Home.** `GET /miler/bookings` returns
/// his whole *open* set, not his day, so a booking raised on Tuesday and
/// never closed came back on Wednesday and on Thursday — inside the kitchen
/// dropdown, and inside every count above it.
/// 2. **Every run was Trip 1.** The hub assigns morning, afternoon and evening
/// slots and the tabs are those slots, but an evening run opened filed
/// under the morning.
///
/// ── The one cause ──
///
/// `ApiConfig.pickupFromBooking` builds a **fixed map**: a field it does not
/// name is a field no screen can ever see. Every timestamp on the booking was
/// unnamed, so it was thrown away at the adapter — and both features downstream
/// are clocks. [ServiceDay] asked "which day is this" in field names the
/// adapted row no longer spoke and got `''`; [Trip] asked "which part of the
/// day" and got the same silence, dropped every stop into one undated bucket,
/// and that bucket landed in the first free slot.
///
/// So the fix is at the adapter, and these tests start there. The two features
/// are then checked on the shapes the wire really produces.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
/// A booking as `GET /miler/bookings` returns it — the new shape, which is
/// the one that goes through the translation.
Map<String, dynamic> booking({
required String id,
String status = 'Miler_Assigned',
Map<String, dynamic> extra = const {},
}) => <String, dynamic>{
'bookingid': id,
'bookingreference': 'DM-$id',
'status': status,
'customername': 'Joe Mathew',
'pickupaddress': '12 SNS Colony, Peelamedu, Coimbatore',
...extra,
};
group('the adapter keeps the clocks', () {
test('a booking that carries a timestamp still carries it afterwards', () {
final row = ApiConfig.pickupFromBooking(
booking(
id: '1',
extra: const {
'createdat': '2026-08-27T09:14:00',
'updatedon': '2026-08-27T11:02:00',
},
),
);
// The whole failure was here: these came back and went nowhere.
expect(row['createdat'], '2026-08-27T09:14:00');
expect(row['updatedon'], '2026-08-27T11:02:00');
expect(ServiceDay.of(row), '2026-08-27');
});
test('every name ServiceDay reads survives the translation', () {
// A regression net for the shape of the bug rather than one instance of
// it: the adapter and the day filter must agree on the vocabulary, or a
// key added to one and not the other silently un-dates the row again.
for (final key in ServiceDay.timeKeys) {
// Written by the local stores, never by the wire.
if (key == 'completedat' || key == 'skippedat') continue;
final row = ApiConfig.pickupFromBooking(
booking(id: 'x', extra: {key: '2026-08-27T10:00:00'}),
);
expect(
ServiceDay.of(row),
'2026-08-27',
reason: '$key was dropped at the adapter',
);
}
});
test('an absent timestamp is not invented', () {
final row = ApiConfig.pickupFromBooking(booking(id: '2'));
expect(row.containsKey('pickedtime'), isFalse);
expect(ServiceDay.of(row), '');
});
});
group("yesterday's assignment is not today's work", () {
const today = '2026-08-27';
test('a booking still open from yesterday does not belong to today', () {
final row = ApiConfig.pickupFromBooking(
booking(id: '3', extra: const {'createdat': '2026-08-26T18:40:00'}),
);
expect(ServiceDay.belongsToToday(row, now: today), isFalse);
});
test('the same booking, raised this morning, does', () {
final row = ApiConfig.pickupFromBooking(
booking(id: '4', extra: const {'createdat': '2026-08-27T07:05:00'}),
);
expect(ServiceDay.belongsToToday(row, now: today), isTrue);
});
test('tomorrow is not today either', () {
// A pre-assigned booking is real work and it is not this shift's. It
// reappears on its own morning, when the same predicate admits it.
final row = ApiConfig.pickupFromBooking(
booking(id: '5', extra: const {'createdat': '2026-08-28T06:00:00'}),
);
expect(ServiceDay.belongsToToday(row, now: today), isFalse);
});
test('the local assignment stamp dates a row the backend does not', () {
// The live contract sends no clock the app can trust as an assignment
// time, so [WorkRepository] writes when the booking first reached this
// rider. It is the weakest key on the list and it is the one that
// actually fires on Doormile.
final row = ApiConfig.pickupFromBooking(booking(id: '6'))
..['assignedat'] = '2026-08-26T09:00:00';
expect(ServiceDay.of(row), '2026-08-26');
expect(ServiceDay.belongsToToday(row, now: today), isFalse);
});
test('a clock the backend sends outranks the local guess', () {
// The rider's phone met this booking on the 27th; the hub finished it on
// the 26th. The hub is right — the app's first sighting says only when
// the app was open.
final row = ApiConfig.pickupFromBooking(
booking(
id: '7',
status: 'Picked',
extra: const {'pickedtime': '2026-08-26T20:10:00'},
),
)..['assignedat'] = '2026-08-27T08:00:00';
expect(ServiceDay.of(row), '2026-08-26');
});
});
group('the work screens filter one day, by one rule', () {
const today = '2026-08-27';
Map<String, dynamic> dated(String id, String at) =>
ApiConfig.pickupFromBooking(booking(id: id))..['assignedat'] = at;
test("yesterday's leftovers are dropped and today's kept", () {
final kept = ServiceDay.onlyToday([
dated('old', '2026-08-26T09:00:00'),
dated('new', '2026-08-27T09:00:00'),
], now: today);
expect(kept.map((s) => s['orderid']), ['DM-new']);
});
test('an undated row among dated ones is dropped', () {
// The burden of proof is on the row — the same rule Activity applies.
final kept = ServiceDay.onlyToday([
dated('a', '2026-08-27T09:00:00'),
ApiConfig.pickupFromBooking(booking(id: 'nodate')),
], now: today);
expect(kept.map((s) => s['orderid']), ['DM-a']);
});
test('a day where NOTHING can be dated is kept whole', () {
// The narrow safety net. Not a stale queue — a build that has lost the
// ability to date anything — and blanking a rider's screen would hide
// that behind something that reads like good news.
final rows = [
ApiConfig.pickupFromBooking(booking(id: 'a')),
ApiConfig.pickupFromBooking(booking(id: 'b')),
];
expect(ServiceDay.onlyToday(rows, now: today).length, 2);
});
test('an empty day stays empty', () {
expect(ServiceDay.onlyToday(const [], now: today), isEmpty);
});
test('a picked stop finished today stays — the rail needs it', () {
// Deliveries and Home both count finished work into the day's figures.
// Dropping it would empty the progress ring every time a rider succeeds.
final done = ApiConfig.pickupFromBooking(
booking(
id: 'p',
status: 'Picked',
extra: const {'pickedtime': '2026-08-27T11:20:00'},
),
);
expect(ServiceDay.onlyToday([done], now: today).length, 1);
});
});
group('the slot a stop lands in is the part of the day', () {
/// The adapted shape plus the stamp the repository merges on.
Map<String, dynamic> assigned(String id, DateTime at) =>
ApiConfig.pickupFromBooking(booking(id: id))
..['assignedat'] = at.toIso8601String();
test('assigned in the morning is Trip 1', () {
final trips = Trip.groupIntoTrips([
assigned('a', DateTime(2026, 8, 27, 8, 30)),
]);
expect(trips.single.dayPart, DayPart.morning);
expect(trips.single.tripNumber, 1);
expect(trips.slots.indexOf(trips.single), 0);
});
test('assigned in the afternoon is Trip 2, on a day with no morning', () {
// The point of the whole slot model. A rider with only afternoon work
// used to see it as Trip 1, so his "Trip 2" and the hub's were different
// runs — and dispatch rings him about the hub's.
final trips = Trip.groupIntoTrips([
assigned('b', DateTime(2026, 8, 27, 14, 5)),
]);
expect(trips.single.tripNumber, 2);
final slots = trips.slots;
expect(slots[0], isNull);
expect(slots[1], isNotNull);
});
test('assigned in the evening is Trip 3', () {
final trips = Trip.groupIntoTrips([
assigned('c', DateTime(2026, 8, 27, 19, 0)),
]);
expect(trips.single.tripNumber, 3);
expect(trips.slots[2], isNotNull);
});
test('a full day fills all three slots in order', () {
final trips = Trip.groupIntoTrips([
assigned('m', DateTime(2026, 8, 27, 9, 0)),
assigned('a', DateTime(2026, 8, 27, 13, 0)),
assigned('e', DateTime(2026, 8, 27, 18, 0)),
]);
expect(trips.length, 3);
expect([for (final t in trips) t.tripNumber], [1, 2, 3]);
// `orderid` is the booking *reference* after translation — the id the
// rider reads, not the row's primary key.
expect(trips.slots.map((t) => t?.stops.single['orderid']), [
'DM-m',
'DM-a',
'DM-e',
]);
});
test('two runs in one part stay one trip', () {
// A day part is not a rolling window: two stops handed over an hour apart
// in the same morning are the same run, and which trip a stop is on must
// never depend on what else is in the day.
final trips = Trip.groupIntoTrips([
assigned('a', DateTime(2026, 8, 27, 7, 30)),
assigned('b', DateTime(2026, 8, 27, 11, 45)),
]);
expect(trips.length, 1);
expect(trips.single.stops.length, 2);
expect(trips.single.tripNumber, 1);
});
test('a real due time still outranks the assignment clock', () {
// Handed over at eight in the morning, due at three in the afternoon. The
// work is the afternoon's; the handover time was only ever a stand-in for
// a field the backend does not send.
final stop = assigned('d', DateTime(2026, 8, 27, 8, 0));
stop['expected_pickup_time'] = DateTime(
2026,
8,
27,
15,
0,
).toIso8601String();
expect(Trip.groupIntoTrips([stop]).single.tripNumber, 2);
});
test('an explicit backend slot window still wins outright', () {
// If the console ever grows real slots, they are the answer and none of
// the inference above runs.
final stop = assigned('s', DateTime(2026, 8, 27, 19, 30))
..['slotstarttime'] = DateTime(2026, 8, 27, 10).toIso8601String()
..['slotendtime'] = DateTime(2026, 8, 27, 13).toIso8601String();
final trip = Trip.groupIntoTrips([stop]).single;
expect(trip.slotLabel, '10:00 AM – 1:00 PM');
expect(trip.tripNumber, 1);
});
test('a stop with no clock at all keeps its arrival order', () {
// Unchanged behaviour, stated so it stays deliberate: a trip that cannot
// be placed by day part must not be guessed at from the wall clock, or it
// would move between tabs as the afternoon wore on.
final trips = Trip.groupIntoTrips([
ApiConfig.pickupFromBooking(booking(id: 'z')),
]);
expect(trips.single.dayPart, isNull);
expect(trips.slots[0], isNotNull);
});
});
}