device infos
@@ -1199,7 +1199,7 @@ void main() {
|
||||
);
|
||||
expect(find.textContaining('0 m'), findsNothing);
|
||||
// The load still reads, so the node degrades rather than emptying.
|
||||
expect(find.text('5 bags'), findsOneWidget);
|
||||
expect(find.text('5 orders'), findsOneWidget);
|
||||
});
|
||||
|
||||
// ── The destination never truncates first ──
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/data/stop_area.dart';
|
||||
import 'package:miler/views/Dashboard/home/homepage_banner.dart';
|
||||
import 'package:miler/views/Dashboard/pickups/pickups.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE WORK QUEUE, NOT A LIST OF CARDS
|
||||
@@ -96,22 +95,19 @@ void main() {
|
||||
await ready(tester);
|
||||
const inset = 34.0;
|
||||
|
||||
// Live stop: the banner is up, and the floor clears the banner.
|
||||
// ── One surface, where there were two ──
|
||||
//
|
||||
// This case used to be a three-way check, because a **Start delivery**
|
||||
// bar shared this corner with the live strip. That bar is gone — the
|
||||
// release it made now happens per stop, when the rider slides Start ride
|
||||
// on the map, which is the moment he actually sets off. See `startRound`.
|
||||
//
|
||||
// What the rule protects is unchanged: the floor reserves exactly what is
|
||||
// floating over the list, plus the device inset.
|
||||
expect(
|
||||
bottomReservationFor(inset: inset, live: true, unreleased: true),
|
||||
bottomReservationFor(inset: inset, live: true),
|
||||
inset + ActivePickupBanner.maxHeight,
|
||||
);
|
||||
// A live stop suppresses the start bar, so the banner wins even when
|
||||
// there is unreleased work — the two never stack.
|
||||
expect(
|
||||
bottomReservationFor(inset: inset, live: true, unreleased: false),
|
||||
inset + ActivePickupBanner.maxHeight,
|
||||
);
|
||||
// Nothing running, load in hand: the start bar.
|
||||
expect(
|
||||
bottomReservationFor(inset: inset, live: false, unreleased: true),
|
||||
inset + StartRoundBar.maxHeight,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the device inset is always counted', (tester) async {
|
||||
@@ -120,16 +116,8 @@ void main() {
|
||||
// inset, so on a gesture-navigation phone the last row of the run sat
|
||||
// behind the banner *and* behind the navigation.
|
||||
for (final live in const [true, false]) {
|
||||
final withInset = bottomReservationFor(
|
||||
inset: 34,
|
||||
live: live,
|
||||
unreleased: true,
|
||||
);
|
||||
final without = bottomReservationFor(
|
||||
inset: 0,
|
||||
live: live,
|
||||
unreleased: true,
|
||||
);
|
||||
final withInset = bottomReservationFor(inset: 34, live: live);
|
||||
final without = bottomReservationFor(inset: 0, live: live);
|
||||
expect(withInset - without, 34);
|
||||
}
|
||||
});
|
||||
@@ -137,98 +125,29 @@ void main() {
|
||||
testWidgets('an idle queue reserves almost nothing', (tester) async {
|
||||
await ready(tester);
|
||||
expect(
|
||||
bottomReservationFor(inset: 0, live: false, unreleased: false),
|
||||
lessThan(StartRoundBar.maxHeight),
|
||||
bottomReservationFor(inset: 0, live: false),
|
||||
lessThan(ActivePickupBanner.maxHeight),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('both surfaces declare a real height', (tester) async {
|
||||
testWidgets('the floating surface declares a real height', (tester) async {
|
||||
await ready(tester);
|
||||
// Measured, not guessed. Each is the only thing that knows how much of
|
||||
// the list it covers.
|
||||
// Measured, not guessed. It is the only thing that knows how much of the
|
||||
// list it covers.
|
||||
expect(ActivePickupBanner.maxHeight, greaterThan(48));
|
||||
expect(StartRoundBar.maxHeight, greaterThan(48));
|
||||
});
|
||||
});
|
||||
|
||||
group('the command surface', () {
|
||||
Future<void> pumpBar(
|
||||
WidgetTester tester, {
|
||||
double width = 390,
|
||||
double textScale = 1.0,
|
||||
int count = 5,
|
||||
}) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
tester.view.physicalSize = Size(width * 3, 844 * 3);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => MaterialApp(
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).copyWith(textScaler: TextScaler.linear(textScale)),
|
||||
child: child!,
|
||||
),
|
||||
home: Scaffold(
|
||||
backgroundColor: ColorConstants.daylightSurface,
|
||||
body: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: StartRoundBar(
|
||||
count: count,
|
||||
busy: false,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('it says what is in hand and what the press does', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpBar(tester);
|
||||
// Setting off is reversible and operational, so it is a press — unlike
|
||||
// Picked and Delivered, which are assertions about custody.
|
||||
expect(find.textContaining('Start'), findsOneWidget);
|
||||
});
|
||||
|
||||
for (final (width, scale) in const [
|
||||
(390.0, 1.0),
|
||||
(390.0, 1.3),
|
||||
(390.0, 2.0),
|
||||
(375.0, 2.0),
|
||||
(320.0, 1.0),
|
||||
(320.0, 2.0),
|
||||
]) {
|
||||
testWidgets('${width.toInt()}px @ ${scale}x lays out clean', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpBar(tester, width: width, textScale: scale, count: 12);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('it never wraps itself in a second SafeArea', (tester) async {
|
||||
await pumpBar(tester);
|
||||
// `BottomPage.bottomInset` is the one owner of the device inset on this
|
||||
// screen. A `SafeArea` here would count it twice and float the bar a
|
||||
// navigation bar's height off the bottom of the screen.
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(StartRoundBar),
|
||||
matching: find.byType(SafeArea),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
});
|
||||
// ── The "command surface" group is gone with its control ──
|
||||
//
|
||||
// It covered the **Start delivery** bar: what it said, how it laid out at six
|
||||
// widths and text scales, and that it never double-counted the device inset.
|
||||
// The bar was removed at the rider's request — the release it made is made
|
||||
// per stop now, by the Start ride slider on the map, at the moment he
|
||||
// actually sets off. See `startRound` for the whole argument.
|
||||
//
|
||||
// Nothing replaced it in that corner, so there is nothing here to test. The
|
||||
// reservation rule above still pins the geometry the corner has to keep.
|
||||
}
|
||||
|
||||
/// Real addresses from the live tenant, added after seeing them render on a
|
||||
|
||||
@@ -48,7 +48,7 @@ void main() {
|
||||
String id,
|
||||
String customer, {
|
||||
String status = 'picked',
|
||||
String bag = '',
|
||||
String label = '',
|
||||
}) => {
|
||||
'orderid': id,
|
||||
'tripid': 'T1',
|
||||
@@ -64,7 +64,7 @@ void main() {
|
||||
'orderstatus': status,
|
||||
'pickupcontactno': '9787698259',
|
||||
'deliverycontactno': '9787698259',
|
||||
if (bag.isNotEmpty) 'baglabel': bag,
|
||||
if (label.isNotEmpty) 'baglabel': label,
|
||||
'Quantity': 1,
|
||||
'pickuplat': 11.0168,
|
||||
'pickuplon': 76.9558,
|
||||
@@ -191,10 +191,10 @@ void main() {
|
||||
tester,
|
||||
'card_picked',
|
||||
PickupCard(
|
||||
item: drop('D1', 'Joe Mathew', bag: 'Bag 8'),
|
||||
item: drop('D1', 'Joe Mathew', label: 'DG-8'),
|
||||
displayStep: 7,
|
||||
distanceMeters: 1100,
|
||||
bag: 'Bag 8',
|
||||
label: 'DG-8',
|
||||
collectedIds: const {'D1'},
|
||||
),
|
||||
height: 320,
|
||||
@@ -206,10 +206,10 @@ void main() {
|
||||
tester,
|
||||
'card_active',
|
||||
PickupCard(
|
||||
item: drop('D1', 'Joe Mathew', bag: 'Bag 8'),
|
||||
item: drop('D1', 'Joe Mathew', label: 'DG-8'),
|
||||
displayStep: 7,
|
||||
distanceMeters: 1100,
|
||||
bag: 'Bag 8',
|
||||
label: 'DG-8',
|
||||
collectedIds: const {'D1'},
|
||||
outForDeliveryIds: const {'D1'},
|
||||
),
|
||||
|
||||
@@ -2,25 +2,35 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/data/accepted_store.dart';
|
||||
import 'package:miler/data/bag_manifest.dart';
|
||||
import 'package:miler/data/order_manifest.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE DELIVERY QUEUE, AND THE BAG THAT SURVIVES IT
|
||||
/// THE DELIVERY QUEUE, AND THE LABEL THAT SURVIVES IT
|
||||
///
|
||||
/// Two promises are made at a kitchen counter and both have to hold all the way
|
||||
/// to a doorstep:
|
||||
///
|
||||
/// 1. **Five orders picked is five deliveries** — no order goes missing in the
|
||||
/// hand-off, and nothing appears that was not collected.
|
||||
/// 2. **Joe is Bag 1 and stays Bag 1** — even after Joe is delivered and the
|
||||
/// list underneath him shrinks.
|
||||
/// 2. **A label recorded at the counter is the label at the door** — even
|
||||
/// after the first order is delivered and the list underneath it shrinks,
|
||||
/// and even after a second counter loads on top of it.
|
||||
///
|
||||
/// The second one is the subtle one, and it is why bag identity is *stored* at
|
||||
/// pickup rather than recomputed from whatever is on screen. A position-derived
|
||||
/// bag number silently renames every remaining order the moment one leaves the
|
||||
/// list, which sends the rider looking for a bag that is already in somebody's
|
||||
/// The second is the subtle one, and it is why the label is *stored* at pickup
|
||||
/// rather than recomputed from whatever is on screen. Anything derived from a
|
||||
/// position silently renames every remaining order the moment one leaves the
|
||||
/// list, which sends the rider looking for something already in somebody's
|
||||
/// hallway.
|
||||
///
|
||||
/// ── The derived numbering is gone, and these tests changed with it ──
|
||||
///
|
||||
/// They used to assert `Bag 1 … Bag 5`, manufactured from each order's position
|
||||
/// when the payload printed nothing — the app numbering its own list and
|
||||
/// presenting the result as a physical fact. Only a label a counter actually
|
||||
/// printed is stored now, so the fixtures carry real codes and a counter that
|
||||
/// prints nothing stores nothing. The store's contract is unchanged and is
|
||||
/// still what is pinned here.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -28,47 +38,50 @@ void main() {
|
||||
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
||||
|
||||
Map<String, dynamic> order(String id, String customer) => {
|
||||
/// [printed] is what the counter stuck on the order. Omitted, the counter
|
||||
/// printed nothing — which is the common route, and stores nothing.
|
||||
Map<String, dynamic> order(String id, String customer, {String? printed}) => {
|
||||
'orderid': id,
|
||||
'pickupcustomer': customer,
|
||||
'kitchenname': 'Vidhya Kitchen',
|
||||
'kitchenid': 'K1',
|
||||
'quantity': 3,
|
||||
if (printed != null) 'baglabel': printed,
|
||||
};
|
||||
|
||||
final five = [
|
||||
order('1', 'Joe'),
|
||||
order('2', 'Arun'),
|
||||
order('3', 'Priya'),
|
||||
order('4', 'Kumar'),
|
||||
order('5', 'Ravi'),
|
||||
order('1', 'Joe', printed: 'DG-1001'),
|
||||
order('2', 'Arun', printed: 'DG-1002'),
|
||||
order('3', 'Priya', printed: 'DG-1003'),
|
||||
order('4', 'Kumar', printed: 'DG-1004'),
|
||||
order('5', 'Ravi', printed: 'DG-1005'),
|
||||
];
|
||||
|
||||
group('the hand-off', () {
|
||||
test(
|
||||
'five picked orders become five deliveries, each with its bag',
|
||||
'five picked orders become five deliveries, each with its label',
|
||||
() async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
|
||||
// What the pickup confirmation writes when the slide lands.
|
||||
final manifest = BagManifest.forGroup(five);
|
||||
final manifest = OrderManifest.forGroup(five);
|
||||
await addCollectedOrderIds([for (final l in manifest) l.orderId]);
|
||||
await saveBagLabels({for (final l in manifest) l.orderId: l.bag});
|
||||
await saveOrderLabels({for (final l in manifest) l.orderId: l.label});
|
||||
|
||||
final carrying = await getCollectedOrderIds();
|
||||
final bags = await getBagLabels();
|
||||
final labels = await getOrderLabels();
|
||||
|
||||
expect(carrying.length, 5, reason: 'five picked is five to deliver');
|
||||
expect(bags.length, 5, reason: 'and five bags, one each');
|
||||
expect(bags['1'], 'Bag 1');
|
||||
expect(bags['5'], 'Bag 5');
|
||||
// No aggregate anywhere: the count of deliveries and the count of bags
|
||||
// are the same set, not two numbers that can drift.
|
||||
expect(bags.keys.toSet(), carrying);
|
||||
expect(labels.length, 5, reason: 'and five labels, one each');
|
||||
expect(labels['1'], 'DG-1001');
|
||||
expect(labels['5'], 'DG-1005');
|
||||
// No aggregate anywhere: the count of deliveries and the count of
|
||||
// labels are the same set, not two numbers that can drift.
|
||||
expect(labels.keys.toSet(), carrying);
|
||||
},
|
||||
);
|
||||
|
||||
test('a bag the counter could not supply never becomes a delivery', () async {
|
||||
test('an order the counter could not supply never becomes a delivery', () async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
|
||||
// Four handed over, one short — the shortfall path the pickup sheet owns.
|
||||
@@ -76,8 +89,8 @@ void main() {
|
||||
await addCollectedOrderIds([
|
||||
for (final o in loaded) o['orderid'] as String,
|
||||
]);
|
||||
await saveBagLabels({
|
||||
for (final l in BagManifest.forGroup(loaded)) l.orderId: l.bag,
|
||||
await saveOrderLabels({
|
||||
for (final l in OrderManifest.forGroup(loaded)) l.orderId: l.label,
|
||||
});
|
||||
await addNotLoadedOrderIds(['3']);
|
||||
|
||||
@@ -86,16 +99,16 @@ void main() {
|
||||
// A short pick is its own record. It is not a skip: nobody was visited
|
||||
// and the customer did nothing.
|
||||
expect(await getSkippedOrderIds(), isEmpty);
|
||||
// And it is not carrying a bag, because it was never in the box.
|
||||
expect((await getBagLabels()).containsKey('3'), isFalse);
|
||||
// And it carries no label, because it was never handed over.
|
||||
expect((await getOrderLabels()).containsKey('3'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('bag identity outlives the queue', () {
|
||||
test('delivering the first does not renumber the rest', () async {
|
||||
group('label identity outlives the queue', () {
|
||||
test('delivering the first does not disturb the rest', () async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
await saveBagLabels({
|
||||
for (final l in BagManifest.forGroup(five)) l.orderId: l.bag,
|
||||
await saveOrderLabels({
|
||||
for (final l in OrderManifest.forGroup(five)) l.orderId: l.label,
|
||||
});
|
||||
|
||||
// Joe is delivered and leaves the carrying set.
|
||||
@@ -104,38 +117,42 @@ void main() {
|
||||
]);
|
||||
await removeCollectedOrderIds(['1']);
|
||||
|
||||
final bags = await getBagLabels();
|
||||
final labels = await getOrderLabels();
|
||||
expect(await getCollectedOrderIds(), {'2', '3', '4', '5'});
|
||||
// Arun does *not* become Bag 1 because the list in front of him shrank.
|
||||
expect(bags['2'], 'Bag 2');
|
||||
expect(bags['3'], 'Bag 3');
|
||||
expect(bags['5'], 'Bag 5');
|
||||
// Arun keeps the code the counter stuck on his order. Nothing is read
|
||||
// off a position, so a list that shrinks under him changes nothing.
|
||||
expect(labels['2'], 'DG-1002');
|
||||
expect(labels['3'], 'DG-1003');
|
||||
expect(labels['5'], 'DG-1005');
|
||||
});
|
||||
|
||||
test('a second kitchen adds to the map rather than replacing it', () async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
await saveBagLabels({
|
||||
for (final l in BagManifest.forGroup(five.take(2).toList()))
|
||||
l.orderId: l.bag,
|
||||
await saveOrderLabels({
|
||||
for (final l in OrderManifest.forGroup(five.take(2).toList()))
|
||||
l.orderId: l.label,
|
||||
});
|
||||
|
||||
// A different counter, its own Bag 1 and Bag 2.
|
||||
final second = [order('9', 'Meena'), order('10', 'Sai')];
|
||||
await saveBagLabels({
|
||||
for (final l in BagManifest.forGroup(second)) l.orderId: l.bag,
|
||||
// A different counter, printing its own codes.
|
||||
final second = [
|
||||
order('9', 'Meena', printed: 'AM-2001'),
|
||||
order('10', 'Sai', printed: 'AM-2002'),
|
||||
];
|
||||
await saveOrderLabels({
|
||||
for (final l in OrderManifest.forGroup(second)) l.orderId: l.label,
|
||||
});
|
||||
|
||||
final bags = await getBagLabels();
|
||||
expect(bags.length, 4, reason: 'the first load must survive the second');
|
||||
expect(bags['1'], 'Bag 1');
|
||||
expect(bags['9'], 'Bag 1', reason: 'each counter counts its own shelf');
|
||||
expect(bags['10'], 'Bag 2');
|
||||
final labels = await getOrderLabels();
|
||||
expect(labels.length, 4, reason: 'the first load must survive the second');
|
||||
expect(labels['1'], 'DG-1001');
|
||||
expect(labels['9'], 'AM-2001');
|
||||
expect(labels['10'], 'AM-2002');
|
||||
});
|
||||
|
||||
test('a skipped delivery keeps its bag so it can be resumed', () async {
|
||||
test('a skipped delivery keeps its label so it can be resumed', () async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
await saveBagLabels({
|
||||
for (final l in BagManifest.forGroup(five)) l.orderId: l.bag,
|
||||
await saveOrderLabels({
|
||||
for (final l in OrderManifest.forGroup(five)) l.orderId: l.label,
|
||||
});
|
||||
await addSkippedBooking(five[2], reason: 'Customer unavailable');
|
||||
|
||||
@@ -149,13 +166,13 @@ void main() {
|
||||
);
|
||||
// He is still carrying Priya's lunch — the bag identity is exactly what
|
||||
// he needs when he comes back to it.
|
||||
expect((await getBagLabels())['3'], 'Bag 3');
|
||||
expect((await getOrderLabels())['3'], 'DG-1003');
|
||||
});
|
||||
|
||||
test("a day's run is wiped so bags never leak into tomorrow", () async {
|
||||
await saveBagLabels({'1': 'Bag 1'});
|
||||
await saveOrderLabels({'1': 'Bag 1'});
|
||||
await clearServiceRunState();
|
||||
expect(await getBagLabels(), isEmpty);
|
||||
expect(await getOrderLabels(), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,10 +181,10 @@ void main() {
|
||||
// bag map is keyed by order id, so it is swept by the same rule — a stored
|
||||
// pairing is trusted later *because* it is stored, and a stale one is worse
|
||||
// than none.
|
||||
await saveBagLabels({'MOCK-M-1001': 'Bag 1', '77': 'Bag 2'});
|
||||
await saveOrderLabels({'MOCK-M-1001': 'Bag 1', '77': 'Bag 2'});
|
||||
await purgeDemoRecords();
|
||||
|
||||
final bags = await getBagLabels();
|
||||
final bags = await getOrderLabels();
|
||||
expect(bags.containsKey('MOCK-M-1001'), isFalse);
|
||||
expect(bags['77'], 'Bag 2', reason: 'real work is untouched');
|
||||
});
|
||||
|
||||
100
test/device_telemetry_test.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/data/device_telemetry.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE CONSOLE'S DEVICE PANEL WAS READING AN EMPTY FIELD
|
||||
///
|
||||
/// ── What the dispatcher saw ──
|
||||
///
|
||||
/// Battery —, Connection —, GPS Accuracy —, Location Service —, Current
|
||||
/// Order —. Every column on the panel that answers *why has this rider gone
|
||||
/// quiet* was blank, on riders whose phones were reporting all of it.
|
||||
///
|
||||
/// ── Why ──
|
||||
///
|
||||
/// Not because nothing was measured. The heartbeat read the battery, the
|
||||
/// charging state, the connection type and the location-service status on every
|
||||
/// tick — and published them to **MQTT only**, in a block that ran *after* the
|
||||
/// rider log had already been posted. `POST /miler/logs` has a named field for
|
||||
/// each one and carried none of them.
|
||||
///
|
||||
/// Two lanes for one fact, and the lane the console reads was the empty one.
|
||||
/// On top of that, `location_service` was dropped a second time further down:
|
||||
/// the provider forwarded every other device field to [MilerApi.postLog] and
|
||||
/// silently skipped that one, so it could not have arrived even if the caller
|
||||
/// had sent it.
|
||||
///
|
||||
/// These pin the shape of the payload, which is the part a wrong refactor would
|
||||
/// break silently — a missing key renders as an em dash, which is exactly what
|
||||
/// it looked like before anyone noticed.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
group('the payload carries what the console reads', () {
|
||||
test('every field the contract names is emitted under its own name', () {
|
||||
const t = DeviceTelemetry(
|
||||
battery: 72,
|
||||
isCharging: false,
|
||||
connection: '4G',
|
||||
locationService: 'enabled',
|
||||
);
|
||||
|
||||
// The names are the wire's, from `POST /miler/logs`. A rename here is a
|
||||
// column that goes blank on the console with nothing else failing.
|
||||
expect(t.toPayload(), {
|
||||
'battery': '72',
|
||||
'is_charging': false,
|
||||
'connection': '4G',
|
||||
'location_service': 'enabled',
|
||||
});
|
||||
});
|
||||
|
||||
test('numbers go as strings, as every other numeric field does', () {
|
||||
// Documented at the top of `MilerApi`: real numbers fail to parse
|
||||
// server-side on `/logs` and `/consignments/logs`.
|
||||
const t = DeviceTelemetry(battery: 5);
|
||||
expect(t.toPayload()['battery'], isA<String>());
|
||||
expect(t.toPayload()['battery'], '5');
|
||||
});
|
||||
|
||||
test('is_charging stays a bool, because the contract says so', () {
|
||||
const t = DeviceTelemetry(isCharging: true);
|
||||
expect(t.toPayload()['is_charging'], isTrue);
|
||||
expect(t.toPayload()['is_charging'], isA<bool>());
|
||||
});
|
||||
});
|
||||
|
||||
group('unknown is absent, never a plausible number', () {
|
||||
test('a reading that failed emits no key at all', () {
|
||||
// The whole panel is read by somebody deciding whether to call a rider.
|
||||
// A `0` battery standing in for "could not ask" is a fact they would act
|
||||
// on; an absent key draws the em dash it already drew, which is true.
|
||||
const t = DeviceTelemetry();
|
||||
expect(t.toPayload(), isEmpty);
|
||||
});
|
||||
|
||||
test('a partial reading sends the half it has', () {
|
||||
// The reads are guarded individually, so a platform that will not answer
|
||||
// about the battery must not cost the connection its column too.
|
||||
const t = DeviceTelemetry(connection: 'wifi');
|
||||
expect(t.toPayload(), {'connection': 'wifi'});
|
||||
});
|
||||
|
||||
test('an empty string is treated as no answer', () {
|
||||
const t = DeviceTelemetry(connection: '', locationService: '');
|
||||
expect(t.toPayload(), isEmpty);
|
||||
});
|
||||
|
||||
test('a zero battery is a real reading and is sent', () {
|
||||
// The one number that must survive the "absent, never plausible" rule:
|
||||
// 0% is not a missing measurement, it is a rider about to disappear.
|
||||
const t = DeviceTelemetry(battery: 0);
|
||||
expect(t.toPayload()['battery'], '0');
|
||||
});
|
||||
|
||||
test('not charging is a real reading and is sent', () {
|
||||
const t = DeviceTelemetry(isCharging: false);
|
||||
expect(t.toPayload().containsKey('is_charging'), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -99,11 +100,24 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('every Bag ends on one axis', (tester) async {
|
||||
testWidgets('every order row ends on one axis', (tester) async {
|
||||
// ── What this used to measure ──
|
||||
//
|
||||
// The right edge of `Bag 1 … Bag 4`, which was a tag the app derived from
|
||||
// each row's position. That numbering is gone — it was the app's own
|
||||
// arithmetic drawn as a physical reference, beside a rail already
|
||||
// numbering the same rows — so the column it formed is gone with it.
|
||||
//
|
||||
// The rule it existed to protect is not: whatever closes a step row, the
|
||||
// rows close together. A ragged right edge down a list of people is what
|
||||
// makes a route unscannable, and it is now the chevrons that hold that
|
||||
// edge.
|
||||
await pumpHome(tester, orders: 4);
|
||||
final rights = [
|
||||
for (var i = 1; i <= 4; i++) tester.getRect(find.text('Bag $i')).right,
|
||||
];
|
||||
final rights = tester
|
||||
.widgetList<Icon>(find.byIcon(LucideIcons.chevronRight))
|
||||
.map((i) => tester.getRect(find.byWidget(i)).right)
|
||||
.toList();
|
||||
expect(rights.length, greaterThanOrEqualTo(4));
|
||||
for (final r in rights) {
|
||||
expect(r, closeTo(rights.first, 0.5));
|
||||
}
|
||||
@@ -117,27 +131,21 @@ void main() {
|
||||
// This pinned the label to the *name* axis, when the control headed its
|
||||
// list from the left. Inside a group it is a control over a list rather
|
||||
// than a heading for one, so it sits on the edge a list's own controls
|
||||
// use — the same right edge the bag labels and the chevrons below it
|
||||
// already keep. What must not happen is that it floats between the two.
|
||||
// use — the same right edge the chevrons below it already keep. What must
|
||||
// not happen is that it floats between the two.
|
||||
final label = tester.getRect(find.text('Select all 3')).right;
|
||||
final list = tester.getRect(find.byType(RouteTimeline)).right;
|
||||
final bag = tester.getRect(find.text('Bag 1')).right;
|
||||
expect(
|
||||
label,
|
||||
closeTo(list, 4),
|
||||
reason: 'the select-all ends at $label, the list at $list',
|
||||
);
|
||||
expect(
|
||||
label,
|
||||
greaterThan(bag),
|
||||
reason: 'and it closes past the bag column rather than inside it',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the kitchen and its state share one axis', (tester) async {
|
||||
await pumpHome(tester);
|
||||
expect(
|
||||
tester.getRect(find.text('Pending')).left,
|
||||
tester.getRect(find.textContaining('Pending')).left,
|
||||
closeTo(tester.getRect(find.text('Vidhya Kitchen')).left, 0.5),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,8 +27,14 @@ import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
void main() {
|
||||
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
||||
|
||||
Map<String, dynamic> meal(String id, String who, {int step = 1}) => {
|
||||
Map<String, dynamic> meal(
|
||||
String id,
|
||||
String who, {
|
||||
int step = 1,
|
||||
String? printed,
|
||||
}) => {
|
||||
'orderid': id,
|
||||
if (printed != null) 'baglabel': printed,
|
||||
'type': 'delivery',
|
||||
'step': step,
|
||||
'pickupcustomer': who,
|
||||
@@ -48,6 +54,10 @@ void main() {
|
||||
void Function(String)? onToggleSelect,
|
||||
double width = 390,
|
||||
double textScale = 1.0,
|
||||
|
||||
/// What each counter printed on an order, by id. Empty by default: most
|
||||
/// counters print nothing, and the app invents nothing for them.
|
||||
Map<String, String> printedLabels = const {},
|
||||
}) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
tester.view.physicalSize = Size(width * 3, 2600 * 3);
|
||||
@@ -57,9 +67,9 @@ void main() {
|
||||
final trip = Trip.fromStops(
|
||||
id: 'run',
|
||||
stops: [
|
||||
meal('1', 'Joe Mathew', step: 1),
|
||||
meal('2', 'Arun Prakash', step: 2),
|
||||
meal('3', 'Ravi Kumar', step: 3),
|
||||
meal('1', 'Joe Mathew', step: 1, printed: printedLabels['1']),
|
||||
meal('2', 'Arun Prakash', step: 2, printed: printedLabels['2']),
|
||||
meal('3', 'Ravi Kumar', step: 3, printed: printedLabels['3']),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -276,35 +286,72 @@ void main() {
|
||||
expect(opened, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('the bag block opens details, and does not select', (
|
||||
testWidgets('the corner block opens details, and does not select', (
|
||||
tester,
|
||||
) async {
|
||||
final selected = <String>[];
|
||||
await pumpHome(tester, onToggleSelect: selected.add);
|
||||
// The tag is the counter's printed label now — the derived `Bag 1` is
|
||||
// gone — so the block is tapped by the label the kitchen really stuck on.
|
||||
await pumpHome(
|
||||
tester,
|
||||
onToggleSelect: selected.add,
|
||||
printedLabels: const {'1': 'DG-1042'},
|
||||
);
|
||||
await openKitchen(tester);
|
||||
|
||||
// Siblings, never nested: two recognisers over the same pixels resolve
|
||||
// to neither, which is how the chevron was once dead for a whole build.
|
||||
await tester.tap(find.text('Bag 1'));
|
||||
await tester.tap(find.text('DG-1042'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
selected,
|
||||
isEmpty,
|
||||
reason: 'the bag corner is the way in, not a second tick',
|
||||
reason: 'the corner is the way in, not a second tick',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('the metadata is metadata', () {
|
||||
testWidgets('Pending is neutral, not a warning', (tester) async {
|
||||
testWidgets('Pending is its own amber, and never the warning one', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHome(tester);
|
||||
|
||||
// Amber is this app's exception hue. On a fresh morning every kitchen is
|
||||
// pending, so spending it here opens the whole screen in warning and
|
||||
// leaves nothing to say with when a stop genuinely goes wrong.
|
||||
final word = tester.widget<Text>(find.text('Pending'));
|
||||
expect(word.style!.color, ColorConstants.secondaryText);
|
||||
// ── What this test used to assert, and what survives of it ──
|
||||
//
|
||||
// It demanded neutral grey, on the argument that amber is this app's
|
||||
// exception hue and every kitchen is pending on a fresh morning — so
|
||||
// spending it here opens the whole screen in warning.
|
||||
//
|
||||
// Half of that argument is still load-bearing and is still checked: the
|
||||
// word must not be [ColorConstants.warning]. The other half was wrong —
|
||||
// "waiting on you" is worth a colour of its own, and grey made the rider
|
||||
// read undecided work by the *absence* of a mark. Pending has its own
|
||||
// gold now; the thing that must never happen is it colliding with the
|
||||
// hue reserved for a skip or a short payment.
|
||||
// `Pending 3/3` — the word carries the count now, the same way Accepted
|
||||
// states its share of a counter. See the chip in `route_timeline.dart`.
|
||||
final word = tester.widget<Text>(find.textContaining('Pending'));
|
||||
expect(word.data, 'Pending 3/3');
|
||||
expect(word.style!.color, ColorConstants.pendingAmber);
|
||||
expect(word.style!.color, isNot(ColorConstants.warning));
|
||||
expect(word.style!.color, isNot(ColorConstants.primary));
|
||||
});
|
||||
|
||||
testWidgets('Accepted is green and Collected is not', (tester) async {
|
||||
// The two rungs a rider most needs to tell apart on one counter. They
|
||||
// were both green once — accepted took the brand red only on the group
|
||||
// being ridden to, and collected took [ColorConstants.acceptGreen] — so
|
||||
// the pair could render as one hue and two words.
|
||||
await pumpHome(tester, accepted: const {'1', '2', '3'});
|
||||
|
||||
final accepted = tester.widget<Text>(find.textContaining('Accepted'));
|
||||
expect(accepted.style!.color, ColorConstants.acceptGreen);
|
||||
expect(
|
||||
ColorConstants.pickedBlue,
|
||||
isNot(ColorConstants.acceptGreen),
|
||||
reason: 'in-hand and taken-on must not share a colour',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a partly accepted kitchen says so as a fraction', (
|
||||
@@ -333,9 +380,9 @@ void main() {
|
||||
// plus its padding. A page surface is not. Filtering by width keeps the
|
||||
// original claim — the state is set, not stamped — and makes it immune
|
||||
// to whatever the page is standing on.
|
||||
final word = tester.getRect(find.text('Pending'));
|
||||
final word = tester.getRect(find.textContaining('Pending'));
|
||||
final chip = find.ancestor(
|
||||
of: find.text('Pending'),
|
||||
of: find.textContaining('Pending'),
|
||||
matching: find.byType(DecoratedBox),
|
||||
);
|
||||
for (final e in chip.evaluate()) {
|
||||
@@ -366,44 +413,40 @@ void main() {
|
||||
expect(find.text('Peelamedu'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('one order, one bag — and the column is stable', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('a counter that prints nothing shows no tag', (tester) async {
|
||||
// ── What this file used to assert here ──
|
||||
//
|
||||
// That every step carried `Bag 1 … Bag 3`, right-aligned on one axis so
|
||||
// the phone could be read against a shelf. The numbering was the app's
|
||||
// own — derived from each row's position whenever the payload printed
|
||||
// nothing, which on this fixture is every row — and it sat two columns
|
||||
// from a rail already numbering the same steps.
|
||||
//
|
||||
// Only what a counter actually printed survives. The fixture prints
|
||||
// nothing, so the rows carry the person and the chevron and no tag at
|
||||
// all.
|
||||
await pumpHome(tester);
|
||||
await openKitchen(tester);
|
||||
|
||||
for (final bag in const ['Bag 1', 'Bag 2', 'Bag 3']) {
|
||||
expect(find.text(bag), findsOneWidget);
|
||||
}
|
||||
|
||||
// Right-aligned on one axis, so the phone can be read against a shelf.
|
||||
final rights = [
|
||||
for (final bag in const ['Bag 1', 'Bag 2', 'Bag 3'])
|
||||
tester.getRect(find.text(bag)).right,
|
||||
];
|
||||
for (final r in rights) {
|
||||
expect(r, closeTo(rights.first, 0.5));
|
||||
for (final label in const ['Bag 1', 'Bag 2', 'Bag 3']) {
|
||||
expect(find.text(label), findsNothing);
|
||||
}
|
||||
// The people are still there, one row each.
|
||||
expect(find.text('Joe Mathew'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the name outranks the bag, and the bag yields first', (
|
||||
testWidgets('a printed label is shown exactly as it was sent', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpHome(tester, textScale: 2.0, width: 320);
|
||||
// A code the kitchen stuck on the order is a fact about an object in the
|
||||
// rider's hands, so it travels verbatim — never prefixed, never renamed.
|
||||
await pumpHome(tester, printedLabels: const {'1': 'DG-1042'});
|
||||
await openKitchen(tester);
|
||||
|
||||
// `Bag 1` is compared against a shelf, not read as prose, so it stops
|
||||
// scaling before the customer's name does. Left uncapped it took half a
|
||||
// 320pt row at 2× and the name — the one item that must yield last —
|
||||
// was what ellipsised.
|
||||
final bag = tester.getRect(find.text('Bag 1'));
|
||||
expect(
|
||||
bag.width,
|
||||
lessThan(120),
|
||||
reason: 'the bag column does not grow into the name',
|
||||
);
|
||||
expect(tester.takeException(), isNull, reason: 'and nothing overflows');
|
||||
expect(find.text('DG-1042'), findsOneWidget);
|
||||
expect(find.textContaining('Bag'), findsNothing);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
group('the offer states its own number', () {
|
||||
|
||||
305
test/home_today_only_test.dart
Normal file
@@ -0,0 +1,305 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/data/api_config.dart';
|
||||
import 'package:miler/data/service_day.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// HOME IS TODAY, AND TODAY IS THREE TRIPS
|
||||
///
|
||||
/// Two bugs with one cause, and this is the file that holds the cause down.
|
||||
///
|
||||
/// ── What the rider saw ──
|
||||
///
|
||||
/// 1. **Yesterday's assignments were on Home.** `GET /miler/bookings` returns
|
||||
/// his whole *open* set, not his day, so a booking raised on Tuesday and
|
||||
/// never closed came back on Wednesday and on Thursday — inside the kitchen
|
||||
/// dropdown, and inside every count above it.
|
||||
/// 2. **Every run was Trip 1.** The hub assigns morning, afternoon and evening
|
||||
/// slots and the tabs are those slots, but an evening run opened filed
|
||||
/// under the morning.
|
||||
///
|
||||
/// ── The one cause ──
|
||||
///
|
||||
/// `ApiConfig.pickupFromBooking` builds a **fixed map**: a field it does not
|
||||
/// name is a field no screen can ever see. Every timestamp on the booking was
|
||||
/// unnamed, so it was thrown away at the adapter — and both features downstream
|
||||
/// are clocks. [ServiceDay] asked "which day is this" in field names the
|
||||
/// adapted row no longer spoke and got `''`; [Trip] asked "which part of the
|
||||
/// day" and got the same silence, dropped every stop into one undated bucket,
|
||||
/// and that bucket landed in the first free slot.
|
||||
///
|
||||
/// So the fix is at the adapter, and these tests start there. The two features
|
||||
/// are then checked on the shapes the wire really produces.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
/// A booking as `GET /miler/bookings` returns it — the new shape, which is
|
||||
/// the one that goes through the translation.
|
||||
Map<String, dynamic> booking({
|
||||
required String id,
|
||||
String status = 'Miler_Assigned',
|
||||
Map<String, dynamic> extra = const {},
|
||||
}) => <String, dynamic>{
|
||||
'bookingid': id,
|
||||
'bookingreference': 'DM-$id',
|
||||
'status': status,
|
||||
'customername': 'Joe Mathew',
|
||||
'pickupaddress': '12 SNS Colony, Peelamedu, Coimbatore',
|
||||
...extra,
|
||||
};
|
||||
|
||||
group('the adapter keeps the clocks', () {
|
||||
test('a booking that carries a timestamp still carries it afterwards', () {
|
||||
final row = ApiConfig.pickupFromBooking(
|
||||
booking(
|
||||
id: '1',
|
||||
extra: const {
|
||||
'createdat': '2026-08-27T09:14:00',
|
||||
'updatedon': '2026-08-27T11:02:00',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// The whole failure was here: these came back and went nowhere.
|
||||
expect(row['createdat'], '2026-08-27T09:14:00');
|
||||
expect(row['updatedon'], '2026-08-27T11:02:00');
|
||||
expect(ServiceDay.of(row), '2026-08-27');
|
||||
});
|
||||
|
||||
test('every name ServiceDay reads survives the translation', () {
|
||||
// A regression net for the shape of the bug rather than one instance of
|
||||
// it: the adapter and the day filter must agree on the vocabulary, or a
|
||||
// key added to one and not the other silently un-dates the row again.
|
||||
for (final key in ServiceDay.timeKeys) {
|
||||
// Written by the local stores, never by the wire.
|
||||
if (key == 'completedat' || key == 'skippedat') continue;
|
||||
final row = ApiConfig.pickupFromBooking(
|
||||
booking(id: 'x', extra: {key: '2026-08-27T10:00:00'}),
|
||||
);
|
||||
expect(
|
||||
ServiceDay.of(row),
|
||||
'2026-08-27',
|
||||
reason: '$key was dropped at the adapter',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('an absent timestamp is not invented', () {
|
||||
final row = ApiConfig.pickupFromBooking(booking(id: '2'));
|
||||
expect(row.containsKey('pickedtime'), isFalse);
|
||||
expect(ServiceDay.of(row), '');
|
||||
});
|
||||
});
|
||||
|
||||
group("yesterday's assignment is not today's work", () {
|
||||
const today = '2026-08-27';
|
||||
|
||||
test('a booking still open from yesterday does not belong to today', () {
|
||||
final row = ApiConfig.pickupFromBooking(
|
||||
booking(id: '3', extra: const {'createdat': '2026-08-26T18:40:00'}),
|
||||
);
|
||||
expect(ServiceDay.belongsToToday(row, now: today), isFalse);
|
||||
});
|
||||
|
||||
test('the same booking, raised this morning, does', () {
|
||||
final row = ApiConfig.pickupFromBooking(
|
||||
booking(id: '4', extra: const {'createdat': '2026-08-27T07:05:00'}),
|
||||
);
|
||||
expect(ServiceDay.belongsToToday(row, now: today), isTrue);
|
||||
});
|
||||
|
||||
test('tomorrow is not today either', () {
|
||||
// A pre-assigned booking is real work and it is not this shift's. It
|
||||
// reappears on its own morning, when the same predicate admits it.
|
||||
final row = ApiConfig.pickupFromBooking(
|
||||
booking(id: '5', extra: const {'createdat': '2026-08-28T06:00:00'}),
|
||||
);
|
||||
expect(ServiceDay.belongsToToday(row, now: today), isFalse);
|
||||
});
|
||||
|
||||
test('the local assignment stamp dates a row the backend does not', () {
|
||||
// The live contract sends no clock the app can trust as an assignment
|
||||
// time, so [WorkRepository] writes when the booking first reached this
|
||||
// rider. It is the weakest key on the list and it is the one that
|
||||
// actually fires on Doormile.
|
||||
final row = ApiConfig.pickupFromBooking(booking(id: '6'))
|
||||
..['assignedat'] = '2026-08-26T09:00:00';
|
||||
expect(ServiceDay.of(row), '2026-08-26');
|
||||
expect(ServiceDay.belongsToToday(row, now: today), isFalse);
|
||||
});
|
||||
|
||||
test('a clock the backend sends outranks the local guess', () {
|
||||
// The rider's phone met this booking on the 27th; the hub finished it on
|
||||
// the 26th. The hub is right — the app's first sighting says only when
|
||||
// the app was open.
|
||||
final row = ApiConfig.pickupFromBooking(
|
||||
booking(
|
||||
id: '7',
|
||||
status: 'Picked',
|
||||
extra: const {'pickedtime': '2026-08-26T20:10:00'},
|
||||
),
|
||||
)..['assignedat'] = '2026-08-27T08:00:00';
|
||||
expect(ServiceDay.of(row), '2026-08-26');
|
||||
});
|
||||
});
|
||||
|
||||
group('the work screens filter one day, by one rule', () {
|
||||
const today = '2026-08-27';
|
||||
|
||||
Map<String, dynamic> dated(String id, String at) =>
|
||||
ApiConfig.pickupFromBooking(booking(id: id))..['assignedat'] = at;
|
||||
|
||||
test("yesterday's leftovers are dropped and today's kept", () {
|
||||
final kept = ServiceDay.onlyToday([
|
||||
dated('old', '2026-08-26T09:00:00'),
|
||||
dated('new', '2026-08-27T09:00:00'),
|
||||
], now: today);
|
||||
|
||||
expect(kept.map((s) => s['orderid']), ['DM-new']);
|
||||
});
|
||||
|
||||
test('an undated row among dated ones is dropped', () {
|
||||
// The burden of proof is on the row — the same rule Activity applies.
|
||||
final kept = ServiceDay.onlyToday([
|
||||
dated('a', '2026-08-27T09:00:00'),
|
||||
ApiConfig.pickupFromBooking(booking(id: 'nodate')),
|
||||
], now: today);
|
||||
|
||||
expect(kept.map((s) => s['orderid']), ['DM-a']);
|
||||
});
|
||||
|
||||
test('a day where NOTHING can be dated is kept whole', () {
|
||||
// The narrow safety net. Not a stale queue — a build that has lost the
|
||||
// ability to date anything — and blanking a rider's screen would hide
|
||||
// that behind something that reads like good news.
|
||||
final rows = [
|
||||
ApiConfig.pickupFromBooking(booking(id: 'a')),
|
||||
ApiConfig.pickupFromBooking(booking(id: 'b')),
|
||||
];
|
||||
expect(ServiceDay.onlyToday(rows, now: today).length, 2);
|
||||
});
|
||||
|
||||
test('an empty day stays empty', () {
|
||||
expect(ServiceDay.onlyToday(const [], now: today), isEmpty);
|
||||
});
|
||||
|
||||
test('a picked stop finished today stays — the rail needs it', () {
|
||||
// Deliveries and Home both count finished work into the day's figures.
|
||||
// Dropping it would empty the progress ring every time a rider succeeds.
|
||||
final done = ApiConfig.pickupFromBooking(
|
||||
booking(
|
||||
id: 'p',
|
||||
status: 'Picked',
|
||||
extra: const {'pickedtime': '2026-08-27T11:20:00'},
|
||||
),
|
||||
);
|
||||
expect(ServiceDay.onlyToday([done], now: today).length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('the slot a stop lands in is the part of the day', () {
|
||||
/// The adapted shape plus the stamp the repository merges on.
|
||||
Map<String, dynamic> assigned(String id, DateTime at) =>
|
||||
ApiConfig.pickupFromBooking(booking(id: id))
|
||||
..['assignedat'] = at.toIso8601String();
|
||||
|
||||
test('assigned in the morning is Trip 1', () {
|
||||
final trips = Trip.groupIntoTrips([
|
||||
assigned('a', DateTime(2026, 8, 27, 8, 30)),
|
||||
]);
|
||||
expect(trips.single.dayPart, DayPart.morning);
|
||||
expect(trips.single.tripNumber, 1);
|
||||
expect(trips.slots.indexOf(trips.single), 0);
|
||||
});
|
||||
|
||||
test('assigned in the afternoon is Trip 2, on a day with no morning', () {
|
||||
// The point of the whole slot model. A rider with only afternoon work
|
||||
// used to see it as Trip 1, so his "Trip 2" and the hub's were different
|
||||
// runs — and dispatch rings him about the hub's.
|
||||
final trips = Trip.groupIntoTrips([
|
||||
assigned('b', DateTime(2026, 8, 27, 14, 5)),
|
||||
]);
|
||||
expect(trips.single.tripNumber, 2);
|
||||
final slots = trips.slots;
|
||||
expect(slots[0], isNull);
|
||||
expect(slots[1], isNotNull);
|
||||
});
|
||||
|
||||
test('assigned in the evening is Trip 3', () {
|
||||
final trips = Trip.groupIntoTrips([
|
||||
assigned('c', DateTime(2026, 8, 27, 19, 0)),
|
||||
]);
|
||||
expect(trips.single.tripNumber, 3);
|
||||
expect(trips.slots[2], isNotNull);
|
||||
});
|
||||
|
||||
test('a full day fills all three slots in order', () {
|
||||
final trips = Trip.groupIntoTrips([
|
||||
assigned('m', DateTime(2026, 8, 27, 9, 0)),
|
||||
assigned('a', DateTime(2026, 8, 27, 13, 0)),
|
||||
assigned('e', DateTime(2026, 8, 27, 18, 0)),
|
||||
]);
|
||||
expect(trips.length, 3);
|
||||
expect([for (final t in trips) t.tripNumber], [1, 2, 3]);
|
||||
// `orderid` is the booking *reference* after translation — the id the
|
||||
// rider reads, not the row's primary key.
|
||||
expect(trips.slots.map((t) => t?.stops.single['orderid']), [
|
||||
'DM-m',
|
||||
'DM-a',
|
||||
'DM-e',
|
||||
]);
|
||||
});
|
||||
|
||||
test('two runs in one part stay one trip', () {
|
||||
// A day part is not a rolling window: two stops handed over an hour apart
|
||||
// in the same morning are the same run, and which trip a stop is on must
|
||||
// never depend on what else is in the day.
|
||||
final trips = Trip.groupIntoTrips([
|
||||
assigned('a', DateTime(2026, 8, 27, 7, 30)),
|
||||
assigned('b', DateTime(2026, 8, 27, 11, 45)),
|
||||
]);
|
||||
expect(trips.length, 1);
|
||||
expect(trips.single.stops.length, 2);
|
||||
expect(trips.single.tripNumber, 1);
|
||||
});
|
||||
|
||||
test('a real due time still outranks the assignment clock', () {
|
||||
// Handed over at eight in the morning, due at three in the afternoon. The
|
||||
// work is the afternoon's; the handover time was only ever a stand-in for
|
||||
// a field the backend does not send.
|
||||
final stop = assigned('d', DateTime(2026, 8, 27, 8, 0));
|
||||
stop['expected_pickup_time'] = DateTime(
|
||||
2026,
|
||||
8,
|
||||
27,
|
||||
15,
|
||||
0,
|
||||
).toIso8601String();
|
||||
|
||||
expect(Trip.groupIntoTrips([stop]).single.tripNumber, 2);
|
||||
});
|
||||
|
||||
test('an explicit backend slot window still wins outright', () {
|
||||
// If the console ever grows real slots, they are the answer and none of
|
||||
// the inference above runs.
|
||||
final stop = assigned('s', DateTime(2026, 8, 27, 19, 30))
|
||||
..['slotstarttime'] = DateTime(2026, 8, 27, 10).toIso8601String()
|
||||
..['slotendtime'] = DateTime(2026, 8, 27, 13).toIso8601String();
|
||||
|
||||
final trip = Trip.groupIntoTrips([stop]).single;
|
||||
expect(trip.slotLabel, '10:00 AM – 1:00 PM');
|
||||
expect(trip.tripNumber, 1);
|
||||
});
|
||||
|
||||
test('a stop with no clock at all keeps its arrival order', () {
|
||||
// Unchanged behaviour, stated so it stays deliberate: a trip that cannot
|
||||
// be placed by day part must not be guessed at from the wall clock, or it
|
||||
// would move between tabs as the afternoon wore on.
|
||||
final trips = Trip.groupIntoTrips([
|
||||
ApiConfig.pickupFromBooking(booking(id: 'z')),
|
||||
]);
|
||||
expect(trips.single.dayPart, isNull);
|
||||
expect(trips.slots[0], isNotNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import 'package:miler/views/Dashboard/home/trip_card.dart';
|
||||
/// under it, ticked in batches and advanced with one gesture:
|
||||
///
|
||||
/// ```
|
||||
/// Vidhya Kitchen · 4 bags · 2 to accept ˅
|
||||
/// Vidhya Kitchen · 4 orders · 2 to accept ˅
|
||||
/// ◉ Joe Gandhipuram Bag 1
|
||||
/// ◉ Priya RS Puram Bag 2
|
||||
/// ```
|
||||
@@ -153,7 +153,9 @@ void main() {
|
||||
// the detail sheet and the Activity record.
|
||||
expect(find.text('Gandhipuram'), findsOneWidget);
|
||||
expect(find.textContaining('1001'), findsNothing);
|
||||
expect(find.text('Bag 1'), findsOneWidget);
|
||||
// And no invented shelf reference beside it: this counter printed
|
||||
// nothing, so the row carries the person and the area and stops there.
|
||||
expect(find.text('Bag 1'), findsNothing);
|
||||
});
|
||||
|
||||
// Updated when `areaOf` was corrected against real Coimbatore addresses
|
||||
@@ -181,19 +183,25 @@ void main() {
|
||||
expect(find.text(''), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('one order is one bag, counted off the orders', (tester) async {
|
||||
testWidgets('the load is stated once, and counted off the orders', (
|
||||
tester,
|
||||
) async {
|
||||
await pump(tester, [
|
||||
order('1001', 'Joe', 'Gandhipuram, Coimbatore'),
|
||||
order('1002', 'Priya', 'RS Puram, Coimbatore'),
|
||||
order('1003', 'Arun', 'Peelamedu, Coimbatore'),
|
||||
]);
|
||||
// The header states the load once, in the unit the rider carries; the
|
||||
// check that orders and bags agree is the manifest itself, one named
|
||||
// bag per order. `N orders · N bags` was the same number twice and it
|
||||
// cost the header its `to accept` clause on a real route.
|
||||
expect(find.textContaining('3 bags'), findsWidgets);
|
||||
for (final bag in const ['Bag 1', 'Bag 2', 'Bag 3']) {
|
||||
expect(find.text(bag), findsOneWidget);
|
||||
// The header states the load once. `N orders · N bags` was the same
|
||||
// number twice and it cost the header its `to accept` clause on a real
|
||||
// route; the derived per-row numbering that made up the second half is
|
||||
// gone entirely.
|
||||
expect(find.textContaining('3 orders'), findsWidgets);
|
||||
for (final invented in const ['Bag 1', 'Bag 2', 'Bag 3']) {
|
||||
expect(find.text(invented), findsNothing);
|
||||
}
|
||||
// One row per order, named by the person it is for.
|
||||
for (final who in const ['Joe', 'Priya', 'Arun']) {
|
||||
expect(find.textContaining(who), findsWidgets);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -213,7 +221,7 @@ void main() {
|
||||
|
||||
// The rider is mid-decision at one counter; the count of what is still
|
||||
// undecided is the whole content of that state.
|
||||
expect(find.textContaining('4 bags'), findsWidgets);
|
||||
expect(find.textContaining('4 orders'), findsWidgets);
|
||||
expect(find.textContaining('to accept'), findsWidgets);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ void main() {
|
||||
test('every stop is a drop, with a bag the rider can read off a box', () {
|
||||
for (final s in MealRunMock.stops()) {
|
||||
expect(stopKindOf(s).isDelivery, isTrue);
|
||||
expect(stopBagLabel(s), isNotEmpty);
|
||||
expect(stopPrintedLabel(s), isNotEmpty);
|
||||
expect((s['pickupcustomer'] as String), isNotEmpty);
|
||||
expect((s['pickupaddress'] as String), contains('Coimbatore'));
|
||||
}
|
||||
|
||||
@@ -402,17 +402,73 @@ void main() {
|
||||
expect(s['dropaddress'], 'Flat 3, Anna Nagar');
|
||||
});
|
||||
|
||||
test('a booking without source fields does not invent one', () {
|
||||
// BACKEND DEPENDENCY: sourceid/sourcename are not deployed. Until they
|
||||
// are, every order groups under one unnamed bucket — visibly wrong, and
|
||||
// deliberately not papered over with a guessed kitchen name.
|
||||
test('a booking without source fields is never given a name', () {
|
||||
// BACKEND DEPENDENCY: sourceid/sourcename are not deployed everywhere.
|
||||
// The rule that matters is unchanged and is what this asserts: the app
|
||||
// does not paper over a missing counter with a guessed kitchen name.
|
||||
final s = ApiConfig.pickupFromBooking({
|
||||
'bookingid': 43,
|
||||
'bookingreference': 'DM-43',
|
||||
'pickupaddress': '9 Mettupalayam Road',
|
||||
});
|
||||
expect(MilkRun.sourceNameOf(s), '');
|
||||
expect(MilkRun.sourceKeyOf(s), 'none');
|
||||
});
|
||||
|
||||
test('...but it still groups by the address it does carry', () {
|
||||
// ── What changed, and why it is not "inventing" one ──
|
||||
//
|
||||
// This used to demand `'none'`, which put every source-less order in one
|
||||
// bucket regardless of where it was collected. On a real morning that
|
||||
// failed in the other direction: eight orders off one kitchen, seven
|
||||
// carrying its id and one not, and the eighth could not join them — it
|
||||
// fell out of the dropdown into a flat header of its own titled with the
|
||||
// customer's name.
|
||||
//
|
||||
// Keying on the address invents nothing. It is the weakest *true*
|
||||
// statement on the row — these are collected from the same address — and
|
||||
// it is only reached when the id and the name are both absent. The name
|
||||
// is still empty, which is what the test above pins.
|
||||
Map<String, dynamic> at(String address) =>
|
||||
ApiConfig.pickupFromBooking({
|
||||
'bookingid': address.hashCode,
|
||||
'bookingreference': 'DM-$address',
|
||||
'pickupaddress': address,
|
||||
});
|
||||
|
||||
// Same counter, spelled as two different rows would spell it.
|
||||
expect(
|
||||
MilkRun.sourceKeyOf(at('9 Mettupalayam Road')),
|
||||
MilkRun.sourceKeyOf(at('9, METTUPALAYAM ROAD ')),
|
||||
);
|
||||
// A different place stays a different group.
|
||||
expect(
|
||||
MilkRun.sourceKeyOf(at('9 Mettupalayam Road')),
|
||||
isNot(MilkRun.sourceKeyOf(at('12 SNS Colony'))),
|
||||
);
|
||||
// Nothing to group on at all is still its own bucket, never a guess.
|
||||
expect(
|
||||
MilkRun.sourceKeyOf(
|
||||
ApiConfig.pickupFromBooking({
|
||||
'bookingid': 44,
|
||||
'bookingreference': 'DM-44',
|
||||
}),
|
||||
),
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
test('an identified counter still outranks the address', () {
|
||||
// Order of trust: the hub's id, then the name it reads by, then where it
|
||||
// is. Two kitchens at one address must not merge because a row lost its
|
||||
// id; two rows with the same id must not split because someone retyped
|
||||
// the address.
|
||||
final byId = ApiConfig.pickupFromBooking({
|
||||
'bookingid': 45,
|
||||
'bookingreference': 'DM-45',
|
||||
'sourceid': 'K1',
|
||||
'pickupaddress': '9 Mettupalayam Road',
|
||||
});
|
||||
expect(MilkRun.sourceKeyOf(byId), 'id:K1');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/Models/stop_status.dart';
|
||||
import 'package:miler/data/bag_manifest.dart';
|
||||
import 'package:miler/data/order_manifest.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/home/stop_action_sheet.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip.dart';
|
||||
@@ -63,22 +63,21 @@ void main() {
|
||||
];
|
||||
|
||||
group('the rule itself', () {
|
||||
test('bags are counted from orders, never stored', () {
|
||||
expect(BagManifest.countLabel(5), '5 bags');
|
||||
expect(BagManifest.countLabel(1), '1 bag');
|
||||
test('the load is counted from the orders, never stored', () {
|
||||
expect(OrderManifest.countLabel(5), '5 orders');
|
||||
expect(OrderManifest.countLabel(1), '1 order');
|
||||
});
|
||||
|
||||
test('every order gets exactly one bag, in group order', () {
|
||||
final lines = BagManifest.forGroup(fiveOrders);
|
||||
test('every order gets exactly one line, in group order', () {
|
||||
// ── The derived numbering is gone ──
|
||||
//
|
||||
// This demanded `Bag 1 … Bag 5` — the app numbering a list it had built
|
||||
// and drawing the result as a physical reference. What it was really
|
||||
// protecting is the pairing, and that is unchanged: one line per order,
|
||||
// in the counter's order, each carrying its own customer.
|
||||
final lines = OrderManifest.forGroup(fiveOrders);
|
||||
|
||||
expect(lines.length, 5, reason: 'five orders is five manifest lines');
|
||||
expect(lines.map((l) => l.bag).toList(), [
|
||||
'Bag 1',
|
||||
'Bag 2',
|
||||
'Bag 3',
|
||||
'Bag 4',
|
||||
'Bag 5',
|
||||
]);
|
||||
expect(lines.map((l) => l.customer).toList(), [
|
||||
'Joe',
|
||||
'Arun',
|
||||
@@ -86,41 +85,53 @@ void main() {
|
||||
'Kumar',
|
||||
'Ravi',
|
||||
]);
|
||||
// Order 1 → Bag 1 → Joe, and it stays that way.
|
||||
expect(lines.first.orderId, '1');
|
||||
expect(
|
||||
lines.map((l) => l.bag).toSet().length,
|
||||
lines.map((l) => l.orderId).toSet().length,
|
||||
5,
|
||||
reason: 'two orders may never claim the same bag',
|
||||
reason: 'two lines may never claim the same order',
|
||||
);
|
||||
// A counter that printed nothing yields no labels at all.
|
||||
expect(lines.every((l) => l.label.isEmpty), isTrue);
|
||||
});
|
||||
|
||||
test('a label the kitchen actually printed wins over the position', () {
|
||||
final lines = BagManifest.forGroup([
|
||||
test('a label the kitchen printed travels verbatim, and only that', () {
|
||||
final lines = OrderManifest.forGroup([
|
||||
order('1', customer: 'Joe', printedBag: 'DG-1042'),
|
||||
order('2', customer: 'Arun'),
|
||||
]);
|
||||
expect(lines[0].bag, 'DG-1042', reason: 'what is on the physical bag');
|
||||
expect(lines[1].bag, 'Bag 2');
|
||||
// Exactly as it was sent — never prefixed, never renamed.
|
||||
expect(lines[0].label, 'DG-1042');
|
||||
// And nothing is manufactured for the order beside it.
|
||||
expect(lines[1].label, '');
|
||||
});
|
||||
|
||||
test('the item quantity is never read as a bag count', () {
|
||||
test('the item quantity is never read as a count of handovers', () {
|
||||
// Each of these orders carries `quantity: 4`. Four is an item count; the
|
||||
// bag count is one, per order, always.
|
||||
final lines = BagManifest.forGroup(fiveOrders);
|
||||
// count of handovers is one, per order, always.
|
||||
final lines = OrderManifest.forGroup(fiveOrders);
|
||||
expect(lines.length, 5);
|
||||
expect(BagManifest.countLabel(lines.length), '5 bags');
|
||||
expect(OrderManifest.countLabel(lines.length), '5 orders');
|
||||
});
|
||||
|
||||
test('a bag belongs to its own counter, not the whole route', () {
|
||||
// Third order from the second kitchen is *its* Bag 3, not the route's
|
||||
// eighth — the rider counts one shelf at a time.
|
||||
final kitchenTwo = [
|
||||
order('6', customer: 'Meena', kitchen: 'Anna Kitchen', kitchenId: 'K2'),
|
||||
order('7', customer: 'Sai', kitchen: 'Anna Kitchen', kitchenId: 'K2'),
|
||||
order('8', customer: 'Devi', kitchen: 'Anna Kitchen', kitchenId: 'K2'),
|
||||
];
|
||||
expect(BagManifest.bagFor(kitchenTwo[2], kitchenTwo), 'Bag 3');
|
||||
test('nothing is invented for a counter that prints nothing', () {
|
||||
// ── What this test used to assert ──
|
||||
//
|
||||
// That the third order from the second kitchen was *its* `Bag 3` and not
|
||||
// the route's eighth — the app numbering a list it had built and drawing
|
||||
// the result as a shelf reference.
|
||||
//
|
||||
// The numbering is gone. It was the app's own arithmetic presented as a
|
||||
// physical fact, on rows that already carry a rail numbering them two
|
||||
// columns to the left. What a counter really printed still travels
|
||||
// verbatim; where it printed nothing, nothing shows.
|
||||
final unlabelled = order(
|
||||
'6',
|
||||
customer: 'Meena',
|
||||
kitchen: 'Anna Kitchen',
|
||||
kitchenId: 'K2',
|
||||
);
|
||||
expect(OrderManifest.labelFor(unlabelled), '');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,7 +169,7 @@ void main() {
|
||||
).buildStops(),
|
||||
);
|
||||
|
||||
expect(find.text('5 bags'), findsOneWidget);
|
||||
expect(find.text('5 orders'), findsOneWidget);
|
||||
// Nothing anywhere offers a second, contradictable count.
|
||||
expect(find.textContaining('meals'), findsNothing);
|
||||
expect(find.textContaining('Quantity'), findsNothing);
|
||||
@@ -191,7 +202,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(find.text('Vidhya Kitchen'), findsOneWidget);
|
||||
expect(find.text('5 bags'), findsOneWidget);
|
||||
expect(find.text('5 orders'), findsOneWidget);
|
||||
// One obvious next action on the whole screen, and it is the journey.
|
||||
expect(find.bySemanticsLabel('Navigate to this pickup'), findsOneWidget);
|
||||
});
|
||||
@@ -218,10 +229,10 @@ void main() {
|
||||
// node still names the kitchen and its load, so nothing is hidden.
|
||||
expect(find.bySemanticsLabel('Navigate to this pickup'), findsNothing);
|
||||
expect(find.text('Vidhya Kitchen'), findsOneWidget);
|
||||
expect(find.text('5 bags'), findsOneWidget);
|
||||
expect(find.text('5 orders'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the confirmation names every order and its bag', (
|
||||
testWidgets('the confirmation names every order it covers', (
|
||||
tester,
|
||||
) async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
@@ -231,24 +242,23 @@ void main() {
|
||||
stage: StopStatus.accepted,
|
||||
placeName: 'Vidhya Kitchen',
|
||||
count: fiveOrders.length,
|
||||
lines: BagManifest.forGroup(fiveOrders),
|
||||
lines: OrderManifest.forGroup(fiveOrders),
|
||||
onConfirm: (_) async => null,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Arrived at Vidhya Kitchen'), findsOneWidget);
|
||||
expect(find.text('5 bags'), findsOneWidget);
|
||||
expect(find.text('5 orders'), findsOneWidget);
|
||||
|
||||
// The list he checks against the shelf: five names, five bags, paired.
|
||||
for (final pair in const [
|
||||
('Joe', 'Bag 1'),
|
||||
('Arun', 'Bag 2'),
|
||||
('Priya', 'Bag 3'),
|
||||
('Kumar', 'Bag 4'),
|
||||
('Ravi', 'Bag 5'),
|
||||
]) {
|
||||
expect(find.text(pair.$1), findsOneWidget);
|
||||
expect(find.text(pair.$2), findsOneWidget);
|
||||
// The list he checks against the shelf: five people, one line each. The
|
||||
// derived `Bag 1 … Bag 5` that used to sit beside them was the app's own
|
||||
// numbering of a list it had built, and it is gone; a label a counter
|
||||
// really printed still appears, and this fixture prints none.
|
||||
for (final who in const ['Joe', 'Arun', 'Priya', 'Kumar', 'Ravi']) {
|
||||
expect(find.text(who), findsOneWidget);
|
||||
}
|
||||
for (final invented in const ['Bag 1', 'Bag 2', 'Bag 5']) {
|
||||
expect(find.text(invented), findsNothing);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -262,36 +272,46 @@ void main() {
|
||||
stage: StopStatus.arrived,
|
||||
placeName: 'Vidhya Kitchen',
|
||||
count: fiveOrders.length,
|
||||
lines: BagManifest.forGroup(fiveOrders),
|
||||
lines: OrderManifest.forGroup(fiveOrders),
|
||||
onConfirm: (_) async => null,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Picked up from Vidhya Kitchen'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Make sure all 5 bags are loaded before continuing.'),
|
||||
find.text('Make sure all 5 orders are loaded before continuing.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
// Both rungs are the same interaction, named for what they claim.
|
||||
expect(find.textContaining('Slide to confirm'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a card says which bag it is carrying', (tester) async {
|
||||
testWidgets('a card carries the label its counter printed', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
// A counter that really prints labels. Five near-identical lunches is
|
||||
// where mis-delivery comes from, and a printed code is the cheapest thing
|
||||
// that catches it — so where one exists it must reach the card, verbatim.
|
||||
final printed = [
|
||||
order('1', customer: 'Joe', printedBag: 'DG-1001'),
|
||||
order('2', customer: 'Arun', printedBag: 'DG-1002'),
|
||||
order('3', customer: 'Priya', printedBag: 'DG-1003'),
|
||||
order('4', customer: 'Kumar', printedBag: 'DG-1004'),
|
||||
order('5', customer: 'Ravi', printedBag: 'DG-1005'),
|
||||
];
|
||||
await pump(
|
||||
tester,
|
||||
() => TripCard(
|
||||
trip: Trip.fromStops(id: 't', stops: fiveOrders),
|
||||
trip: Trip.fromStops(id: 't', stops: printed),
|
||||
acceptedIds: const {'1', '2', '3', '4', '5'},
|
||||
onToggleSelect: (_) {},
|
||||
).buildStops(),
|
||||
);
|
||||
|
||||
// Five near-identical lunch bags is where mis-delivery comes from, so
|
||||
// every card names the one it is for.
|
||||
expect(find.text('Bag 1'), findsOneWidget);
|
||||
expect(find.text('Bag 5'), findsOneWidget);
|
||||
expect(find.text('DG-1001'), findsOneWidget);
|
||||
expect(find.text('DG-1005'), findsOneWidget);
|
||||
expect(find.text('Ravi'), findsWidgets);
|
||||
// Shown as the counter wrote it — never dressed up with a word.
|
||||
expect(find.textContaining('Bag'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
248
test/one_counter_one_dropdown_test.dart
Normal file
@@ -0,0 +1,248 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:miler/data/milk_run.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/home/route_timeline.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip_card.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// EIGHT ORDERS OFF ONE COUNTER MAKE ONE DROPDOWN OF EIGHT
|
||||
///
|
||||
/// ── What the rider saw ──
|
||||
///
|
||||
/// The hub assigned eight orders from one kitchen. Home drew a dropdown holding
|
||||
/// **seven**, and the eighth sat above it as a bare header titled with the
|
||||
/// customer's name — reading as a second place he had to ride to, off the same
|
||||
/// shelf as the other seven.
|
||||
///
|
||||
/// ── Two causes, one level apart ──
|
||||
///
|
||||
/// **The grouping key.** `routeGroups` built its own — `stopSourceId`, falling
|
||||
/// back to `stopSourceName` — while [MilkRun.sourceKeyOf] is the function that
|
||||
/// exists to answer *which counter is this*, and is what the bulk collect keys
|
||||
/// on. They disagreed: `sourceKeyOf` reads `pickuplocationid` and the card's
|
||||
/// own expression did not. A row identified only by its location id was one
|
||||
/// place according to the collect and another according to the card offering
|
||||
/// it. And a row carrying no source identity at all got its own bucket, with
|
||||
/// nothing consulted about *where it is collected from* — which is on every
|
||||
/// row, as the pickup address.
|
||||
///
|
||||
/// **The `flat` flag.** A lone stop was drawn as a bare header — the header
|
||||
/// *is* the stop — whenever the payload carried no source name. A missing name
|
||||
/// is a gap in the data, not evidence that there is no counter, so the one
|
||||
/// order that lost its kitchen name was demoted from a place to a row.
|
||||
///
|
||||
/// Both now answer from the same fact: a place is identified by its id, then
|
||||
/// its name, then **where it is**.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
tearDown(() {
|
||||
Get.reset();
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
});
|
||||
|
||||
const kitchen = '9 Mettupalayam Road, RS Puram, Coimbatore 641002';
|
||||
|
||||
/// One order off the kitchen. [sourceId] and [sourceName] are omitted on the
|
||||
/// row that lost them, which is the case the whole file is about.
|
||||
Map<String, dynamic> order(
|
||||
int id, {
|
||||
String? sourceId = 'K1',
|
||||
String? sourceName = 'Vidhya Kitchen',
|
||||
String address = kitchen,
|
||||
String status = 'assigned',
|
||||
}) => <String, dynamic>{
|
||||
'orderid': 'M-$id',
|
||||
'pickupid': id,
|
||||
'step': id,
|
||||
if (sourceId != null) 'sourceid': sourceId,
|
||||
if (sourceName != null) 'kitchenname': sourceName,
|
||||
'pickupcustomer': 'Customer $id',
|
||||
'pickupaddress': address,
|
||||
'dropaddress': 'Door $id, Peelamedu, Coimbatore',
|
||||
'orderstatus': status,
|
||||
'type': 'delivery',
|
||||
'pickuplat': 11.01,
|
||||
'pickuplon': 76.95,
|
||||
};
|
||||
|
||||
List<RouteGroup> groupsFor(List<Map<String, dynamic>> stops) {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
return TripCard(trip: Trip.fromStops(id: 'T1', stops: stops)).routeGroups();
|
||||
}
|
||||
|
||||
group('the reported case: eight assigned, seven in the list', () {
|
||||
test('the odd row out joins its siblings', () {
|
||||
// Seven carry the kitchen's id; the eighth lost both id and name and has
|
||||
// only the address it shares with them.
|
||||
final stops = [
|
||||
for (var i = 1; i <= 7; i++) order(i),
|
||||
order(8, sourceId: null, sourceName: null),
|
||||
];
|
||||
|
||||
final groups = groupsFor(stops);
|
||||
expect(groups.length, 1, reason: 'one counter, one dropdown');
|
||||
expect(groups.single.stops.length, 8);
|
||||
expect(groups.single.flat, isFalse);
|
||||
});
|
||||
|
||||
test('the count the rider reads says eight', () {
|
||||
final stops = [
|
||||
for (var i = 1; i <= 7; i++) order(i),
|
||||
order(8, sourceId: null, sourceName: null),
|
||||
];
|
||||
final group = groupsFor(stops).single;
|
||||
|
||||
// The load line and the list under it are the same eight.
|
||||
expect(group.countLabel, '8 orders');
|
||||
expect(group.manifest.length, 8);
|
||||
expect(group.pendingCount, 8);
|
||||
});
|
||||
|
||||
test('a row from a genuinely different place still splits', () {
|
||||
// The fix must not merge two counters. Grouping by address is the weakest
|
||||
// true statement available, not a licence to collapse the route.
|
||||
final stops = [
|
||||
for (var i = 1; i <= 7; i++) order(i),
|
||||
order(
|
||||
8,
|
||||
sourceId: null,
|
||||
sourceName: null,
|
||||
address: '12 SNS Colony, Peelamedu, Coimbatore 641004',
|
||||
),
|
||||
];
|
||||
expect(groupsFor(stops).length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('the heading names the place, never an order', () {
|
||||
test('a counter with no name is headed by its address', () {
|
||||
final stops = [
|
||||
for (var i = 1; i <= 3; i++)
|
||||
order(i, sourceId: null, sourceName: null),
|
||||
];
|
||||
final group = groupsFor(stops).single;
|
||||
|
||||
// It used to fall through to `MilkRun.navigationLabel`, which answers
|
||||
// with the customer — so the heading read as one more order.
|
||||
expect(group.name, isNot(contains('Customer')));
|
||||
expect(group.name, contains('Mettupalayam'));
|
||||
});
|
||||
|
||||
test('a named counter still leads with its name, address beneath', () {
|
||||
final group = groupsFor([for (var i = 1; i <= 3; i++) order(i)]).single;
|
||||
expect(group.name, 'Vidhya Kitchen');
|
||||
expect(group.location, contains('Mettupalayam'));
|
||||
});
|
||||
|
||||
test('the address is not printed twice when it is the heading', () {
|
||||
final stops = [
|
||||
for (var i = 1; i <= 3; i++)
|
||||
order(i, sourceId: null, sourceName: null),
|
||||
];
|
||||
final group = groupsFor(stops).single;
|
||||
expect(group.name, isNotEmpty);
|
||||
expect(
|
||||
group.location,
|
||||
isEmpty,
|
||||
reason: 'the heading already says where it is',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('a lone stop is a place when it has one', () {
|
||||
test('one order off an unnamed counter still opens as a list', () {
|
||||
// The same demotion, one level down: a single order whose row lost its
|
||||
// kitchen name was drawn as a bare customer header rather than as a place
|
||||
// holding one order.
|
||||
final group = groupsFor([
|
||||
order(1, sourceId: null, sourceName: null),
|
||||
]).single;
|
||||
|
||||
expect(group.flat, isFalse, reason: 'it has an address to be a place by');
|
||||
expect(group.name, contains('Mettupalayam'));
|
||||
expect(group.manifest.length, 1);
|
||||
});
|
||||
|
||||
test('with nothing at all to name a place by, it is the stop', () {
|
||||
// Unchanged, and deliberately: no counter, no name, no address means
|
||||
// there is no place above the order worth drawing, so the header is the
|
||||
// stop and opens its detail sheet.
|
||||
final group = groupsFor([
|
||||
order(1, sourceId: null, sourceName: null, address: ''),
|
||||
]).single;
|
||||
|
||||
expect(group.flat, isTrue);
|
||||
});
|
||||
|
||||
test('a parcel route is always flat, whatever the address says', () {
|
||||
// Every stop is its own group there — the rider really does ride to each
|
||||
// customer for one consignment, and a header plus a single child would
|
||||
// print the same name twice.
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
final groups = TripCard(
|
||||
trip: Trip.fromStops(
|
||||
id: 'T1',
|
||||
stops: [for (var i = 1; i <= 3; i++) order(i)],
|
||||
),
|
||||
).routeGroups();
|
||||
|
||||
expect(groups.length, 3);
|
||||
expect(groups.every((g) => g.flat), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('adopting a counter, and the limits on it', () {
|
||||
test('an identified stop never consults the day', () {
|
||||
// Adoption only ever fills a gap. A stop carrying its own id keeps it,
|
||||
// so two counters that happen to share a street cannot merge and a row
|
||||
// cannot be pulled off the id the hub gave it.
|
||||
final mine = <String, dynamic>{
|
||||
'orderid': 'M-1',
|
||||
'sourceid': 'K1',
|
||||
'pickupaddress': kitchen,
|
||||
};
|
||||
final neighbour = <String, dynamic>{
|
||||
'orderid': 'M-2',
|
||||
'sourceid': 'K2',
|
||||
'pickupaddress': kitchen,
|
||||
};
|
||||
expect(MilkRun.sourceKeyOf(mine, within: [mine, neighbour]), 'id:K1');
|
||||
expect(MilkRun.sourceKeyOf(neighbour, within: [mine, neighbour]), 'id:K2');
|
||||
});
|
||||
|
||||
test('with no day to consult it falls back to the address', () {
|
||||
final orphan = <String, dynamic>{
|
||||
'orderid': 'M-3',
|
||||
'pickupaddress': kitchen,
|
||||
};
|
||||
expect(MilkRun.sourceKeyOf(orphan), startsWith('at:'));
|
||||
});
|
||||
|
||||
test('the bulk collect gathers the adopted order too', () {
|
||||
// The collect must not leave behind the order the card has just learned
|
||||
// to group in — that is the two-readers failure in a new place.
|
||||
final day = [
|
||||
for (var i = 1; i <= 7; i++) order(i),
|
||||
order(8, sourceId: null, sourceName: null),
|
||||
];
|
||||
expect(MilkRun.sameSourceAs(day.first, day).length, 8);
|
||||
expect(MilkRun.sameSourceAs(day.last, day).length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
group('one reader for which counter this is', () {
|
||||
test('the card groups on the same key the bulk collect does', () {
|
||||
// The two disagreed, so a kitchen could be split on screen while the
|
||||
// collect still treated it as one operation.
|
||||
final byLocationId = <String, dynamic>{
|
||||
'orderid': 'M-9',
|
||||
'pickuplocationid': 'L7',
|
||||
'pickupaddress': kitchen,
|
||||
};
|
||||
expect(MilkRun.sourceKeyOf(byLocationId), 'id:L7');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,7 @@ 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/data/order_manifest.dart';
|
||||
import 'package:miler/views/helpers/widgets/miler_sheet_kit.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -189,17 +189,17 @@ void main() {
|
||||
c,
|
||||
placeName: 'Vidhya Kitchen',
|
||||
lines: [
|
||||
BagLine(
|
||||
ManifestLine(
|
||||
stop: order(),
|
||||
orderId: 'D1',
|
||||
customer: 'Joe Mathew',
|
||||
bag: 'Bag 1',
|
||||
label: 'Bag 1',
|
||||
),
|
||||
BagLine(
|
||||
ManifestLine(
|
||||
stop: order(),
|
||||
orderId: 'D2',
|
||||
customer: 'Priya Nair',
|
||||
bag: 'Bag 2',
|
||||
label: 'Bag 2',
|
||||
),
|
||||
],
|
||||
meters: 3100,
|
||||
@@ -311,7 +311,7 @@ void main() {
|
||||
stop: long,
|
||||
stopNumber: 7,
|
||||
ofOrder: true,
|
||||
bag: 'Bag 1',
|
||||
label: 'Bag 1',
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
133
test/rail_counts_the_route_test.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/Models/stop_status.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip.dart';
|
||||
import 'package:miler/views/Dashboard/pickups/pickups.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE RAIL COUNTS THE ROUTE, NOT THE RECORD
|
||||
///
|
||||
/// ── What the rider saw ──
|
||||
///
|
||||
/// He accepted one order out of five, picked it up, and the progress rail at
|
||||
/// the top of Deliveries drew **five nodes**. One of them was his.
|
||||
///
|
||||
/// ── Where the other four came from ──
|
||||
///
|
||||
/// The rail reads the completed store, and that store is not a list of
|
||||
/// finished deliveries. It is where every order that leaves the rider's hands
|
||||
/// is filed, whatever ended it — and Home files a **declined** stop there too,
|
||||
/// with `terminalStatus: 'rejected'`, so the decision survives on Activity.
|
||||
/// That was harmless for as long as nothing read the store as a route. The rail
|
||||
/// reads it as a route.
|
||||
///
|
||||
/// The count was the visible half. The colours were worse, and this is what
|
||||
/// these tests really pin:
|
||||
///
|
||||
/// `rejected` → [StopProgress.pending] — four stops he had **refused**,
|
||||
/// drawn on the rail ahead of him as work still to come.
|
||||
/// `cancelled` → [StopStatusX.isWorkComplete] → [StopProgress.done] —
|
||||
/// drawn **green**, claiming a delivery that never happened.
|
||||
///
|
||||
/// A skip is the case that must survive the fix: it is a door he went to and
|
||||
/// could not finish, the rail has an amber node for precisely that, and losing
|
||||
/// it would hide work he still owes.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
||||
|
||||
/// A row as the completed store hands it back — `orderstatus` stamped with
|
||||
/// whatever ended it. See `addCompletedBookings`.
|
||||
Map<String, dynamic> filed(String id, String terminalStatus) => {
|
||||
'orderid': id,
|
||||
'orderstatus': terminalStatus,
|
||||
'completedday': '2026-08-27',
|
||||
'completedat': '2026-08-27T10:00:00',
|
||||
};
|
||||
|
||||
group('what belongs on a progress rail', () {
|
||||
test('a declined stop does not', () {
|
||||
// The four that made the rail read five. He said no to these.
|
||||
expect(wasRiddenTo(filed('r', 'rejected')), isFalse);
|
||||
});
|
||||
|
||||
test('a cancelled stop does not', () {
|
||||
// The office called it off. It was never a door he stood at, and on a
|
||||
// milk run it would have drawn green.
|
||||
expect(wasRiddenTo(filed('c', 'cancelled')), isFalse);
|
||||
});
|
||||
|
||||
test('a skipped stop does', () {
|
||||
// Unfinished business, and the one state the rail has an amber node for.
|
||||
// Dropping it with the others would hide a door he still owes a visit.
|
||||
expect(wasRiddenTo(filed('s', 'skipped')), isTrue);
|
||||
});
|
||||
|
||||
test('a delivered stop does', () {
|
||||
expect(wasRiddenTo(filed('d', 'delivered')), isTrue);
|
||||
});
|
||||
|
||||
test('a picked stop does', () {
|
||||
expect(wasRiddenTo(filed('p', 'picked')), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('the states the excluded rows would have been drawn in', () {
|
||||
test('a declined stop reads as work still ahead', () {
|
||||
// Not a subtle wrong colour — [StopProgress.pending] is the same node a
|
||||
// stop he has yet to ride to gets. This is why exclusion is the fix and
|
||||
// recolouring is not: it is not on his route at all.
|
||||
expect(
|
||||
stopProgressFor(stopStatusFromRaw('rejected')),
|
||||
StopProgress.pending,
|
||||
);
|
||||
});
|
||||
|
||||
test('a cancelled stop reads as done', () {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
expect(stopStatusFromRaw('cancelled').isWorkComplete, isTrue);
|
||||
expect(
|
||||
stopProgressFor(stopStatusFromRaw('cancelled')),
|
||||
StopProgress.done,
|
||||
);
|
||||
});
|
||||
|
||||
test('a skipped stop reads as skipped, on either line', () {
|
||||
for (final profile in [ServiceProfile.parcel, ServiceProfile.milkMan]) {
|
||||
ServiceProfile.setActive(profile);
|
||||
expect(
|
||||
stopProgressFor(stopStatusFromRaw('skipped')),
|
||||
StopProgress.skipped,
|
||||
reason: 'the amber node is the whole reason a skip stays',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group("the rider's own case: one of five", () {
|
||||
test('four declined and one picked leaves a rail of one', () {
|
||||
// The report, as arithmetic. The store holds all five because Home files
|
||||
// a decline; only one of them is a place he rode to.
|
||||
final store = [
|
||||
filed('1', 'picked'),
|
||||
filed('2', 'rejected'),
|
||||
filed('3', 'rejected'),
|
||||
filed('4', 'rejected'),
|
||||
filed('5', 'rejected'),
|
||||
];
|
||||
|
||||
expect(store.length, 5);
|
||||
expect(store.where(wasRiddenTo).length, 1);
|
||||
});
|
||||
|
||||
test('a day genuinely worked is not trimmed', () {
|
||||
// The fix must not cost a rider his progress. Four real completions and
|
||||
// one in hand is still a five-node rail, four of them green.
|
||||
final store = [
|
||||
for (final id in ['1', '2', '3', '4']) filed(id, 'delivered'),
|
||||
];
|
||||
expect(store.where(wasRiddenTo).length, 4);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -87,27 +87,27 @@ void main() {
|
||||
expect(stopSourceName({'kitchenname': 'Kitchen 1'}), 'Kitchen 1');
|
||||
expect(stopSourceName({'SourceName': ' Anna Nagar '}), 'Anna Nagar');
|
||||
expect(stopSourceId({'kitchenid': 'K-77'}), 'K-77');
|
||||
expect(stopBagLabel({'baglabel': 'DG-1042'}), 'DG-1042');
|
||||
expect(stopBagLabel({'labelcode': 'DG-9'}), 'DG-9');
|
||||
expect(stopPrintedLabel({'baglabel': 'DG-1042'}), 'DG-1042');
|
||||
expect(stopPrintedLabel({'labelcode': 'DG-9'}), 'DG-9');
|
||||
});
|
||||
|
||||
test(
|
||||
'absent on a parcel stop rather than empty-stringed onto the card',
|
||||
() {
|
||||
expect(stopSourceName({'orderid': '7'}), '');
|
||||
expect(stopBagLabel({'orderid': '7'}), '');
|
||||
expect(stopPrintedLabel({'orderid': '7'}), '');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('StopSourceLine', () {
|
||||
Future<void> pump(WidgetTester tester, String source, String bag) async {
|
||||
Future<void> pump(WidgetTester tester, String source, String label) async {
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: StopSourceLine(source: source, bagLabel: bag),
|
||||
body: StopSourceLine(source: source, printedLabel: label),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -126,12 +126,17 @@ void main() {
|
||||
testWidgets('shows both when the stop carries both', (tester) async {
|
||||
await pump(tester, 'Kitchen 1', 'DG-1042');
|
||||
expect(find.text('Kitchen 1'), findsOneWidget);
|
||||
expect(find.text('Bag DG-1042'), findsOneWidget);
|
||||
// The counter's code, exactly as it was sent. It used to be prefixed
|
||||
// `Bag DG-1042` — a word bolted onto a reference that already names
|
||||
// itself.
|
||||
expect(find.text('DG-1042'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows the bag alone when there is no source', (tester) async {
|
||||
testWidgets('shows the label alone when there is no source', (
|
||||
tester,
|
||||
) async {
|
||||
await pump(tester, '', 'DG-1042');
|
||||
expect(find.text('Bag DG-1042'), findsOneWidget);
|
||||
expect(find.text('DG-1042'), findsOneWidget);
|
||||
expect(
|
||||
tester.getSize(find.byType(StopSourceLine)).height,
|
||||
greaterThan(0),
|
||||
@@ -206,12 +211,12 @@ void main() {
|
||||
|
||||
// Named once, by the place — not again on every order under it.
|
||||
expect(find.text('Kitchen 1'), findsOneWidget);
|
||||
// The label the kitchen printed, straight from [BagManifest]. The `Bag `
|
||||
// The label the kitchen printed, straight from [OrderManifest]. The `Bag `
|
||||
// prefix belonged to the card's chip and went with it, which stopped a
|
||||
// printed `DG-1042` reading as `Bag DG-1042`.
|
||||
expect(find.text('DG-1042'), findsOneWidget);
|
||||
// The count the rider checks the shelf against.
|
||||
expect(find.text('1 bag'), findsOneWidget);
|
||||
expect(find.text('1 order'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a parcel route keeps a bare pending row', (tester) async {
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:miler/data/accepted_store.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
|
||||
import 'package:miler/Models/stop_status.dart';
|
||||
import 'package:miler/data/bag_manifest.dart';
|
||||
import 'package:miler/data/order_manifest.dart';
|
||||
import 'package:miler/views/Dashboard/home/stop_action_sheet.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip.dart';
|
||||
import 'package:miler/views/Dashboard/home/selection_bar.dart';
|
||||
@@ -196,9 +196,9 @@ void main() {
|
||||
// orders for the bar, and the bar is the only thing that commits.
|
||||
expect(find.textContaining('Collect '), findsNothing);
|
||||
// One order, one bag — the two numbers the rider checks against the
|
||||
// shelf, and they are the same number by definition. See [BagManifest].
|
||||
expect(find.text('2 bags'), findsOneWidget);
|
||||
expect(find.text('1 bag'), findsOneWidget);
|
||||
// shelf, and they are the same number by definition. See [OrderManifest].
|
||||
expect(find.text('2 orders'), findsOneWidget);
|
||||
expect(find.text('1 order'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a heading gathers one rung, never a mixed batch', (
|
||||
@@ -246,7 +246,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The header still says how much of the counter is not his yet.
|
||||
expect(find.text('2 bags · 1 to accept'), findsOneWidget);
|
||||
expect(find.text('2 orders · 1 to accept'), findsOneWidget);
|
||||
|
||||
// And its tick gathers only the orders on one rung. He cannot be handed
|
||||
// food for an order he has not taken on, so a heading that swept both
|
||||
@@ -295,7 +295,7 @@ void main() {
|
||||
stage: StopStatus.arrived,
|
||||
placeName: 'Kitchen 1',
|
||||
count: stops.length,
|
||||
lines: BagManifest.forGroup(stops),
|
||||
lines: OrderManifest.forGroup(stops),
|
||||
onConfirm: (outcome) async {
|
||||
seen = outcome;
|
||||
return null;
|
||||
@@ -308,7 +308,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
if (markMissing.isNotEmpty) {
|
||||
await tester.tap(find.text('Report missing bag'));
|
||||
await tester.tap(find.text('Report a missing order'));
|
||||
await tester.pumpAndSettle();
|
||||
for (final name in markMissing) {
|
||||
await tester.tap(find.text(name));
|
||||
@@ -326,11 +326,11 @@ void main() {
|
||||
// A screen that makes him tick three boxes to record "nothing was wrong"
|
||||
// is a screen he learns to dismiss without reading. So the common case is
|
||||
// a list he reads, and the shortfall is one tap away.
|
||||
expect(find.text('3 bags'), findsOneWidget);
|
||||
expect(find.text('Report missing bag'), findsOneWidget);
|
||||
expect(find.text('3 orders'), findsOneWidget);
|
||||
expect(find.text('Report a missing order'), findsOneWidget);
|
||||
expect(find.text('Missing'), findsNothing);
|
||||
expect(
|
||||
find.text('Make sure all 3 bags are loaded before continuing.'),
|
||||
find.text('Make sure all 3 orders are loaded before continuing.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
@@ -340,7 +340,7 @@ void main() {
|
||||
) async {
|
||||
await open(tester, 3, markMissing: ['Customer 2']);
|
||||
|
||||
expect(find.text('2 bags · 1 missing'), findsOneWidget);
|
||||
expect(find.text('2 orders · 1 missing'), findsOneWidget);
|
||||
expect(find.text('Missing'), findsOneWidget);
|
||||
expect(
|
||||
find.text('1 of 3 not supplied — they will leave your route.'),
|
||||
@@ -391,7 +391,7 @@ void main() {
|
||||
stage: StopStatus.arrived,
|
||||
placeName: 'Kitchen 1',
|
||||
count: stops.length,
|
||||
lines: BagManifest.forGroup(stops),
|
||||
lines: OrderManifest.forGroup(stops),
|
||||
onConfirm: (outcome) async {
|
||||
captured = outcome;
|
||||
return null;
|
||||
@@ -406,7 +406,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Report missing bag'));
|
||||
await tester.tap(find.text('Report a missing order'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Customer 2'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/Models/stop_status.dart';
|
||||
import 'package:miler/data/bag_manifest.dart';
|
||||
import 'package:miler/data/order_manifest.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/home/duty_sheet.dart';
|
||||
import 'package:miler/views/Dashboard/home/stop_action_sheet.dart';
|
||||
@@ -123,8 +123,8 @@ void main() {
|
||||
placeName: 'Vidhya Kitchen',
|
||||
count: 2,
|
||||
lines: const [
|
||||
BagLine(stop: {}, orderId: 'A', customer: 'Joe', bag: 'Bag 1'),
|
||||
BagLine(stop: {}, orderId: 'B', customer: 'Arun', bag: 'Bag 2'),
|
||||
ManifestLine(stop: {}, orderId: 'A', customer: 'Joe', label: 'Bag 1'),
|
||||
ManifestLine(stop: {}, orderId: 'B', customer: 'Arun', label: 'Bag 2'),
|
||||
],
|
||||
onConfirm: (_) async => null,
|
||||
),
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/Models/stop_status.dart';
|
||||
import 'package:miler/data/bag_manifest.dart';
|
||||
import 'package:miler/data/order_manifest.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/home/duty_sheet.dart';
|
||||
import 'package:miler/views/Dashboard/home/pickup_preview_sheet.dart';
|
||||
@@ -49,14 +49,14 @@ void main() {
|
||||
const textScales = [1.0, 1.3, 2.0];
|
||||
|
||||
const lines = [
|
||||
BagLine(
|
||||
ManifestLine(
|
||||
stop: {},
|
||||
orderId: 'D1',
|
||||
customer: 'Venkataraman Subramanian',
|
||||
bag: 'Bag 1',
|
||||
label: 'Bag 1',
|
||||
),
|
||||
BagLine(stop: {}, orderId: 'D2', customer: 'Arun Prakash', bag: 'Bag 2'),
|
||||
BagLine(stop: {}, orderId: 'D3', customer: 'Priya Nair', bag: 'Bag 3'),
|
||||
ManifestLine(stop: {}, orderId: 'D2', customer: 'Arun Prakash', label: 'Bag 2'),
|
||||
ManifestLine(stop: {}, orderId: 'D3', customer: 'Priya Nair', label: 'Bag 3'),
|
||||
];
|
||||
|
||||
Map<String, dynamic> stop() => {
|
||||
@@ -181,7 +181,7 @@ void main() {
|
||||
c,
|
||||
stop: stop(),
|
||||
stopNumber: 7,
|
||||
bag: 'Bag 1',
|
||||
label: 'Bag 1',
|
||||
riderLat: 11.0168,
|
||||
riderLng: 76.9558,
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/Models/stop_status.dart';
|
||||
import 'package:miler/data/bag_manifest.dart';
|
||||
import 'package:miler/data/order_manifest.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/home/duty_sheet.dart';
|
||||
import 'package:miler/views/Dashboard/home/pickup_preview_sheet.dart';
|
||||
@@ -52,11 +52,11 @@ void main() {
|
||||
});
|
||||
|
||||
const lines = [
|
||||
BagLine(stop: {}, orderId: 'D1', customer: 'Joe Mathew', bag: 'Bag 1'),
|
||||
BagLine(stop: {}, orderId: 'D2', customer: 'Arun Prakash', bag: 'Bag 2'),
|
||||
BagLine(stop: {}, orderId: 'D3', customer: 'Priya Nair', bag: 'Bag 3'),
|
||||
BagLine(stop: {}, orderId: 'D4', customer: 'Ravi Kumar', bag: 'Bag 4'),
|
||||
BagLine(stop: {}, orderId: 'D5', customer: 'Kumar S', bag: 'Bag 5'),
|
||||
ManifestLine(stop: {}, orderId: 'D1', customer: 'Joe Mathew', label: 'Bag 1'),
|
||||
ManifestLine(stop: {}, orderId: 'D2', customer: 'Arun Prakash', label: 'Bag 2'),
|
||||
ManifestLine(stop: {}, orderId: 'D3', customer: 'Priya Nair', label: 'Bag 3'),
|
||||
ManifestLine(stop: {}, orderId: 'D4', customer: 'Ravi Kumar', label: 'Bag 4'),
|
||||
ManifestLine(stop: {}, orderId: 'D5', customer: 'Kumar S', label: 'Bag 5'),
|
||||
];
|
||||
|
||||
Map<String, dynamic> stop() => {
|
||||
@@ -253,7 +253,7 @@ void main() {
|
||||
'notes': 'Gate code 4411, leave with security if unreachable.',
|
||||
},
|
||||
stopNumber: 7,
|
||||
bag: 'Bag 1',
|
||||
label: 'Bag 1',
|
||||
riderLat: 11.0168,
|
||||
riderLng: 76.9558,
|
||||
),
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@@ -234,10 +234,10 @@ void main() {
|
||||
// normalising to `doormile` matched the Logistics list, won on
|
||||
// name-first, and the id was never consulted.
|
||||
//
|
||||
// On Logistics `MyPickups._unreleased` is empty by design, so the
|
||||
// **Start round** bar does not render. With
|
||||
// `MILER_COLLECTED_STATE_ENABLED` on, that rider's collected parcels
|
||||
// strand at `Collected_By_Miler` with no control to release them.
|
||||
// On Logistics a rider has no customer round — he carries what he
|
||||
// collects to the hub — so nothing in the app calls `start-delivery` for
|
||||
// him. With `MILER_COLLECTED_STATE_ENABLED` on, that rider's collected
|
||||
// orders strand at `Collected_By_Miler` with nothing to release them.
|
||||
for (final name in const ['Doormile', 'doormile', 'DOOR-MILE']) {
|
||||
final p = await resolveWith({'tenantname': name, 'tenantid': 13});
|
||||
expect(
|
||||
|
||||
@@ -124,7 +124,7 @@ void main() {
|
||||
expect(find.text('Kitchen 1'), findsOneWidget);
|
||||
expect(find.text('Kitchen 2'), findsOneWidget);
|
||||
// And the bag, which is what he checks against the box in his hand.
|
||||
// The label the kitchen printed, as [BagManifest] hands it over — the
|
||||
// The label the kitchen printed, as [OrderManifest] hands it over — the
|
||||
// `Bag ` prefix belonged to the card's chip and went with it, which
|
||||
// stopped a printed `BAG-1` reading as `Bag BAG-1`.
|
||||
expect(find.text('BAG-1'), findsOneWidget);
|
||||
|
||||