Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
218 lines
8.9 KiB
Dart
218 lines
8.9 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 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');
|
||
});
|
||
});
|
||
}
|