import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:miler/data/miler_api.dart'; import 'package:miler/data/mock_backend.dart'; import 'package:miler/providers/pickup/pickup_provider.dart'; import 'package:miler/data/meal_run_mock.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/home/trip.dart'; import 'package:miler/views/Dashboard/pickups/stop_type.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE MEAL DAY THAT HAS NO BACKEND /// /// The meal line runs on a day held in memory, because the backend has no /// kitchens, crates or subscribers to serve. That is a deliberate stand-in, and /// it carries the risk the app has already been burned by once: a mock layer was /// deleted from this repo after demo stops kept surfacing on live riders' tabs /// weeks later. /// /// So these tests are mostly about containment — the day is complete enough to /// walk, and there is no path from a parcel login to any of it. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { MealRunMock.reset(); // The canned day is opt-in now, so the tests that exercise it have to ask // for it — exactly as a bench session does. The containment group turns it // back off for the assertions that are about the default. MockBackend.enabled = true; }); tearDown(() { MealRunMock.reset(); MockBackend.enabled = kMockBackend; ServiceProfile.setActive(ServiceProfile.parcel); }); group('containment', () { test('a parcel rider cannot reach it, whatever else is true', () { ServiceProfile.setActive(ServiceProfile.parcel); expect(MealRunMock.active, isFalse); }); test('it is off unless somebody asks for it', () { MockBackend.enabled = kMockBackend; // ── What this assertion is for ── // // This switch was off, and it was off for a real reason: while it was on, // a meal rider tapped Arrived and Picked up, watched the card advance, // and the hub console never changed — every status write was recorded on // the phone and reported as sent. // // It survives as a bench tool, but it is **opt in** now. It used to // default to on in debug and profile, which meant the default development // experience diverged from production silently: a rider signing in with // his real number became the fixture's rider, and a whole class of // contract bugs could not be seen because nothing left the device. // // Now it is off unless somebody types the flag, in every build type. expect( kMockBackend, isFalse, reason: 'the canned day must never be what you get by not deciding', ); expect(MockBackend.enabled, isFalse); // And the two are one switch, so there is no state where the day is // fictional but the statuses are real, or the other way round. expect(kMealMockEnabled, MockBackend.enabled); }); test('the meal line is still the only thing that could turn it on', () { // The containment rule survives the switch: `active` is `enabled && meal // line`, so re-enabling it for a demo still cannot reach a parcel rider. // Asserted on the guard itself, since `active` is false either way now. ServiceProfile.setActive(ServiceProfile.parcel); expect(ServiceProfile.active.sourceIsKitchen, isFalse); ServiceProfile.setActive(ServiceProfile.meals); expect(ServiceProfile.active.sourceIsKitchen, isTrue); }); test('every id announces itself as mock', () { // `purgeDemoRecords()` matches on exactly this prefix, so anything that // leaks into a rider's stores is removed at the next launch by code that // already ships. See the note in `accepted_store.dart`. ServiceProfile.setActive(ServiceProfile.meals); for (final s in MealRunMock.stops()) { expect( (s['orderid'] as String).toLowerCase(), startsWith('mock-'), reason: 'a row that does not say it is mock cannot be purged', ); } }); }); group('the day itself', () { setUp(() => ServiceProfile.setActive(ServiceProfile.meals)); test('is two kitchens, so the grouping has something to group', () { final kitchens = MealRunMock.stops() .map((s) => stopSourceName(s)) .toSet(); // One kitchen hides every question the sections exist to answer. expect(kitchens.length, 2); }); test('every stop is a drop, with a bag the rider can read off a box', () { for (final s in MealRunMock.stops()) { expect(stopKindOf(s).isDelivery, isTrue); expect(stopPrintedLabel(s), isNotEmpty); expect((s['pickupcustomer'] as String), isNotEmpty); expect((s['pickupaddress'] as String), contains('Coimbatore')); } }); test('nothing is owed at any door', () { // A subscriber paid the client by the month. A rupee figure here would be // a question the rider cannot answer. for (final s in MealRunMock.stops()) { expect(stopCollectionAmount(s), 0); } }); test('it starts undecided — the rider has done nothing yet', () { for (final s in MealRunMock.stops()) { expect(s['orderstatus'], 'assigned'); } }); test('it groups into one trip in route order', () { final trips = Trip.groupIntoTrips(MealRunMock.stops()); expect(trips, hasLength(1)); expect(trips.single.stops.map((s) => s['step']), [ 1, 2, 3, 4, 5, 6, 7, 8, 9, ]); }); }); group('walking it', () { setUp(() => ServiceProfile.setActive(ServiceProfile.meals)); test('a status change sticks, and only touches its own stop', () { MealRunMock.setStatus('MOCK-M-1001', 'accepted'); final byId = { for (final s in MealRunMock.stops()) s['orderid']: s['orderstatus'], }; expect(byId['MOCK-M-1001'], 'accepted'); expect(byId['MOCK-M-1002'], 'assigned'); }); test('the controllers key on the booking id they actually carry', () { // Every status method has an int `pickupId` and often no order id at all, // so the mock has to resolve one from the other or half the ladder writes // nothing. expect(MealRunMock.setStatusByPickupId(1002, 'arrived'), isTrue); final row = MealRunMock.stops().firstWhere( (s) => s['orderid'] == 'MOCK-M-1002', ); expect(row['orderstatus'], 'arrived'); }); test('the whole ladder runs: assigned → accepted → arrived → picked', () { const id = 'MOCK-M-1003'; String statusOf() => MealRunMock.stops().firstWhere( (s) => s['orderid'] == id, )['orderstatus'] as String; expect(statusOf(), 'assigned'); for (final rung in ['accepted', 'arrived', 'picked']) { MealRunMock.setStatus(id, rung); expect(statusOf(), rung); } }); test('reset puts the day back, so a demo can be run twice', () { MealRunMock.setStatus('MOCK-M-1001', 'picked'); MealRunMock.reset(); expect( MealRunMock.stops().every((s) => s['orderstatus'] == 'assigned'), isTrue, ); }); test('handing out rows never hands out the master copy', () { // The screens stamp compliance and proof blocks onto stop maps in place. // Sharing the originals would carry one run's leftovers into the next. MealRunMock.stops().first['pickupcustomer'] = 'SCRIBBLED'; expect(MealRunMock.stops().first['pickupcustomer'], isNot('SCRIBBLED')); }); }); // ── The wiring, not just the fixture ── // // The day above is only useful if the app actually receives it. These walk the // real path — provider, transport, auth — with no network available, which is // exactly the situation a developer running `flutter run` on a bench is in. group('the app runs on it', () { test( 'the queue provider serves the day without touching the network', () async { ServiceProfile.setActive(ServiceProfile.meals); final stops = await PickupProvider().getPickupQueues(); expect(stops, hasLength(9)); expect( stops.every((s) => (s['orderid'] as String).startsWith('MOCK-')), isTrue, reason: 'every row is self-identifying, so a leak is purged at launch', ); }, ); test('a parcel rider still gets the real call, and no fixture', () async { ServiceProfile.setActive(ServiceProfile.parcel); // The transport answers rather than the fixture: an empty booking list, // not somebody else's kitchens. Inventing parcel work is the one thing // this mock must not do. final stops = await PickupProvider().getPickupQueues(); expect(stops, isEmpty); }); test('every write is answered without a server', () async { final res = await MilerApi.pickupComplete(1001); expect(res.ok, isTrue); expect(res.status, 200); }); test('signing in works with no directory to ask', () async { expect(MockBackend.enabled, isTrue); // The profile the mock hands back is what puts the build on the meal // line, so the login and the day agree about which app the rider is in. expect(MockBackend.rider['tenantname'], 'DailyGrubs'); }); }); }