device infos

This commit is contained in:
2026-08-28 15:07:30 +05:30
parent 5723d373b2
commit 69f4f3e909
84 changed files with 4337 additions and 2339 deletions

View File

@@ -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 ──

View File

@@ -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

View File

@@ -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'},
),

View File

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

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

View File

@@ -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),
);
});

View File

@@ -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', () {

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

View File

@@ -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);
});
});

View File

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

View File

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

View File

@@ -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);
});
});
}

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

View File

@@ -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',
),
);

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

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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,
),

View File

@@ -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,
),

View File

@@ -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,
),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -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(

View File

@@ -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);