import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/home/trip.dart'; import 'package:miler/views/Dashboard/home/trip_card.dart'; import 'package:miler/views/Dashboard/home/trip_progress_rail.dart'; import 'package:miler/views/Dashboard/pickups/stop_type.dart'; /// ───────────────────────────────────────────────────────────────────────── /// ONE APP, TWO DAYS /// /// The whole merge rests on one promise, in both directions: /// /// • A rider whose login carries a **meal tenant id** gets the meal process — /// kitchen loading, deliveries, no money, no return leg. /// • A rider **without** one gets the parcel app exactly as it was: hub to hub, /// bookings, cash, per-stop decisions. /// /// The second half of that is the one that can break silently. Nobody files a /// bug saying "the meal build looks fine"; a parcel rider losing his cash screen /// or gaining a kitchen heading is found at a customer's door. So every meal /// assertion below has a parcel one beside it. /// ───────────────────────────────────────────────────────────────────────── void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() => SharedPreferences.setMockInitialValues({})); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); Map stop({ required String id, String status = 'ASSIGNED', String kitchen = '', num cash = 0, }) => { 'orderid': id, 'step': int.parse(id), 'pickupcustomer': 'Customer $id', 'pickupaddress': '$id Main Road, RS Puram', 'orderstatus': status, if (kitchen.isNotEmpty) 'kitchenname': kitchen, if (kitchen.isNotEmpty) 'baglabel': 'BAG-$id', if (cash > 0) 'collectionamt': cash, 'pickuplat': 13.08, 'pickuplon': 80.21, }; Future pumpRoute( WidgetTester tester, { required List> stops, }) async { tester.view.physicalSize = const Size(1080, 2600); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final trip = Trip.fromStops(id: 't', stops: stops); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold( body: SingleChildScrollView( child: TripCard( trip: trip, onToggleSelect: (_) {}, onViewRoute: () {}, ).buildStops(), ), ), ), ), ); await tester.pumpAndSettle(); } // ══════════════════════════════════════════════════════════════════════ // THE MEAL LOGIN — what a tenant id buys // ══════════════════════════════════════════════════════════════════════ group('a rider who logs in on a meal tenant', () { setUp(() => ServiceProfile.setActive(ServiceProfile.meals)); testWidgets('starts at a kitchen, and never comes back to a hub', ( tester, ) async { await pumpRoute( tester, stops: [ stop(id: '1', kitchen: 'Kitchen 1'), stop(id: '2', kitchen: 'Kitchen 1'), ], ); // ── Ported to the timeline ── // // `START · KITCHEN` was the hub row's label, and the hub row is gone with // the cards: the run opens on one line-agnostic head instead. What that // row was really holding is the half below, which is unchanged — a meal // run has no return leg. expect(find.text('TODAY’S RUN'), findsOneWidget); // His day ends at the last subscriber's door. A return leg to a warehouse // he has never seen is an un-completable step at the end of a route he // has in fact finished. expect(find.text('RETURN · HUB'), findsNothing); expect(find.text('START · HUB'), findsNothing); }); testWidgets('sees his route grouped under the kitchen it comes from', ( tester, ) async { await pumpRoute( tester, stops: [ stop(id: '1', kitchen: 'Kitchen 1'), stop(id: '2', kitchen: 'Kitchen 2'), ], ); // Two kitchens, two headings — the shape of a meal day, which a flat list // hides until he opens every card. expect(find.text('Kitchen 1'), findsOneWidget); expect(find.text('Kitchen 2'), findsOneWidget); // And the bag, which is what he checks against the box in his hand. // The label the kitchen printed, as [BagManifest] hands it over — the // `Bag ` prefix belonged to the card's chip and went with it, which // stopped a printed `BAG-1` reading as `Bag BAG-1`. expect(find.text('BAG-1'), findsOneWidget); }); test('works deliveries, not bookings', () { final p = ServiceProfile.active; expect(p.workTabLabel, 'Deliveries'); expect(p.jobNounPlural, 'deliveries'); expect(p.completionVerb, 'Delivered'); }); test('hands a subscriber food — he does not collect from one', () { // Every stop after the kitchen is a drop, whether or not the payload // bothered to label it. expect(stopKindOf(stop(id: '1')).isDelivery, isTrue); }); test('is never asked for money, however the payload is priced', () { // Subscribers pay the client monthly. A rupee figure at the door is a // question the rider cannot answer and must not be asked. expect(stopCollectionAmount(stop(id: '1', cash: 450)), 0); expect(ServiceProfile.active.collectsCash, isFalse); }); test('takes the whole assignment, not stop by stop', () { // He cannot decline one subscriber's lunch. expect(ServiceProfile.active.acceptsPerStop, isFalse); }); test('photographs the crate once, at the counter', () { expect(ServiceProfile.active.bulkLoadProof, isTrue); }); test('keeps the order on Home until it is physically in his box', () { // Accepting changes nothing physical — the food is still in a kitchen he // has not reached. expect(ServiceProfile.active.handsOffAtCollection, isTrue); }); }); // ══════════════════════════════════════════════════════════════════════ // THE PARCEL LOGIN — nothing moved // ══════════════════════════════════════════════════════════════════════ group('a rider who logs in without a meal tenant', () { setUp(() => ServiceProfile.setActive(ServiceProfile.parcel)); testWidgets('gets the hub-to-hub route he has always had', (tester) async { await pumpRoute( tester, stops: [ stop(id: '1'), stop(id: '2'), ], ); // The parcel day is still a loop: he leaves the hub empty, fills up at // customer doors and comes back. The head is shared with the meal run; // the closing leg is what tells the two apart. expect(find.text('TODAY’S RUN'), findsOneWidget); expect(find.text('RETURN · HUB'), findsOneWidget); expect(find.text('START · KITCHEN'), findsNothing); }); testWidgets('gets no kitchen sections and no meal chrome', (tester) async { // Nothing from the meal run may leak onto a parcel route. The *source // pill* is data-driven and stays if a parcel payload ever names one — // see [StopSourceLine] — but the grouping, its counts and its bulk // collect are meal-flow structure and must not appear. await pumpRoute( tester, stops: [stop(id: '1', kitchen: 'Kitchen 1')], ); expect(find.text('1 meal · 1 to accept'), findsNothing); expect(find.textContaining('ready to collect'), findsNothing); expect(find.text('MEAL'), findsNothing); // The stop is drawn — it is just drawn flat. A parcel route has no // counter to group under, so the timeline is one node per stop and the // header *is* the stop. `PICKUP` was the card's type mark; the type is // progressive disclosure in the detail sheet now. expect(find.text('Customer 1'), findsOneWidget); expect(find.text('Kitchen 1'), findsNothing); // "Collect 1" on the meta line is the parcel card's own count of what he // is picking up at this door, and stays. The heading assertions above are // what tell the two apart. }); test('still works bookings', () { final p = ServiceProfile.active; expect(p.workTabLabel, 'Bookings'); expect(p.jobNounPlural, 'bookings'); expect(p.completionVerb, 'Picked up'); }); test('still collects the cash the stop is priced at', () { // The failure this app cannot afford: a rider at a door with no way to // record the money he is holding. expect(stopCollectionAmount(stop(id: '1', cash: 450)), 450); expect(ServiceProfile.active.collectsCash, isTrue); }); test('still decides stop by stop', () { expect(ServiceProfile.active.acceptsPerStop, isTrue); }); test('an unlabelled stop is still a pickup', () { expect(stopKindOf(stop(id: '1')), StopKind.pickup); }); }); // ══════════════════════════════════════════════════════════════════════ // THE SWITCH ITSELF // ══════════════════════════════════════════════════════════════════════ group('what decides which day he gets', () { Future loginWith(Map prefs) async { SharedPreferences.setMockInitialValues(prefs); final p = await resolveServiceProfile(); ServiceProfile.setActive(p); return p; } test('a recognised meal tenant name puts him on the meal line', () async { final p = await loginWith({'tenantname': 'DailyGrubs'}); expect(p.line, ServiceLine.milkMan); }); test( 'no tenant, an unknown tenant, or a blank one is a parcel day', () async { expect((await loginWith({})).line, ServiceLine.parcel); expect((await loginWith({'tenantid': 4242})).line, ServiceLine.parcel); expect((await loginWith({'tenantname': ''})).line, ServiceLine.parcel); }, ); test('the fallback direction is the safe one', () async { // Wrong-way round, a live parcel rider loses the screen that records a // customer's cash. This way, a meal rider sees a payment prompt he can // dismiss. final p = await loginWith({'tenantname': 'something-nobody-configured'}); expect(p.collectsCash, isTrue); }); }); // ── The round does not end at a warehouse ── // // A logistics day is a loop: leave the hub empty, fill up at customer doors, // return with the consignments — so "then hub" is the true last leg and the // rider is not finished until he is there. A meal rider finishes at the last // subscriber's door and goes home. Telling him his round ends at a building he // has never visited is the same fiction the RETURN · HUB row was removed from // Home for, and it survived on the progress rail. group("the progress rail speaks each line's own language", () { Future subtitle(WidgetTester tester) async { tester.view.physicalSize = const Size(1080, 1200); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold( body: TripProgressRail( progress: const [ StopProgress.done, StopProgress.current, StopProgress.pending, ], slotLabel: 'Trip 1', remaining: const Duration(minutes: 25), ), ), ), ), ); await tester.pumpAndSettle(); return tester .widgetList(find.byType(Text)) .map((t) => t.data ?? '') .firstWhere((d) => d.contains('left'), orElse: () => ''); } testWidgets('a meal round just ends', (tester) async { ServiceProfile.setActive(ServiceProfile.meals); final line = await subtitle(tester); expect(line, contains('left')); expect( line, isNot(contains('hub')), reason: 'he goes home from the last door, not to a warehouse', ); }); testWidgets('a parcel day is a loop, and says so', (tester) async { ServiceProfile.setActive(ServiceProfile.parcel); final line = await subtitle(tester); expect(line, contains('then hub')); }); }); }