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> 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 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())); }); // 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); }); }