431 lines
15 KiB
Dart
431 lines
15 KiB
Dart
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');
|
||
});
|
||
});
|
||
}
|