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>
240 lines
8.3 KiB
Dart
240 lines
8.3 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'package:miler/data/api_config.dart';
|
|
import 'package:miler/data/consignment_state.dart';
|
|
import 'package:miler/data/miler_api.dart';
|
|
import 'package:miler/data/mock_backend.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
import 'package:miler/views/Dashboard/pickups/pickups.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// TWO BACKENDS, ONE BUILD
|
|
///
|
|
/// `Collected_By_Miler` and `start-delivery` ship behind a server-side flag
|
|
/// that is **off in production today**, so both lifecycles are live at once:
|
|
///
|
|
/// old `pickup-complete` releases hyperlocal work itself. The consignment
|
|
/// is already `Out_for_Delivery` when it reaches the Deliveries tab and
|
|
/// there is nothing for **Start delivery** to call.
|
|
/// new the pivot stops at `Collected_By_Miler` and the press is what puts
|
|
/// the load on the road.
|
|
///
|
|
/// The app must be right in both **without mirroring the flag**, because a
|
|
/// mirrored flag is a second source of truth that can be wrong — and would be
|
|
/// wrong for every rider for however long it took to ship an app release after
|
|
/// the server toggled. So the behaviour is inferred from what the data says:
|
|
/// `consignmentstatus` on the row, then the consignment itself, then the
|
|
/// `can_start_delivery` / `can_deliver` permissions.
|
|
///
|
|
/// These pin both modes, and pin that the old one costs no request.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late List<http.Request> sent;
|
|
|
|
http.Response jsonRes(Object body, [int code = 200]) => http.Response(
|
|
json.encode(body),
|
|
code,
|
|
headers: const {'content-type': 'application/json'},
|
|
);
|
|
|
|
void stubRoutes(Map<String, http.Response> routes) {
|
|
sent = <http.Request>[];
|
|
MilerApi.client = MockClient((req) async {
|
|
sent.add(req);
|
|
for (final e in routes.entries) {
|
|
if (req.url.path.endsWith(e.key)) return e.value;
|
|
}
|
|
return jsonRes({'success': true});
|
|
});
|
|
}
|
|
|
|
setUp(() async {
|
|
MockBackend.enabled = false;
|
|
SharedPreferences.setMockInitialValues({
|
|
'userid': 38,
|
|
'authtoken': 'test-token',
|
|
});
|
|
await ApiConfig.setToken('test-token');
|
|
ServiceProfile.setActive(ServiceProfile.milkMan);
|
|
stubRoutes(const {});
|
|
});
|
|
|
|
tearDown(() {
|
|
MilerApi.client = http.Client();
|
|
MockBackend.enabled = kMockBackend;
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
});
|
|
|
|
Map<String, dynamic> stop({String? consignmentStatus}) => {
|
|
'orderid': 'ORDER-1',
|
|
'pickupid': 75,
|
|
'consignmentid': 46,
|
|
if (consignmentStatus != null) 'consignmentstatus': consignmentStatus,
|
|
'orderstatus': 'picked',
|
|
};
|
|
|
|
group('old backend — the pivot already released the load', () {
|
|
test('the press costs no request at all', () async {
|
|
stubRoutes(const {});
|
|
final released = await releaseForDelivery(
|
|
stop(consignmentStatus: 'Out_for_Delivery'),
|
|
);
|
|
|
|
expect(released, isTrue);
|
|
expect(
|
|
sent,
|
|
isEmpty,
|
|
reason:
|
|
'a start-delivery the server can only refuse is not a harmless '
|
|
'no-op — it is a 4xx per rider press in the backend\'s log',
|
|
);
|
|
});
|
|
|
|
test('a stop already delivered elsewhere is not re-released', () async {
|
|
stubRoutes(const {});
|
|
expect(
|
|
await releaseForDelivery(stop(consignmentStatus: 'Delivered')),
|
|
isTrue,
|
|
);
|
|
expect(sent, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('new backend — the rider makes the release', () {
|
|
test('a collected consignment is started on the server', () async {
|
|
stubRoutes({
|
|
'/start-delivery': jsonRes({
|
|
'success': true,
|
|
'data': {'status': 'Out_for_Delivery'},
|
|
}),
|
|
});
|
|
|
|
expect(
|
|
await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')),
|
|
isTrue,
|
|
);
|
|
expect(sent.single.method, 'POST');
|
|
expect(
|
|
sent.single.url.path.endsWith('/miler/consignments/46/start-delivery'),
|
|
isTrue,
|
|
);
|
|
expect(sent.single.headers['Idempotency-Key'], isNotNull);
|
|
});
|
|
|
|
test('a refusal is answered by the consignment, not assumed', () async {
|
|
// `INVALID_STATE` means "not Collected_By_Miler" — which covers both a
|
|
// real refusal and a consignment another device already released.
|
|
stubRoutes({
|
|
'/start-delivery': jsonRes({
|
|
'success': false,
|
|
'code': 'INVALID_STATE',
|
|
'message': 'consignment is not collected',
|
|
}, 400),
|
|
'/miler/consignments/46': jsonRes({
|
|
'success': true,
|
|
'data': {'status': 'Out_for_Delivery'},
|
|
}),
|
|
});
|
|
|
|
expect(
|
|
await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')),
|
|
isTrue,
|
|
reason: 'already on the road is not a reason to block the rider',
|
|
);
|
|
});
|
|
|
|
test('a genuine refusal does not fake a release', () async {
|
|
stubRoutes({
|
|
'/start-delivery': jsonRes({
|
|
'success': false,
|
|
'code': 'INVALID_STATE',
|
|
'message': 'consignment is at the hub',
|
|
}, 400),
|
|
'/miler/consignments/46': jsonRes({
|
|
'success': true,
|
|
'data': {'status': 'Inwarded_at_Hub'},
|
|
}),
|
|
});
|
|
|
|
expect(
|
|
await releaseForDelivery(stop(consignmentStatus: 'Collected_By_Miler')),
|
|
isFalse,
|
|
reason:
|
|
'showing ACTIVE for a consignment the hub still holds is the '
|
|
'fiction that made deliver fail at the door with nothing on '
|
|
'screen to explain it',
|
|
);
|
|
});
|
|
});
|
|
|
|
group('a row that says nothing falls back to asking', () {
|
|
test('the consignment is read when the row carries no status', () async {
|
|
stubRoutes({
|
|
'/miler/consignments/46': jsonRes({
|
|
'success': true,
|
|
'data': {'status': 'Out_for_Delivery'},
|
|
}),
|
|
});
|
|
|
|
expect(await releaseForDelivery(stop()), isTrue);
|
|
expect(sent.single.method, 'GET');
|
|
expect(sent.single.url.path.endsWith('/miler/consignments/46'), isTrue);
|
|
});
|
|
|
|
test('the permission flags answer when the word does not', () {
|
|
// A status this build has never heard of, plus the server's own verdict
|
|
// on what may be done with it.
|
|
expect(
|
|
ConsignmentGate.stateFromDetailForTest({
|
|
'status': 'Handed_To_Locker_Bank',
|
|
'can_deliver': true,
|
|
}),
|
|
ConsignmentState.outForDelivery,
|
|
);
|
|
expect(
|
|
ConsignmentGate.stateFromDetailForTest({
|
|
'status': 'Awaiting_Rider_Departure',
|
|
'can_start_delivery': true,
|
|
}),
|
|
ConsignmentState.collectedByMiler,
|
|
);
|
|
});
|
|
|
|
test('no consignment id is not a release', () async {
|
|
expect(await releaseForDelivery({'orderid': 'ORDER-9'}), isFalse);
|
|
});
|
|
});
|
|
|
|
group('a skipped stop and the server agree about whether it is open', () {
|
|
test('a consignment the hub still calls out-for-delivery stays open', () {
|
|
expect(ConsignmentState.outForDelivery.isOpenAfterSkip, isTrue);
|
|
expect(ConsignmentState.collectedByMiler.isOpenAfterSkip, isTrue);
|
|
});
|
|
|
|
test('a consignment the hub closed is closed', () {
|
|
expect(ConsignmentState.delivered.isOpenAfterSkip, isFalse);
|
|
expect(ConsignmentState.rtoInitiated.isOpenAfterSkip, isFalse);
|
|
expect(ConsignmentState.returnedToSender.isOpenAfterSkip, isFalse);
|
|
expect(ConsignmentState.cancelled.isOpenAfterSkip, isFalse);
|
|
});
|
|
|
|
test('a read that failed is not permission to close a stop', () {
|
|
expect(
|
|
ConsignmentState.unknown.isOpenAfterSkip,
|
|
isTrue,
|
|
reason:
|
|
'a terminal local record over a consignment the hub still '
|
|
'calls open is two systems disagreeing about whose problem a '
|
|
'parcel is, with only the rider\'s screen saying "nobody\'s"',
|
|
);
|
|
});
|
|
});
|
|
}
|