production
This commit is contained in:
430
test/order_detail_leg_test.dart
Normal file
430
test/order_detail_leg_test.dart
Normal file
@@ -0,0 +1,430 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user