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>
287 lines
9.7 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|