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

230 lines
10 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/api_status.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// A STATUS THIS BUILD HAS NEVER HEARD OF MUST NOT BE A SUCCESS
///
/// Statuses were compared as string literals wherever a screen needed one, so
/// an unrecognised value took whichever branch the condition happened to fall
/// through to — and the conditions were written as `!= 'Cancelled'`, which
/// means a new backend status read as *fine*.
///
/// The backend will add values this build does not know. These pin the two
/// rules that make that safe: unknown parses to `unknown`, and `unknown` is
/// excluded from every "this is finished / this worked" predicate.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
statusTranslation();
group('assignment', () {
test('parses the contract\'s values', () {
expect(AssignmentStatus.parse('Assigned'), AssignmentStatus.assigned);
expect(AssignmentStatus.parse('Accepted'), AssignmentStatus.accepted);
expect(AssignmentStatus.parse('Rejected'), AssignmentStatus.rejected);
expect(AssignmentStatus.parse('Reassigned'), AssignmentStatus.reassigned);
expect(AssignmentStatus.parse('Completed'), AssignmentStatus.completed);
expect(AssignmentStatus.parse('Cancelled'), AssignmentStatus.cancelled);
});
test('survives case and separator drift on the wire', () {
expect(AssignmentStatus.parse('ACCEPTED'), AssignmentStatus.accepted);
expect(AssignmentStatus.parse(' accepted '), AssignmentStatus.accepted);
});
test('an unknown value is unknown, and is not settled', () {
final s = AssignmentStatus.parse('Escalated_To_Ops');
expect(s, AssignmentStatus.unknown);
expect(
s.isSettled,
isFalse,
reason: 'a status this build cannot read must not be filed as done',
);
expect(s.isLive, isFalse);
expect(s.needsDecision, isFalse);
});
test('null and empty do not throw', () {
expect(AssignmentStatus.parse(null), AssignmentStatus.unknown);
expect(AssignmentStatus.parse(''), AssignmentStatus.unknown);
});
});
group('booking', () {
test('parses the contract\'s values', () {
expect(
BookingStatus.parse('Pending_Pickup'),
BookingStatus.pendingPickup,
);
expect(
BookingStatus.parse('Miler_Assigned'),
BookingStatus.milerAssigned,
);
expect(
BookingStatus.parse('Pickup_Scheduled'),
BookingStatus.pickupScheduled,
);
expect(BookingStatus.parse('Picked_Up'), BookingStatus.pickedUp);
expect(
BookingStatus.parse('Converted_To_Consignment'),
BookingStatus.convertedToConsignment,
);
});
// ── pickup-complete is the pivot ──
//
// It converts the booking into a consignment, so both rungs past it are
// "collected". A check that only looked for Picked_Up would treat a
// converted booking as still needing collection.
test('collection covers both rungs past the pivot', () {
expect(BookingStatus.pickedUp.isCollected, isTrue);
expect(BookingStatus.convertedToConsignment.isCollected, isTrue);
expect(BookingStatus.pickupScheduled.isCollected, isFalse);
});
// The server refuses cancellation once picked up; the client must not offer
// a control whose only outcome is a 400.
test('cancellation is refused once picked up', () {
expect(BookingStatus.pendingPickup.canCancel, isTrue);
expect(BookingStatus.milerAssigned.canCancel, isTrue);
expect(BookingStatus.pickedUp.canCancel, isFalse);
expect(BookingStatus.convertedToConsignment.canCancel, isFalse);
expect(BookingStatus.unknown.canCancel, isFalse);
});
});
group('consignment', () {
test('parses every state the contract lists', () {
const wire = {
'Created': ConsignmentStatus.created,
'Inwarded_at_Hub': ConsignmentStatus.inwardedAtHub,
'Tripsheet_Loaded': ConsignmentStatus.tripsheetLoaded,
'In_Transit': ConsignmentStatus.inTransit,
'Out_for_Delivery': ConsignmentStatus.outForDelivery,
'Delivered': ConsignmentStatus.delivered,
'RTO_Initiated': ConsignmentStatus.rtoInitiated,
'Returned_to_Sender': ConsignmentStatus.returnedToSender,
'Missing': ConsignmentStatus.missing,
'Damaged': ConsignmentStatus.damaged,
};
wire.forEach((raw, expected) {
expect(ConsignmentStatus.parse(raw), expected, reason: raw);
});
});
// Deliver and skip are legal only from Out_for_Delivery — anything else is
// a 400. Offering the control elsewhere offers a guaranteed failure.
test('only Out_for_Delivery is deliverable', () {
expect(ConsignmentStatus.outForDelivery.isDeliverable, isTrue);
for (final s in ConsignmentStatus.values) {
if (s == ConsignmentStatus.outForDelivery) continue;
expect(s.isDeliverable, isFalse, reason: '$s must not offer deliver');
}
});
test('returning and exception states are closed, unknown is not', () {
expect(ConsignmentStatus.rtoInitiated.isReturning, isTrue);
expect(ConsignmentStatus.returnedToSender.isReturning, isTrue);
expect(ConsignmentStatus.missing.isException, isTrue);
expect(ConsignmentStatus.damaged.isException, isTrue);
expect(ConsignmentStatus.delivered.isClosed, isTrue);
expect(
ConsignmentStatus.unknown.isClosed,
isFalse,
reason: 'an unreadable status must not close a consignment',
);
});
});
group('rider availability', () {
test('the break value is Break, not On_Break', () {
expect(RiderAvailability.parse('Break'), RiderAvailability.onBreak);
expect(RiderAvailability.onBreak.wire, 'Break');
expect(
RiderAvailability.parse('On_Break'),
RiderAvailability.unknown,
reason: 'On_Break is the obvious guess and the wrong one',
);
});
test('every known value round-trips through the wire form', () {
for (final s in RiderAvailability.values) {
if (s == RiderAvailability.unknown) continue;
expect(RiderAvailability.parse(s.wire), s, reason: s.name);
}
});
test('an unknown status is never echoed back as though understood', () {
final s = RiderAvailability.parse('On_Refuelling_Stop');
expect(s, RiderAvailability.unknown);
expect(s.wire, 'Offline');
expect(s.isWorking, isFalse);
});
});
}
/// ─────────────────────────────────────────────────────────────────────────
/// THE ONE PLACE THE CONTRACT BECOMES THE UI'S VOCABULARY
///
/// `legacyStatusFromNew` was a switch on raw strings whose `default` returned
/// the value unchanged. Two things went wrong quietly: `Pending_Pickup` and
/// `Created` were not listed and fell through as themselves, and any status the
/// backend added tomorrow would arrive in the UI as an unrecognised string the
/// row logic then had to guess at.
/// ─────────────────────────────────────────────────────────────────────────
void statusTranslation() {
group('booking status → the row vocabulary', () {
test('everything the rider still has to decide lands as assigned', () {
for (final raw in const ['Pending_Pickup', 'Created', 'Miler_Assigned']) {
expect(ApiConfig.legacyStatusFromNew(raw), 'assigned', reason: raw);
}
});
test('the working rungs keep their meaning', () {
// ── `Pickup_Scheduled` is the ACCEPTED rung ──
//
// This expected `active`, and that expectation was the bug: `active`
// means *the rider is on the stop* to every row in this app, and
// `stopStateOf` reads a raw `active` ahead of the local accepted record.
// So the moment the queue came back after an accept, the stop reported as
// arrived — the selection bar offered **Mark as Picked** for a kitchen
// the rider had not reached, and arrival, the one rung with a real
// endpoint behind it, could never be recorded or seen by the hub.
//
// The backend writes this status when the rider accepts; the console has
// always mapped it to *accepted*. The two applications were reading one
// status two different ways.
expect(ApiConfig.legacyStatusFromNew('Pickup_Scheduled'), 'accepted');
expect(ApiConfig.legacyStatusFromNew('At_Customer'), 'arrived');
expect(ApiConfig.legacyStatusFromNew('Picked_Up'), 'Picked up');
expect(
ApiConfig.legacyStatusFromNew('Converted_To_Consignment'),
'picked',
);
expect(ApiConfig.legacyStatusFromNew('Cancelled'), 'cancelled');
});
// ── Wrong-but-safe beats wrong-and-settled ──
//
// A status this build cannot classify must never render as picked,
// cancelled or otherwise finished. It renders as undecided, which is the
// state a rider can still act on.
test('an unknown status never becomes a settled one', () {
for (final raw in const [
'In_Transit',
'Escalated_To_Ops',
'',
'total nonsense',
]) {
final legacy = ApiConfig.legacyStatusFromNew(raw);
expect(legacy, '', reason: raw);
expect(legacy, isNot('picked'));
expect(legacy, isNot('cancelled'));
expect(legacy, isNot('Picked up'));
}
expect(ApiConfig.legacyStatusFromNew(null), '');
});
});
}