Files
doormile_milderapp/test/shipment_review_test.dart
Thiru-tenext d7348e253f 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>
2026-08-22 05:40:35 +05:30

287 lines
9.7 KiB
Dart

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';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/Dashboard/pickups/shipment_review.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// REVIEW & PAY
///
/// ```
/// verify parcel ──▶ shipment desk ──▶ REVIEW & PAY ──▶ order created
/// (no review) from · to animated tick
/// weight · fare tracking no
/// payment
/// [ Pay ₹150 ]
/// ```
///
/// Two things are pinned here.
///
/// **The button names the amount.** A rider taking cash on a doorstep should
/// never have to remember what the total was, and the figure he quotes must be
/// the figure the screen collects — so both come from one place.
///
/// **The success state is earned.** The tick and "your order has been created"
/// appear only after the create actually succeeded. A failure keeps the rider
/// on the review with the reason, because the alternative is him riding off
/// with the customer's money believing an order exists.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
ServiceProfile.setActive(ServiceProfile.parcel);
});
Map<String, dynamic> capture({double price = 150}) => {
'captured': true,
'from': {'address': '9 Mettupalayam Road', 'pincode': '641002'},
'to': {
'address': '12 Cross Cut Road',
'pincode': '641012',
'city': 'Coimbatore',
},
'weight': 2.5,
'category': 'General',
'servicetype': 'Normal',
'zone': 'Local',
'price': price,
'currency': 'INR',
'approxkm': 4.2,
};
Future<void> pump(
WidgetTester tester, {
Map<String, dynamic>? shipment,
Future<String?> Function(ShipmentPayMethod, double)? onConfirm,
String tracking = 'DM-TRK-A1B2-42',
}) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: ShipmentReviewPage(
pickup: const {'orderid': '2001', 'pickupid': 2001},
shipment: shipment ?? capture(),
trackingNo: () => tracking,
onConfirm: onConfirm ?? (_, _) async => null,
),
),
),
);
await tester.pumpAndSettle();
}
group('the review', () {
testWidgets('shows the shipment and the payment on one screen', (
tester,
) async {
await pump(tester);
// Route
expect(find.text('FROM'), findsOneWidget);
expect(find.text('TO'), findsOneWidget);
expect(find.text('9 Mettupalayam Road'), findsOneWidget);
expect(find.text('12 Cross Cut Road'), findsOneWidget);
// Shipment
expect(find.text('Shipment'), findsOneWidget);
expect(find.text('2.5 kg'), findsOneWidget);
expect(find.text('General'), findsOneWidget);
expect(find.text('Local'), findsOneWidget);
// Payment
expect(find.text('Payment'), findsOneWidget);
expect(find.text('Cash'), findsOneWidget);
expect(find.text('UPI'), findsOneWidget);
});
testWidgets('the button says the amount out loud', (tester) async {
await pump(tester);
expect(
find.text('Pay ₹150'),
findsOneWidget,
reason: 'a rider at a doorstep should not have to remember the total',
);
});
testWidgets('the amount on the button matches the amount in the summary', (
tester,
) async {
await pump(tester, shipment: capture(price: 275));
expect(find.text('Pay ₹275'), findsOneWidget);
expect(find.text('₹275'), findsOneWidget); // the summary line
});
testWidgets('a zero-rated shipment asks for no money', (tester) async {
await pump(tester, shipment: capture(price: 0));
expect(find.text('Create order'), findsOneWidget);
expect(find.textContaining('Pay '), findsNothing);
});
testWidgets('the distance is labelled as the estimate it is', (
tester,
) async {
await pump(tester);
expect(find.text('≈ 4.2 km'), findsOneWidget);
expect(
find.textContaining('straight line'),
findsOneWidget,
reason:
'the fare comes from the zone, and saying so stops a rider '
'telling a customer the wrong basis for a charge',
);
});
});
group('paying', () {
testWidgets('passes the chosen method and the quoted amount', (
tester,
) async {
ShipmentPayMethod? seenMethod;
double? seenAmount;
await pump(
tester,
onConfirm: (m, a) async {
seenMethod = m;
seenAmount = a;
return null;
},
);
await tester.tap(find.text('UPI'));
await tester.pumpAndSettle();
await tester.tap(find.text('Pay ₹150'));
await tester.pumpAndSettle();
expect(seenMethod, ShipmentPayMethod.upi);
expect(seenAmount, 150);
});
testWidgets('success shows the tick, the copy and the tracking number', (
tester,
) async {
await pump(tester);
await tester.tap(find.text('Pay ₹150'));
await tester.pumpAndSettle();
expect(find.text('Your order has been created'), findsOneWidget);
expect(find.byIcon(LucideIcons.check), findsOneWidget);
expect(find.text('₹150 collected by Cash'), findsOneWidget);
expect(
find.text('DM-TRK-A1B2-42'),
findsOneWidget,
reason:
'a customer asking for the tracking number gets it on the '
'doorstep instead of a phone call to the hub',
);
expect(find.text('Done'), findsOneWidget);
});
testWidgets('a failure keeps him on the review with the reason', (
tester,
) async {
await pump(
tester,
onConfirm: (_, _) async => 'Payment could not be recorded — no network',
);
await tester.tap(find.text('Pay ₹150'));
await tester.pumpAndSettle();
expect(
find.text('Your order has been created'),
findsNothing,
reason:
'the order does not exist; claiming it does would send him '
'away with the customer\'s money and nothing on the hub screen',
);
expect(
find.textContaining('Payment could not be recorded'),
findsOneWidget,
);
// And he can try again.
expect(find.text('Pay ₹150'), findsOneWidget);
});
testWidgets('Done hands back the outcome the pickup flow reads', (
tester,
) async {
Map<String, dynamic>? popped;
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () async {
popped = await Navigator.of(context)
.push<Map<String, dynamic>>(
MaterialPageRoute(
builder: (_) => ShipmentReviewPage(
pickup: const {'orderid': '2001'},
shipment: capture(),
trackingNo: () => 'DM-TRK-1',
onConfirm: (_, _) async => null,
),
),
);
},
child: const Text('open'),
),
),
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
await tester.tap(find.text('Pay ₹150'));
await tester.pumpAndSettle();
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
// The shape `map.dart` already branches on, so the hand-off screen, the
// compliance stamp and the next-stop list keep working untouched.
expect(popped?['outcome'], 'completed');
expect(popped?['amountCollected'], 150);
expect(popped?['paymentMethod'], 'Cash');
});
});
group('the flow order', () {
test('logistics verifies, then captures, then reviews', () {
// The verify page drops its own review exactly where the shipment desk
// follows it — otherwise the rider confirms the parcel, describes the
// shipment, and confirms it all again.
expect(ServiceProfile.parcel.needsVerification, isTrue);
expect(ServiceProfile.parcel.capturesShipmentAddresses, isTrue);
expect(ServiceProfile.parcel.initiatesShipment, isTrue);
});
test('a milk run reaches none of it', () {
const p = ServiceProfile.milkMan;
expect(p.needsVerification, isFalse);
expect(p.capturesShipmentAddresses, isFalse);
expect(p.initiatesShipment, isFalse);
expect(p.collectsCash, isFalse);
});
});
}