import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/home/pickup_preview_sheet.dart'; import 'package:miler/views/Dashboard/home/stop_detail_sheet.dart'; import 'package:miler/views/Dashboard/home/trip.dart'; import 'package:miler/views/Dashboard/home/trip_card.dart'; import 'package:miler/data/order_manifest.dart'; import 'package:miler/views/helpers/widgets/miler_sheet_kit.dart'; /// ───────────────────────────────────────────────────────────────────────── /// WHICH LEG A SHEET IS ABOUT, AND HOW TALL IT IS /// /// ── Two surfaces, two subjects ── /// /// Home's kitchen card and the order rows inside it answer different /// questions, and both used to answer the first one. /// /// Every order under a counter shares that counter's address, so a sheet /// opened from an order row that pinned the counter drew the identical map /// once per order and said nothing about the order the rider actually tapped. /// What distinguishes one order from the next is the **door it is going to**. /// /// The place is not left without a map — it has a better one, on the preview /// behind the card's third utility, which draws the whole leg from where the /// rider is standing to the counter with the manifest waiting on it. /// /// ── Why the payload's own `type` could not decide it ── /// /// `ApiConfig.pickupFromBooking` stamps `type: pickup` on every row it builds, /// because a v1 booking *is* a first-mile pickup and the backend has no /// per-stop type to send. So the sheet's `_isDropLeg` — which reads that type /// through `MilkRun.workingKind` — answered "pickup" for every uncollected /// meal order, and the customer's sheet showed the kitchen. The caller knows /// what it opened the sheet for; the payload does not. Hence /// [StopDetailSheet.ofOrder]. /// /// ── And the sheet reserved space it did not use ── /// /// It was a `DraggableScrollableSheet` at `initialChildSize: 0.78`, which /// claims 78% of the screen whether or not it has 78% of a screen to say. A /// meal order ended at its CONTACT tile with a third of the sheet left over as /// blank paper. It measures its content now, capped at 86%. /// ───────────────────────────────────────────────────────────────────────── void main() { tearDown(() { Get.reset(); ServiceProfile.setActive(ServiceProfile.parcel); }); const kitchenStreet = '9 Kitchen Street, RS Puram, Coimbatore 641002'; const customerStreet = '12, SNS Colony, Peelamedu, Coimbatore 641004'; /// An order exactly as the adapter hands it over — including the `pickup` /// type it stamps on every row, which is the shape that broke. Map order({bool withDrop = true}) => { 'orderid': 'D1', 'pickupid': 1001, 'step': 1, 'kitchenname': 'Vidhya Kitchen', 'sourceid': 'K1', 'pickupcustomer': 'Joe Mathew', 'pickupcontactno': '9787698259', 'pickupaddress': kitchenStreet, 'pickuplat': 11.0168, 'pickuplon': 76.9558, if (withDrop) 'dropaddress': customerStreet, if (withDrop) 'droplat': 11.0208, if (withDrop) 'droplon': 76.9598, 'orderstatus': 'accepted', 'type': 'pickup', 'baglabel': 'Bag 1', 'Quantity': 1, }; Future openSheet( WidgetTester tester, Future Function(BuildContext) open, { double height = 844, }) async { ServiceProfile.setActive(ServiceProfile.milkMan); tester.view.physicalSize = Size(390 * 3, height * 3); tester.view.devicePixelRatio = 3; addTearDown(tester.view.reset); // Tear down whatever was mounted before, routes included. Without this a // test that opens the sheet twice measures the *first* one both times — // the old route is still on the navigator, and `.first` finds it. await tester.pumpWidget(const SizedBox.shrink()); await tester.pumpAndSettle(); late BuildContext ctx; await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold( body: Builder( builder: (c) { ctx = c; return const SizedBox.expand(); }, ), ), ), ), ); unawaited(open(ctx)); await tester.pumpAndSettle(const Duration(seconds: 1)); } group('an order points at its own door', () { testWidgets('opened from an order row, the sheet shows the delivery', ( tester, ) async { await openSheet( tester, (c) => StopDetailSheet.show( c, stop: order(), stopNumber: 1, ofOrder: true, riderLat: 11.0168, riderLng: 76.9558, ), ); expect(find.text('DELIVER TO'), findsOneWidget); expect(find.text(customerStreet), findsOneWidget); expect( find.text(kitchenStreet), findsNothing, reason: 'the counter is the card\'s subject, not the order\'s', ); }); testWidgets('opened for a place, the sheet still shows the place', ( tester, ) async { // A flat group's header IS a location — one stop, one counter — so // nothing about that case changes. await openSheet( tester, (c) => StopDetailSheet.show(c, stop: order(), stopNumber: 1), ); expect(find.text('PICKUP LOCATION'), findsOneWidget); expect(find.text(kitchenStreet), findsOneWidget); expect(find.text(customerStreet), findsNothing); }); testWidgets('a row with no drop falls back without lying about it', ( tester, ) async { // The half-populated payload. What must never happen is `DELIVER TO` // printed over a kitchen's street: the eyebrow, the street and the pin // are resolved together, so falling back to the pickup means it stops // calling itself a drop. await openSheet( tester, (c) => StopDetailSheet.show( c, stop: order(withDrop: false), stopNumber: 1, ofOrder: true, ), ); expect(find.text('DELIVER TO'), findsNothing); expect(find.text('PICKUP LOCATION'), findsOneWidget); expect(find.text(kitchenStreet), findsOneWidget); }); }); group('the place keeps its own leg, on the preview', () { testWidgets('the preview is where the counter is drawn', (tester) async { // The pickup did not disappear when the order sheet stopped showing it — // it moved to the surface that is actually about the place, behind the // card's third utility, where it comes with the manifest. await openSheet( tester, (c) => PickupPreviewSheet.show( c, placeName: 'Vidhya Kitchen', lines: [ ManifestLine( stop: order(), orderId: 'D1', customer: 'Joe Mathew', label: 'Bag 1', ), ManifestLine( stop: order(), orderId: 'D2', customer: 'Priya Nair', label: 'Bag 2', ), ], meters: 3100, ride: const Duration(minutes: 9), riderLat: 11.0168, riderLng: 76.9558, onNavigate: () {}, onCall: () {}, ), ); expect(find.text('Vidhya Kitchen'), findsOneWidget); expect(find.text('Joe Mathew'), findsOneWidget); expect(find.text('Priya Nair'), findsOneWidget); }); }); group('the sheet is as tall as what is on it', () { Finder scaffold() => find.byType(MilerSheetScaffold).first; Finder viewport() => find .descendant( of: scaffold(), matching: find.byType(SingleChildScrollView), ) .first; Finder content() => find.descendant(of: viewport(), matching: find.byType(Column)).first; testWidgets('a short order reserves no blank paper below it', ( tester, ) async { await openSheet( tester, (c) => StopDetailSheet.show( c, stop: order(), stopNumber: 1, ofOrder: true, riderLat: 11.0168, riderLng: 76.9558, ), ); // ── The measurement that actually names the complaint ── // // "Shorter than before" is the wrong assertion: a content-sized sheet is // sometimes *taller* than the fixed 78% it replaced, because it stopped // hiding the last tile behind a scroll. What was wrong was the gap — the // list's viewport was 78% of the screen while the rows inside it ended a // third of the way up it, and the difference was blank paper. // // So: the scroll viewport is exactly as tall as what is inside it. That // is true of a content-sized sheet at any height, and false of a fixed // one at every height short of full. expect( tester.getSize(viewport()).height, tester.getSize(content()).height, reason: 'a viewport taller than its content IS the blank space', ); }); testWidgets('the sheet grows and shrinks with what is on it', ( tester, ) async { await openSheet( tester, (c) => StopDetailSheet.show(c, stop: order(), stopNumber: 1), ); final plain = tester.getSize(scaffold()).height; await openSheet( tester, (c) => StopDetailSheet.show( c, stop: order()..['notes'] = 'Gate code 4411, ring the watchman.', stopNumber: 1, ), ); final withNote = tester.getSize(scaffold()).height; expect( withNote, greaterThan(plain), reason: 'a fixed 0.78 would have reported the same number twice', ); }); testWidgets('a long order is capped, and scrolls rather than overflows', ( tester, ) async { final long = order() ..['pickupcustomer'] = 'Venkataraman Subramanian Balasubramanian' ..['notes'] = 'Gate code 4411, leave with security if unreachable. Second ' 'building on the left after the temple, ask for the watchman at ' 'the side entrance, the main gate is locked after nine, and the ' 'lift stops running at eleven so use the back stairs.' ..['dropaddress'] = 'Flat 14B, Second Floor, Sri Venkateswara Residency, 12 SNS ' 'Colony Extension, Behind the Water Tank, Peelamedu, Coimbatore, ' 'Tamil Nadu 641004'; await openSheet( tester, (c) => StopDetailSheet.show( c, stop: long, stopNumber: 7, ofOrder: true, label: 'Bag 1', ), ); expect(tester.takeException(), isNull); // The ceiling keeps the scrim — and the route behind it — visible at the // top of every phone. expect(tester.getSize(scaffold()).height, lessThanOrEqualTo(844 * 0.87)); // And the overflow goes to the scroll view rather than off the sheet. expect( tester.getSize(content()).height, greaterThan(tester.getSize(viewport()).height), ); expect(find.text('DELIVER TO'), findsOneWidget); }); }); group('the timeline routes the two taps to different handlers', () { Future pumpCard( WidgetTester tester, { required List> stops, void Function(Map)? onStopTap, void Function(Map)? onOrderTap, }) async { ServiceProfile.setActive(ServiceProfile.milkMan); tester.view.physicalSize = const Size(390 * 3, 1400 * 3); tester.view.devicePixelRatio = 3; addTearDown(tester.view.reset); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold( body: SingleChildScrollView( child: TripCard( trip: Trip.fromStops(id: 'T1', stops: stops), onStopTap: onStopTap, onOrderTap: onOrderTap, onToggleSelect: (_) {}, ), ), ), ), ), ); await tester.pumpAndSettle(); } /// The `Bag n ›` block — the one control on an order row that opens its /// sheet, found by the promise it makes to a screen reader. Finder openBlock() => find.byWidgetPredicate( (w) => w is Semantics && w.properties.hint == 'Opens order details', ); Map grouped(String id, String who) => { 'orderid': id, 'pickupid': int.parse(id), 'step': int.parse(id) - 1000, 'kitchenname': 'Vidhya Kitchen', 'sourceid': 'K1', 'pickupcustomer': who, 'pickupaddress': kitchenStreet, 'dropaddress': customerStreet, 'orderstatus': 'accepted', 'type': 'pickup', 'pickuplat': 11.01, 'pickuplon': 76.95, }; testWidgets('tapping an order row calls onOrderTap, not onStopTap', ( tester, ) async { final orders = >[]; final places = >[]; await pumpCard( tester, stops: [grouped('1001', 'Joe Mathew'), grouped('1002', 'Priya Nair')], onStopTap: places.add, onOrderTap: orders.add, ); // The group is open by default — it is the one the rider is working. expect(find.text('Joe Mathew'), findsOneWidget); // The name is the *selection* target; the way into an order's sheet is // the `Bag 1 ›` block at the end of its row. One gesture, one meaning — // see the note on `_OrderOpenBlock`. await tester.tap(openBlock().first); await tester.pumpAndSettle(); expect(orders.length, 1, reason: 'an order row is about one order'); expect(orders.single['orderid'], '1001'); expect(places, isEmpty, reason: 'the place has its own handler'); }); testWidgets('onOrderTap falls back to onStopTap when unset', ( tester, ) async { // A caller that does not care keeps the old single-callback behaviour, // so nothing outside Home has to know this split exists. final places = >[]; await pumpCard( tester, stops: [grouped('1001', 'Joe Mathew'), grouped('1002', 'Priya Nair')], onStopTap: places.add, ); await tester.tap(openBlock().first); await tester.pumpAndSettle(); expect(places.length, 1); expect(places.single['orderid'], '1001'); }); }); }