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>
616 lines
25 KiB
Dart
616 lines
25 KiB
Dart
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);
|
||
});
|
||
});
|
||
}
|