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>
141 lines
5.2 KiB
Dart
141 lines
5.2 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/miler_api.dart';
|
|
import 'package:miler/data/mock_backend.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
import 'package:miler/data/work_repository.dart';
|
|
import 'package:miler/providers/pickup/pickup_provider.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// FOUR SCREENS, ONE REQUEST
|
|
///
|
|
/// Home, Bookings, Activity and the post-delivery screen all read the day, and
|
|
/// each produced its own `GET /miler/bookings` — on mount, on tab switch, and
|
|
/// on every poll. Four timers, four copies, four ideas of what had been
|
|
/// accepted, reconciled only by whichever refreshed last.
|
|
///
|
|
/// The provider still returns a list or throws, which is what every call site
|
|
/// was written against. What changed is that they now share one request.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late int requests;
|
|
final provider = PickupProvider();
|
|
|
|
void stub({List<Map<String, dynamic>> rows = const [], int code = 200}) {
|
|
requests = 0;
|
|
MilerApi.client = MockClient((req) async {
|
|
// The repository also reads `/miler/assignments` for the hub's stop
|
|
// sequence; it is not the day's request and is not counted. See the same
|
|
// note in `work_repository_test.dart`.
|
|
if (req.url.path.endsWith('/miler/assignments')) {
|
|
return http.Response(
|
|
json.encode({'success': true, 'data': const []}),
|
|
200,
|
|
headers: const {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
requests++;
|
|
return http.Response(
|
|
json.encode({'success': code < 400, 'data': rows}),
|
|
code,
|
|
headers: const {'content-type': 'application/json'},
|
|
);
|
|
});
|
|
}
|
|
|
|
Map<String, dynamic> booking(String id) => {
|
|
'bookingid': id,
|
|
'orderid': id,
|
|
'pickupcustomer': 'Stop $id',
|
|
'pickupaddress': '12 Main Rd',
|
|
'status': 'Miler_Assigned',
|
|
};
|
|
|
|
setUp(() async {
|
|
MockBackend.enabled = false;
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
SharedPreferences.setMockInitialValues({'authtoken': 't'});
|
|
await ApiConfig.setToken('t');
|
|
WorkRepository.instance.resetForTest();
|
|
stub(rows: [booking('A')]);
|
|
});
|
|
|
|
tearDown(() {
|
|
MilerApi.client = http.Client();
|
|
MockBackend.enabled = kMockBackend;
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
WorkRepository.instance.resetForTest();
|
|
});
|
|
|
|
test('the three screen entry points share one request', () async {
|
|
final results = await Future.wait([
|
|
provider.getPickupQueues(),
|
|
provider.getCurrentPickups(),
|
|
provider.getPickupQueuesPicked(),
|
|
]);
|
|
|
|
expect(requests, 1, reason: 'three screens, one GET /miler/bookings');
|
|
for (final r in results) {
|
|
expect(r, hasLength(1));
|
|
}
|
|
});
|
|
|
|
test('a tab switch moments later does not re-fetch', () async {
|
|
await provider.getCurrentPickups();
|
|
await provider.getPickupQueuesPicked();
|
|
expect(requests, 1);
|
|
});
|
|
|
|
test(
|
|
'every caller gets the same objects, so no screen has its own copy',
|
|
() async {
|
|
final a = await provider.getCurrentPickups();
|
|
final b = await provider.getPickupQueuesPicked();
|
|
expect(identical(a, b), isTrue);
|
|
},
|
|
);
|
|
|
|
test('an empty day is an empty list, not an error', () async {
|
|
stub(rows: const []);
|
|
expect(await provider.getCurrentPickups(), isEmpty);
|
|
});
|
|
|
|
// ── The milk-man line is dispatched, not written off ──
|
|
//
|
|
// This expected a [LineNotServedException] after zero requests, on the belief
|
|
// that the backend had no endpoint for that line. It has the same one: the
|
|
// console assigns the round on /admin/bookings and the rider reads it back
|
|
// from /miler/bookings. While the two disagreed, a rider on tenant 13 with a
|
|
// booking assigned to him got an empty screen and no error to explain it.
|
|
test('a milk-man rider is asked for, not refused', () async {
|
|
stub(rows: [booking('MILK-1')]);
|
|
ServiceProfile.setActive(ServiceProfile.milkMan);
|
|
WorkRepository.instance.resetForTest();
|
|
|
|
final day = await provider.getCurrentPickups();
|
|
expect(day, hasLength(1));
|
|
expect(requests, 1, reason: 'his work has to be asked for to arrive');
|
|
});
|
|
|
|
test('a server failure throws rather than reading as an empty day', () async {
|
|
stub(code: 500);
|
|
await expectLater(provider.getCurrentPickups(), throwsA(isA<Exception>()));
|
|
});
|
|
|
|
// A filtered read is a different question and must not be served the shared
|
|
// copy of the whole day.
|
|
test('a status-filtered read gets its own request', () async {
|
|
await provider.getCurrentPickups();
|
|
await provider.getPickupQueues(orderstatus: 'Picked_Up');
|
|
expect(requests, 2);
|
|
});
|
|
}
|