Files
doormile_milderapp/test/order_detail_leg_test.dart
2026-08-28 11:13:15 +05:30

431 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/bag_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<String, dynamic> order({bool withDrop = true}) => <String, dynamic>{
'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<void> openSheet(
WidgetTester tester,
Future<void> 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: [
BagLine(
stop: order(),
orderId: 'D1',
customer: 'Joe Mathew',
bag: 'Bag 1',
),
BagLine(
stop: order(),
orderId: 'D2',
customer: 'Priya Nair',
bag: '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,
bag: '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<void> pumpCard(
WidgetTester tester, {
required List<Map<String, dynamic>> stops,
void Function(Map<String, dynamic>)? onStopTap,
void Function(Map<String, dynamic>)? 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<String, dynamic> grouped(String id, String who) => <String, dynamic>{
'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 = <Map<String, dynamic>>[];
final places = <Map<String, dynamic>>[];
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 = <Map<String, dynamic>>[];
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');
});
});
}