production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View 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');
});
});
}