Miler rider app: surface system, visible design language, backend lifecycle
Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
615
test/delivery_details_test.dart
Normal file
615
test/delivery_details_test.dart
Normal file
@@ -0,0 +1,615 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import 'package:miler/data/order_events.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/activity/delivery_details_page.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// DELIVERY DETAILS — one stop, the whole story
|
||||
///
|
||||
/// The record used to open inside the Activity list. It is a page now, for two
|
||||
/// reasons: an open record is several screens tall, so the list stopped being
|
||||
/// scannable the moment it was used — and the two jobs want opposite things.
|
||||
/// The list answers "did that go through?" in one word per row; this answers
|
||||
/// "what happened on that one?", which is a timeline, a route, four
|
||||
/// measurements and a reference.
|
||||
///
|
||||
/// The page takes the record it is given and renders it — no fetch, no store
|
||||
/// write — so these pump it directly.
|
||||
///
|
||||
/// What they hold is the part that regresses silently: **a rung, a row or a
|
||||
/// whole section is drawn only when the data behind it exists.** A record with
|
||||
/// a hole in it can be investigated; one carrying a fabricated `0 km` cannot be
|
||||
/// caught.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
final now = DateTime(2026, 8, 20, 11, 42);
|
||||
|
||||
/// A finished stop. Every stamp is optional, exactly as it is in the store.
|
||||
Map<String, dynamic> record({
|
||||
String type = 'delivery',
|
||||
String status = 'delivered',
|
||||
bool started = true,
|
||||
bool arrived = true,
|
||||
Map<String, dynamic>? compliance,
|
||||
double cash = 0,
|
||||
double? collected,
|
||||
String? skipReason,
|
||||
}) => {
|
||||
'orderid': 'DM101',
|
||||
'type': type,
|
||||
'pickupcustomer': 'Joe Mathew',
|
||||
'sourcename': 'Vidhya Kitchen',
|
||||
'pickupaddress': '78, Cross Cut Road, Gandhipuram',
|
||||
'orderstatus': status,
|
||||
'completedat': now.toIso8601String(),
|
||||
if (started)
|
||||
'startedat': now.subtract(const Duration(minutes: 87)).toIso8601String(),
|
||||
if (arrived)
|
||||
'arrivedat': now.subtract(const Duration(minutes: 5)).toIso8601String(),
|
||||
if (compliance != null) 'compliance': compliance,
|
||||
if (cash > 0) 'collectionamt': '$cash',
|
||||
if (skipReason != null) 'skipreason': skipReason,
|
||||
'proof': {
|
||||
'collected': 2,
|
||||
'weight': '2.4',
|
||||
if (collected != null) 'amountcollected': collected,
|
||||
},
|
||||
};
|
||||
|
||||
Future<void> pump(WidgetTester tester, Map<String, dynamic> stop) async {
|
||||
// The record is a tall page and the `ListView` under it is lazy, so a
|
||||
// default 800×600 harness never builds the sections below the timeline —
|
||||
// and every assertion about them would pass or fail on viewport height
|
||||
// rather than on the widget. Give it a phone's width and room to render.
|
||||
tester.view.physicalSize = const Size(390 * 3, 2600 * 3);
|
||||
tester.view.devicePixelRatio = 3;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => MaterialApp(home: DeliveryDetailsPage(stop: stop)),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Opens every folded group.
|
||||
///
|
||||
/// ── Why the tests now tap first ──
|
||||
///
|
||||
/// The ledger — distance, ETA, payment, the booking reference — used to be
|
||||
/// drawn flat, at the same weight as the outcome, which is what made a
|
||||
/// finished delivery read as a form to work through. It is folded now: three
|
||||
/// rows the rider taps only when somebody in an office asks him something.
|
||||
///
|
||||
/// The facts are unchanged and every assertion about them still holds. What
|
||||
/// changed is that reaching them costs a tap, so the tests take it — the
|
||||
/// alternative is asserting the *absence* of progressive disclosure, which
|
||||
/// is the thing being introduced.
|
||||
Future<void> unfold(WidgetTester tester) async {
|
||||
for (final title in const [
|
||||
'Delivery details',
|
||||
'Payment',
|
||||
'Order details',
|
||||
]) {
|
||||
// Matched inside its own control, not by text alone: the app bar's
|
||||
// title is *also* "Delivery details", and a finder that catches both
|
||||
// makes `tap` throw rather than fail an expectation — which is how one
|
||||
// ambiguous line reads as nine unrelated broken tests.
|
||||
final row = find.descendant(
|
||||
of: find.byType(InkWell),
|
||||
matching: find.text(title),
|
||||
);
|
||||
if (row.evaluate().isEmpty) continue;
|
||||
await tester.tap(row.first);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
||||
|
||||
group('the summary', () {
|
||||
testWidgets('leads with the outcome, in the outcome\'s colour', (
|
||||
tester,
|
||||
) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record());
|
||||
|
||||
// The word comes from `StopStatusX.label` — the canonical list — so a
|
||||
// stop cannot be called one thing on the list and another on the page
|
||||
// that list opens. It appears twice: the summary and the timeline's head
|
||||
// rung, which is the newest event.
|
||||
expect(find.text('Delivered'), findsNWidgets(2));
|
||||
final headline = tester.widget<Text>(find.text('Delivered').first);
|
||||
expect(headline.style!.color, ColorConstants.acceptGreen);
|
||||
|
||||
// Bigger than the route under it, which is bigger than the note under
|
||||
// that: the hierarchy is the answer, then which stop, then the detail.
|
||||
// The route is two `Text`s with a glyph between them — Poppins carries no
|
||||
// U+2192 and the arrow-as-character rendered as a tofu box — so `.first`
|
||||
// is the summary's copy and `.last` the one in ROUTE below it.
|
||||
final route = tester.widget<Text>(find.text('Vidhya Kitchen').first);
|
||||
expect(headline.style!.fontSize, greaterThan(route.style!.fontSize!));
|
||||
expect(find.byIcon(LucideIcons.arrowRight), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a cancelled stop never heads itself "Delivered"', (
|
||||
tester,
|
||||
) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record(status: 'cancelled'));
|
||||
|
||||
expect(find.text('Cancelled'), findsWidgets);
|
||||
// The *summary* is the outcome and it is Cancelled. `Delivered` appears
|
||||
// once and only once, as the open-ring rung for the step this record
|
||||
// never reached — see the group below.
|
||||
final summary = tester.widget<Text>(find.text('Cancelled').first);
|
||||
expect(summary.style!.fontSize, greaterThan(20));
|
||||
expect(find.text('Delivered'), findsOneWidget);
|
||||
expect(find.text('Did not happen'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a skipped stop leads with the reason it was put down', (
|
||||
tester,
|
||||
) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(
|
||||
tester,
|
||||
record(status: 'skipped', skipReason: 'Customer unreachable'),
|
||||
);
|
||||
|
||||
expect(find.text('Skipped'), findsWidgets);
|
||||
// The one thing on the record he cannot reconstruct for himself: he knows
|
||||
// who and where, and what he has forgotten by the afternoon is *why he
|
||||
// walked away*.
|
||||
expect(find.textContaining('Customer unreachable'), findsWidgets);
|
||||
});
|
||||
});
|
||||
|
||||
group('the timeline', () {
|
||||
testWidgets('runs oldest first, named for the leg', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record());
|
||||
|
||||
// The door arrival is not a rung: it folds into the outcome as a
|
||||
// duration, because "he got there 5m before he handed it over" is a
|
||||
// subtraction, not an event worth its own line.
|
||||
expect(find.text('Arrived at customer'), findsNothing);
|
||||
expect(find.text('5m at the door'), findsOneWidget);
|
||||
expect(find.text('Out for delivery'), findsOneWidget);
|
||||
|
||||
// ── Downwards, like every order-tracking screen ──
|
||||
//
|
||||
// Placed at the top, the outcome at the bottom, so the eye finishes on
|
||||
// what the record is about. The order is the whole reason this is a rail
|
||||
// and not a list — the rider takes the sequence off the geometry, not off
|
||||
// the clocks. `.last` is the timeline's rung; `.first` is the summary.
|
||||
final off = tester.getRect(find.text('Out for delivery'));
|
||||
final head = tester.getRect(find.text('Delivered').last);
|
||||
expect(off.top, lessThan(head.top));
|
||||
|
||||
// One rail, so every title starts at the same gutter.
|
||||
expect(head.left, closeTo(off.left, 0.5));
|
||||
});
|
||||
|
||||
testWidgets('a collection is named for its own leg', (tester) async {
|
||||
await pump(tester, record(type: 'pickup', status: 'picked'));
|
||||
|
||||
expect(find.text('Picked up'), findsWidgets);
|
||||
// On a collection `arrivedat` IS the counter, so it fills the same rung
|
||||
// the kitchen event fills on a round.
|
||||
expect(find.text('Arrived'), findsOneWidget);
|
||||
expect(find.text('Set off'), findsOneWidget);
|
||||
|
||||
// Words from the other leg must not leak across.
|
||||
expect(find.text('Out for delivery'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('an unstamped rung is absent, not a placeholder', (
|
||||
tester,
|
||||
) async {
|
||||
// A stop closed in bulk from Home never opened the map, so nothing
|
||||
// recorded a departure or an arrival. One event happened; one rung draws.
|
||||
await pump(
|
||||
tester,
|
||||
record(
|
||||
type: 'pickup',
|
||||
status: 'picked',
|
||||
started: false,
|
||||
arrived: false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Set off'), findsNothing);
|
||||
expect(find.text('Arrived'), findsNothing);
|
||||
|
||||
// None of the shapes a placeholder would take. A record that invents a
|
||||
// plausible step is worse than one with a hole in it, because the hole
|
||||
// can be investigated and the invention cannot be caught.
|
||||
for (final invented in const [
|
||||
'Pending',
|
||||
'Not recorded',
|
||||
'--:--',
|
||||
'12:00 AM',
|
||||
]) {
|
||||
expect(
|
||||
find.text(invented),
|
||||
findsNothing,
|
||||
reason: '"$invented" is a lie',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('the middle rung drops on its own', (tester) async {
|
||||
await pump(
|
||||
tester,
|
||||
record(type: 'pickup', status: 'picked', arrived: false),
|
||||
);
|
||||
expect(find.text('Set off'), findsOneWidget);
|
||||
expect(find.text('Arrived'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('the full history', () {
|
||||
testWidgets('every moment the app recorded gets a rung', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
final base = DateTime(2026, 8, 20, 9, 10);
|
||||
await pump(tester, {
|
||||
...record(),
|
||||
// What the booking carries: when the order was created.
|
||||
'starttime': base
|
||||
.subtract(const Duration(minutes: 25))
|
||||
.toIso8601String(),
|
||||
// What the rider's own screens stamped as they happened.
|
||||
OrderEvent.assigned: base
|
||||
.subtract(const Duration(minutes: 9))
|
||||
.toIso8601String(),
|
||||
OrderEvent.accepted: base.toIso8601String(),
|
||||
OrderEvent.arrivedAtPickup: base
|
||||
.add(const Duration(minutes: 18))
|
||||
.toIso8601String(),
|
||||
OrderEvent.pickedUp: base
|
||||
.add(const Duration(minutes: 25))
|
||||
.toIso8601String(),
|
||||
OrderEvent.outForDelivery: base
|
||||
.add(const Duration(minutes: 65))
|
||||
.toIso8601String(),
|
||||
});
|
||||
|
||||
for (final rung in const [
|
||||
'Assigned',
|
||||
'Accepted',
|
||||
'Arrived',
|
||||
'Picked up',
|
||||
'Out for delivery',
|
||||
'Delivered',
|
||||
]) {
|
||||
expect(find.text(rung), findsWidgets, reason: '$rung is missing');
|
||||
}
|
||||
|
||||
// ── A glyph per event, not one tick eight times ──
|
||||
//
|
||||
// A rail of identical marks says only "these all happened", which the
|
||||
// rail says by being solid. A rider scanning for the moment he took the
|
||||
// bag is looking for a shape.
|
||||
for (final glyph in const [
|
||||
LucideIcons.mapPinned,
|
||||
LucideIcons.squareCheckBig,
|
||||
LucideIcons.store,
|
||||
LucideIcons.package,
|
||||
LucideIcons.bike,
|
||||
]) {
|
||||
expect(find.byIcon(glyph), findsWidgets, reason: '$glyph is missing');
|
||||
}
|
||||
|
||||
// Oldest at the top, whatever order the stamps were assembled in.
|
||||
final tops = [
|
||||
for (final rung in const [
|
||||
'Assigned',
|
||||
'Accepted',
|
||||
'Arrived',
|
||||
'Picked up',
|
||||
'Out for delivery',
|
||||
'Delivered',
|
||||
])
|
||||
tester.getRect(find.text(rung).last).top,
|
||||
];
|
||||
for (var i = 1; i < tops.length; i++) {
|
||||
expect(
|
||||
tops[i],
|
||||
greaterThan(tops[i - 1]),
|
||||
reason: 'rung $i is out of order',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('a record from before the ledger keeps its three rungs', (
|
||||
tester,
|
||||
) async {
|
||||
// Everything already on a rider's device was written before these stamps
|
||||
// existed. It must degrade to what it has, not to a page of placeholders.
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record());
|
||||
|
||||
expect(find.text('Delivered'), findsNWidgets(2));
|
||||
// The door arrival is not a rung: it folds into the outcome as a
|
||||
// duration, because "he got there 5m before he handed it over" is a
|
||||
// subtraction, not an event worth its own line.
|
||||
expect(find.text('Arrived at customer'), findsNothing);
|
||||
expect(find.text('5m at the door'), findsOneWidget);
|
||||
expect(find.text('Out for delivery'), findsOneWidget);
|
||||
for (final absent in const [
|
||||
'Order placed',
|
||||
'Order accepted',
|
||||
'Arrived at pickup',
|
||||
'Picked up',
|
||||
]) {
|
||||
expect(find.text(absent), findsNothing, reason: '$absent was invented');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('the step that never happened', () {
|
||||
testWidgets('a skipped stop shows the delivery it did not make', (
|
||||
tester,
|
||||
) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(
|
||||
tester,
|
||||
record(status: 'skipped', skipReason: 'Customer unreachable'),
|
||||
);
|
||||
|
||||
// The one rung on this page allowed to exist without a timestamp: a
|
||||
// skipped stop has a delivery in its story that did not occur, and saying
|
||||
// so is the point of the record. It is an open ring, not a filled disc,
|
||||
// so it cannot read as a step that is merely pending.
|
||||
expect(find.text('Delivered'), findsOneWidget);
|
||||
expect(find.text('Not completed — return visit owed'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a completed stop has nothing left unreached', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record());
|
||||
|
||||
// Twice — the summary and the timeline's last rung — and never a third
|
||||
// time as a step still to come.
|
||||
expect(find.text('Delivered'), findsNWidgets(2));
|
||||
expect(find.text('Not completed — return visit owed'), findsNothing);
|
||||
expect(find.text('Did not happen'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('route', () {
|
||||
testWidgets('a round draws both ends; a parcel draws one', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record());
|
||||
// ── ROUTE is gone; its two addresses moved onto the journey ──
|
||||
//
|
||||
// Tracking said *when*, Route said *where*, and the rider joined them
|
||||
// himself. Each place is now written onto the rung that happened there.
|
||||
expect(find.text('ROUTE'), findsNothing);
|
||||
expect(find.text('JOURNEY'), findsOneWidget);
|
||||
|
||||
// Once now, not twice. The hero's quiet `from → to` line names both
|
||||
// ends; the journey names the place on the rung that happened there,
|
||||
// with its address under it. A second ROUTE section printing the same
|
||||
// two names again was the duplication this merge removed.
|
||||
expect(find.text('Vidhya Kitchen'), findsOneWidget);
|
||||
expect(
|
||||
find.text('78, Cross Cut Road, Gandhipuram'),
|
||||
findsOneWidget,
|
||||
reason:
|
||||
'the address hangs on the rung that happened at it. With no '
|
||||
'drop address on the booking the one address it has belongs to '
|
||||
'the door — the fallback ROUTE encoded and this inherited.',
|
||||
);
|
||||
|
||||
// A parcel booking is a first-mile collection *from* the customer, and
|
||||
// where it goes next is the hub's problem — there is no second name to
|
||||
// print, so there is no route to draw.
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
await pump(tester, record(type: 'pickup', status: 'picked'));
|
||||
expect(find.text('ROUTE'), findsNothing);
|
||||
expect(find.text('JOURNEY'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('the measurements', () {
|
||||
testWidgets('only what was measured is shown', (tester) async {
|
||||
await pump(
|
||||
tester,
|
||||
record(
|
||||
type: 'pickup',
|
||||
status: 'picked',
|
||||
compliance: const {'ontime': true, 'actualkm': 4.6, 'plannedkm': 4.1},
|
||||
),
|
||||
);
|
||||
await unfold(tester);
|
||||
|
||||
// ── The labelled stats strip is gone ──
|
||||
//
|
||||
// DISTANCE / TOTAL TIME / ORDERS was three captioned cells in a tinted
|
||||
// box — a dashboard widget, and a dashboard is for comparing many things
|
||||
// across records. There is one stop on this page and it is finished. The
|
||||
// figures still earn a place; they earn a line.
|
||||
expect(find.text('DISTANCE'), findsNothing);
|
||||
expect(find.text('TOTAL TIME'), findsNothing);
|
||||
|
||||
// The distance now reads once in that line, and once in the folded row
|
||||
// that names it.
|
||||
expect(find.text('4.6 km'), findsOneWidget);
|
||||
expect(find.textContaining('4.6 km'), findsWidgets);
|
||||
expect(find.text('4.1 km'), findsOneWidget);
|
||||
|
||||
final verdict = tester.widget<Text>(find.text('Yes'));
|
||||
expect(verdict.style!.color, ColorConstants.acceptGreen);
|
||||
|
||||
// A record that invents a plausible number cannot be caught.
|
||||
for (final invented in const ['0 km', '0.0 km', '0m', '₹0']) {
|
||||
expect(
|
||||
find.text(invented),
|
||||
findsNothing,
|
||||
reason: '"$invented" is a lie',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('an unmeasured stop says so out loud', (tester) async {
|
||||
await pump(tester, record(type: 'pickup', status: 'picked'));
|
||||
await unfold(tester);
|
||||
|
||||
// Said out loud, not implied by a missing row: a dropped row would be
|
||||
// indistinguishable from one that simply had not been scrolled to.
|
||||
expect(find.text('Not measured'), findsOneWidget);
|
||||
expect(find.text('Distance'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('order and payment', () {
|
||||
testWidgets('the reference is there and copyable', (tester) async {
|
||||
await pump(tester, record(type: 'pickup', status: 'picked'));
|
||||
await unfold(tester);
|
||||
expect(find.text('#DM101'), findsOneWidget);
|
||||
expect(find.byIcon(LucideIcons.copy), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a round shows no money and no shipment desk', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record(cash: 340));
|
||||
await unfold(tester);
|
||||
|
||||
// Subscribers pay the client monthly; `collectsCash` is false, so not
|
||||
// one collection row is drawn rather than printing "nothing to collect".
|
||||
//
|
||||
// The **group** still exists, because the stop payout lives in it —
|
||||
// earnings folded into Payment rather than standing as a second group
|
||||
// about money. What must be absent is any figure.
|
||||
expect(find.text('Due at this stop'), findsNothing);
|
||||
expect(find.text('Collected'), findsNothing);
|
||||
expect(find.textContaining('₹'), findsNothing);
|
||||
|
||||
// ── No fabricated payout ──
|
||||
//
|
||||
// `deliver` computes `ridercharges` server-side and returns nothing, and
|
||||
// `/miler/earnings` answers for a period rather than a stop. A number
|
||||
// here would be pay derived from a rate the app does not hold — the one
|
||||
// invention on this screen a rider would act on and be wrong about.
|
||||
expect(find.text('Stop payout'), findsOneWidget);
|
||||
expect(find.text('See Account'), findsOneWidget);
|
||||
|
||||
// Weight and parcel counts belong to the shipment desk, which is a
|
||||
// capability this line does not run.
|
||||
expect(find.text('Weight'), findsNothing);
|
||||
expect(find.text('Parcels handed over'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a parcel stop that collected shows what it took', (
|
||||
tester,
|
||||
) async {
|
||||
await pump(
|
||||
tester,
|
||||
record(type: 'pickup', status: 'picked', cash: 340, collected: 340),
|
||||
);
|
||||
await unfold(tester);
|
||||
|
||||
expect(find.text('Payment'), findsWidgets);
|
||||
// Due, collected, and again under EARNINGS — the cash that changed hands
|
||||
// is the one earnings figure this stop really has.
|
||||
expect(find.text('₹340'), findsNWidgets(3));
|
||||
// Earnings folded into Payment — one group about money, not two.
|
||||
expect(find.text('Payment'), findsWidgets);
|
||||
expect(find.text('Cash collected'), findsOneWidget);
|
||||
expect(find.text('Weight'), findsOneWidget);
|
||||
expect(find.text('2.4 kg'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a prepaid parcel stop draws no payment section', (
|
||||
tester,
|
||||
) async {
|
||||
await pump(tester, record(type: 'pickup', status: 'picked'));
|
||||
await unfold(tester);
|
||||
// Nothing was due and nothing was taken, so not one collection row is
|
||||
// drawn — a section spent saying a section does not apply is a section
|
||||
// too many. The group itself survives on the payout row alone.
|
||||
expect(find.text('Due at this stop'), findsNothing);
|
||||
expect(find.text('Collected'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// ROUTE — each end owns its own address
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// `_route` read `pickupaddress` **only** and hung it under the drop. On a
|
||||
// parcel booking that is right by construction — one address, and the
|
||||
// collection point IS the customer. On a milk run the payload carries both,
|
||||
// so the record printed the kitchen's street under the customer's name: the
|
||||
// most checkable fact on the one screen that exists to be checked.
|
||||
group('route', () {
|
||||
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
||||
|
||||
testWidgets('a round hangs each address on its own end', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
// Both ends need a rung to hang on: with no collection stamp the
|
||||
// journey has nothing that happened at the counter, and the kitchen's
|
||||
// address would have nowhere to go. That is the correct behaviour — a
|
||||
// place is only drawn on a moment the record actually holds — so the
|
||||
// fixture supplies the moment.
|
||||
// The fixture's own clock, not the wall clock: a rung stamped after the
|
||||
// record closed is dropped on purpose (a timeline that contradicts
|
||||
// itself is worse than a short one), and `record()` completes at a fixed
|
||||
// past moment.
|
||||
final at = now.subtract(const Duration(minutes: 40));
|
||||
await pump(tester, {
|
||||
...record(),
|
||||
'pickupaddress': '4 Cross, RS Puram, Coimbatore 641002',
|
||||
'dropaddress': '12, SNS Colony, Peelamedu, Coimbatore 641004',
|
||||
'arrived_at_pickup': at.toIso8601String(),
|
||||
'picked_up': at.add(const Duration(minutes: 4)).toIso8601String(),
|
||||
});
|
||||
|
||||
// Both addresses now sit on the journey, each on the rung that happened
|
||||
// at it — the counter on the collection, the door on the outcome — so
|
||||
// this reads the ladder rather than a separate ROUTE section.
|
||||
final kitchen = tester.getRect(
|
||||
find.text('4 Cross, RS Puram, Coimbatore 641002'),
|
||||
);
|
||||
final door = tester.getRect(
|
||||
find.text('12, SNS Colony, Peelamedu, Coimbatore 641004'),
|
||||
);
|
||||
|
||||
expect(
|
||||
kitchen.top,
|
||||
lessThan(door.top),
|
||||
reason: 'the kitchen is the first end of the journey',
|
||||
);
|
||||
expect(
|
||||
tester.getRect(find.text('Joe Mathew').last).top,
|
||||
lessThan(door.top),
|
||||
reason: "the customer's door hangs under the customer, not the kitchen",
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('one address still belongs to the door', (tester) async {
|
||||
// The fallback, unchanged. A payload with no separate drop must keep
|
||||
// hanging its one address exactly where it always hung.
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record());
|
||||
await unfold(tester);
|
||||
|
||||
expect(find.text('78, Cross Cut Road, Gandhipuram'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the same street is never printed twice', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
await pump(tester, record());
|
||||
await unfold(tester);
|
||||
expect(find.text('78, Cross Cut Road, Gandhipuram'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user