Miler rider app: surface system, visible design language, backend lifecycle
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>
This commit is contained in:
217
test/activity_trips_test.dart
Normal file
217
test/activity_trips_test.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user