device infos
This commit is contained in:
305
test/home_today_only_test.dart
Normal file
305
test/home_today_only_test.dart
Normal file
@@ -0,0 +1,305 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user