Files
doormile_milderapp/test/activity_trips_test.dart
Thiru-tenext d7348e253f 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>
2026-08-22 05:40:35 +05:30

218 lines
8.9 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: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');
});
});
}