import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/controllers/pickups_controller.dart'; import 'package:miler/data/api_config.dart'; import 'package:miler/data/load_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/data/work_repository.dart'; import 'package:miler/providers/pickup/pickup_provider.dart'; /// ───────────────────────────────────────────────────────────────────────── /// AFTER A MUTATION, THE SERVER IS ASKED — NOT ASSUMED /// /// `triggerRefresh` bumped a counter and every screen re-read *its own* copy of /// the day. So accepting a stop on Home updated Home, and Bookings and Activity /// carried on showing the state from before the tap until their own poll came /// round — and if a screen's copy was still inside its freshness window, the /// re-read returned the pre-mutation answer. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); final repo = WorkRepository.instance; final provider = PickupProvider(); late int requests; late String serverStatus; void stub() { requests = 0; MilerApi.client = MockClient((req) async { // The repository also reads `/miler/assignments` for the hub's stop // sequence — a different endpoint answering a different question. Not the // day's request, so not counted here; these assertions are about how many // times the *day* is fetched. 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': true, 'data': [ { 'bookingid': 'A', 'orderid': 'A', 'pickupcustomer': 'Sri Balaji Stores', 'pickupaddress': '12 Main Rd', 'status': serverStatus, }, ], }), 200, headers: const {'content-type': 'application/json'}, ); }); } setUp(() async { MockBackend.enabled = false; ServiceProfile.setActive(ServiceProfile.parcel); SharedPreferences.setMockInitialValues({'authtoken': 't', 'userid': 1}); await ApiConfig.setToken('t'); repo.resetForTest(); serverStatus = 'Miler_Assigned'; stub(); Get.testMode = true; }); tearDown(() { MilerApi.client = http.Client(); MockBackend.enabled = kMockBackend; repo.resetForTest(); Get.reset(); }); test( 'a mutation invalidates the shared copy, so the next read is fresh', () async { await provider.getCurrentPickups(); expect(requests, 1); // Inside the freshness window, a second read is served from the copy — // which is the behaviour that stops rebuilds duplicating requests. await provider.getCurrentPickups(); expect(requests, 1); // A mutation must break that: the next read has to reach the API, or the // screens carry on showing the state from before the tap. final controller = Get.put(PickupsController()); serverStatus = 'Picked_Up'; controller.triggerRefresh(); // Let the invalidation's own load finish. await repo.load(); expect( requests, greaterThan(1), reason: 'the mutation must send the next read to the API', ); final state = repo.state; expect(state, isA>>>()); expect( state.valueOrNull!.single['orderstatus'], 'Picked up', reason: 'the screens take the state the server confirmed', ); }, ); test( 'all three screens see the post-mutation state, not just the actor', () async { // Home reads. final home = await provider.getPickupQueues(); expect(home.single['orderstatus'], 'assigned'); serverStatus = 'Converted_To_Consignment'; await repo.invalidate(); // Bookings and Activity read afterwards and get the same new answer, // from the same object. final bookings = await provider.getCurrentPickups(); final activity = await provider.getPickupQueuesPicked(); expect(bookings.single['orderstatus'], 'picked'); expect(identical(bookings, activity), isTrue); }, ); }