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>
230 lines
10 KiB
Dart
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), '');
|
|
});
|
|
});
|
|
}
|