Files
doormile_milderapp/test/lifecycle_boundary_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

211 lines
8.3 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/Models/stop_status.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/data/milk_run.dart';
import 'package:miler/data/work_domain.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// ONE ORDER, ONE SCREEN
///
/// ```
/// ASSIGNED ─ accept ─▶ ACCEPTED ─ go ─▶ ARRIVED ─ pick up ─▶ PICKED
/// └────────────────── HOME owns all of this ──────────────┘ │
/// pickup-complete │
/// ▼
/// DELIVERIES owns from here on
/// ```
///
/// [WorkBoundary] is the single function that decides which side a row is on.
/// Deliveries asked it; **Home never did** — it assigned the raw fetch to its
/// route list and let each card work itself out. So on a kitchen line, where
/// `isWorkComplete` is `delivered || cancelled`, a *picked* stop stayed in
/// Home's route while simultaneously sitting in Deliveries' queue: one bag, two
/// screens, two different next actions.
///
/// These hold the guarantee itself rather than either screen's filter — the
/// two lists are provably disjoint and provably exhaustive, so no future filter
/// can drift back apart from the other.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => SharedPreferences.setMockInitialValues({}));
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
Map<String, dynamic> stop(String id, String status) => {
'orderid': id,
'pickupid': int.parse(id),
'sourcename': 'Vidhya Kitchen',
'orderstatus': status,
};
/// The whole day, in every state a row can reach.
List<Map<String, dynamic>> day() => [
stop('1001', 'assigned'),
stop('1002', 'accepted'),
stop('1003', 'arrived'),
stop('1004', 'picked'),
stop('1005', 'outfordelivery'),
stop('1006', 'delivered'),
stop('1007', 'cancelled'),
stop('1008', 'skipped'),
];
group('the two lists never overlap', () {
for (final profile in [ServiceProfile.milkMan, ServiceProfile.parcel]) {
test('on ${profile.line.name}', () {
ServiceProfile.setActive(profile);
const accepted = {'1002', '1003'};
const collected = {'1004'};
final home = WorkBoundary.pickupQueue(
day(),
acceptedIds: accepted,
collectedIds: collected,
).map((s) => s['orderid']).toSet();
final deliveries = WorkBoundary.deliveryQueue(
day(),
acceptedIds: accepted,
collectedIds: collected,
).map((s) => s['orderid']).toSet();
expect(
home.intersection(deliveries),
isEmpty,
reason: 'an order on both screens is one bag with two next actions',
);
});
}
});
group('the boundary is pickup-complete, never acceptance', () {
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
test('an accepted booking is Home\'s, not delivery work', () {
// The one rule the delivery screens hang off: accepting is a decision
// about work still to be done, not evidence anything was collected.
// Reading it as a hand-over opened a map, an I'VE ARRIVED and a
// confirmation sheet for a pickup the rider had not made.
expect(
WorkBoundary.domainOf(stop('1002', 'accepted'), acceptedIds: {'1002'}),
WorkDomain.pickup,
);
});
test('an arrived booking is still Home\'s', () {
expect(
WorkBoundary.domainOf(stop('1003', 'arrived'), acceptedIds: {'1003'}),
WorkDomain.pickup,
);
});
test('the collected record moves it, and only after the call landed', () {
// `addCollectedOrderIds` is written by Home *after* pickup-complete comes
// back successful — never optimistically.
expect(
WorkBoundary.domainOf(stop('1004', 'accepted')),
WorkDomain.pickup,
);
expect(
WorkBoundary.domainOf(stop('1004', 'accepted'), collectedIds: {'1004'}),
WorkDomain.delivery,
);
});
test('the backend status moves it on its own', () {
for (final s in ['picked', 'outfordelivery']) {
expect(
WorkBoundary.domainOf(stop('1004', s)),
WorkDomain.delivery,
reason: '$s is past the counter',
);
}
});
});
group('closed work belongs to neither', () {
test('a delivered milk-run stop leaves both screens', () {
ServiceProfile.setActive(ServiceProfile.milkMan);
expect(
WorkBoundary.domainOf(stop('1006', 'delivered')),
WorkDomain.closed,
);
});
test('a picked parcel booking is closed; a picked milk run is not', () {
// Collecting IS the job on logistics — the hub delivers it. On a round it
// is the middle of the morning.
ServiceProfile.setActive(ServiceProfile.parcel);
expect(WorkBoundary.domainOf(stop('1004', 'picked')), WorkDomain.closed);
ServiceProfile.setActive(ServiceProfile.milkMan);
expect(
WorkBoundary.domainOf(stop('1004', 'picked')),
WorkDomain.delivery,
);
});
});
_sourceNameContract();
group('the consignment id survives the pivot', () {
test('it is filed under the order id the door reads', () async {
// `updatePickedStatus` sent no `orderid`, so the mapper filed the
// consignment under the *booking* id while `closeDelivery` looked it up
// by **order** id. Every collected stop was stored under a key nothing
// would ask for, and pressing Delivered answered "this order was never
// picked up on the system" for a bag in the rider's hands.
await rememberConsignmentId('DM-BK-16DA518D-6381', '55123');
final ids = await getConsignmentIds();
expect(ids['DM-BK-16DA518D-6381'], '55123');
});
test('an empty id is never stored, so nothing can be fabricated', () async {
await rememberConsignmentId('DM-1', '');
await rememberConsignmentId('', '55124');
expect(await getConsignmentIds(), isEmpty);
});
});
}
/// ─────────────────────────────────────────────────────────────────────────
/// ONE READER FOR A PLACE'S NAME
///
/// `MilkRun.sourceNameOf` read `sourcename` / `kitchenname`; `stopSourceName`
/// read those *and* the CamelCase `SourceName` / `KitchenName` the payload
/// sometimes carries. A booking with only the capitalised key had a counter
/// according to one and none according to the other — and the route card used
/// both in one expression, so the group was not flat (there is a counter) but
/// was titled "pickup" (there is no counter to name).
///
/// On a device that is a foldable place headed by a leg description, holding
/// one order named after the same stop — exactly the shape the `flat` flag
/// exists to prevent.
void _sourceNameContract() {
group('a place has one name', () {
for (final key in const [
'sourcename',
'SourceName',
'kitchenname',
'KitchenName',
]) {
test('$key is read by both readers', () {
final stop = <String, dynamic>{'orderid': '1', key: 'Vidhya Kitchen'};
expect(MilkRun.sourceNameOf(stop), 'Vidhya Kitchen');
expect(stopSourceName(stop), 'Vidhya Kitchen');
expect(stopSourceName(stop), MilkRun.sourceNameOf(stop));
});
}
test('no key means no counter, from both', () {
const stop = <String, dynamic>{'orderid': '1'};
expect(MilkRun.sourceNameOf(stop), isEmpty);
expect(stopSourceName(stop), isEmpty);
});
});
}