Files
doormile_customer_app/test/booking_flow_test.dart
Thiru-tenext 0d66627c3c Replace the customer app with Doormile CX
Book a pickup, track it to delivery — the rebuilt customer app.

- Design language from doormile-screens.html: brand #8F0F06, Manrope +
  Geist Mono (variable fonts), bordered cards instead of shadows, crimson
  brand headers, sliding tab indicator, mono for anything read digit by digit.
- lib/data (one live API implementation, plus a debug-only offline fake),
  lib/state, lib/ui (tokens, widgets, screens).
- 84 tests, plus a design snapshot harness that renders every screen with the
  real fonts: flutter test test/design_snapshot_test.dart --run-skipped
  --update-goldens

This replaces the previous app (pubspec 'doormile', app id
com.doormile.customer). That tree remains in history at 6c7d656; note its
android/app/google-services.json is not carried over, and the application id
here is in.doormile.customer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 16:07:33 +05:30

800 lines
28 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:doormile_cx/data/doormile_api.dart';
import 'package:doormile_cx/data/location_service.dart';
import 'package:doormile_cx/data/models.dart';
import 'package:doormile_cx/main.dart';
import 'package:doormile_cx/state/app_state.dart';
import 'package:doormile_cx/ui/widgets/buttons.dart';
import 'package:doormile_cx/ui/widgets/map_panel.dart';
import 'package:doormile_cx/ui/widgets/chrome.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:doormile_cx/data/dev_doormile_api.dart';
/// The app runs continuous animations (radar, live dots, map pulse, skeletons),
/// so `pumpAndSettle` would never return. Pump fixed slices instead.
Future<void> settle(WidgetTester tester, [int ms = 800]) async {
await tester.pump();
await tester.pump(Duration(milliseconds: ms));
await tester.pump(const Duration(milliseconds: 400));
}
/// Drains toast and fare-estimate timers so a test ends with nothing pending.
Future<void> drainToasts(WidgetTester tester) async {
await tester.pump(const Duration(seconds: 3));
}
/// The street line currently shown under the pickup map. Empty while the pin
/// is still being resolved, which is itself the thing worth asserting.
String pickupAddress(WidgetTester tester) {
final title = find.byKey(const ValueKey('pickupAddressTitle'));
return title.evaluate().isEmpty
? ''
: tester.widget<Text>(title).data ?? '';
}
Future<void> signIn(WidgetTester tester, {AppState? state}) async {
// A tall surface so whole screens fit without scrolling in the harness.
tester.view.physicalSize = const Size(1230, 9000);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(DoormileApp(initialState: state));
await settle(tester);
await tester.enterText(find.byType(TextField).first, '9876543210');
// The CTA only enables once the field rebuilds with a valid number.
await settle(tester, 200);
await tester.tap(find.text('Continue'));
await settle(tester);
// The OTP screen verifies as soon as the fourth digit lands.
final boxes = find.byType(TextField);
for (var i = 0; i < 4; i++) {
await tester.enterText(boxes.at(i), '1');
await tester.pump();
}
await settle(tester);
}
/// Moves a booking on the **server**, then lets the app read it back.
///
/// This is the only way a stage changes now: the app has no local stage
/// machine, so a test cannot step a booking without the backend doing it
/// first. Fire-and-pump rather than await — the fake answers on a delay the
/// test clock only advances while pumping.
Future<void> serverAdvance(
WidgetTester tester,
AppState state,
String reference,
JourneyStage stage,
) async {
final booking = state.byReference(reference)!;
unawaited(state.advanceStage(booking, stage));
await settle(tester, 1400);
}
/// Home ▸ BOOK ▸ confirm the pin ▸ destination step.
Future<void> openDestination(WidgetTester tester) async {
await tester.tap(find.text('Start booking'));
await settle(tester);
await tester.tap(find.text('Next'));
await settle(tester);
}
/// State chips scroll horizontally; drag later ones fully into view first.
Future<void> tapState(WidgetTester tester, String name) async {
final width = tester.view.physicalSize.width / tester.view.devicePixelRatio;
final chips = find
.ancestor(of: find.text('Tamil Nadu'), matching: find.byType(Scrollable))
.first;
for (var i = 0; i < 8; i++) {
final finder = find.text(name);
if (finder.evaluate().isNotEmpty) {
final rect = tester.getRect(finder);
if (rect.left >= 0 && rect.right <= width) break;
}
await tester.drag(chips, const Offset(-160, 0));
await tester.pump();
}
await tester.tap(find.text(name));
await settle(tester);
}
Future<void> pickPlace(WidgetTester tester, String state, String district) async {
await tapState(tester, state);
await tester.tap(find.text(district));
await settle(tester);
}
/// The fake standing in for the backend in this file.
///
/// A fresh one per test, installed as the singleton so the widgets under test
/// reach it exactly as they would reach the real API. It is the **only** source
/// of bookings here: nothing plants one in [AppState].
late DevDoormileApi api;
void main() {
setUp(() {
api = DevDoormileApi();
DoormileApi.overrideInstance(api);
// Widget tests run on a fake clock a real platform-channel reply never
// reaches, so the device GPS is replaced with a fixed fix. Without this the
// pickup screen would sit on "Getting address…" forever.
LocationService.instance = FixedLocationService();
});
tearDown(() => DoormileApi.overrideInstance(null));
testWidgets('signs in and lands on Home', (tester) async {
await signIn(tester);
expect(find.text('Send a parcel'), findsOneWidget);
expect(find.text('Start booking'), findsOneWidget);
// The seeded active booking surfaces on Home.
expect(find.text('Miler assigned'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('dragging the map re-resolves the pickup address',
(tester) async {
await signIn(tester);
await tester.tap(find.text('Start booking'));
await settle(tester);
// The pin resolved to a street, not a placeholder.
expect(find.text('Getting address…'), findsNothing);
final address = pickupAddress(tester);
expect(address, isNotEmpty);
// Move the map under the fixed pin, the way a customer corrects the door.
await tester.drag(find.byType(DmMapPanel), const Offset(-90, -70));
await tester.pump();
// The address goes quiet while the new coordinates are resolved…
expect(find.text('Getting address…'), findsOneWidget);
// …and comes back as a different place.
await settle(tester, 900);
expect(find.text('Getting address…'), findsNothing);
expect(pickupAddress(tester), isNot(address));
// Which is what Next now carries forward.
await tester.tap(find.text('Next'));
await settle(tester);
expect(find.text('Where are you sending it?'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('GPS, map drag and search all write one pickup point',
(tester) async {
final state = AppState(
location: FixedLocationService(lat: 11.0183, lng: 76.9725),
);
await signIn(tester, state: state);
await tester.tap(find.text('Start booking'));
await settle(tester);
DmMapPanel map() => tester.widget<DmMapPanel>(find.byType(DmMapPanel));
// 1 — the device fix. Address and coordinates land together.
final located = state.pickup!;
expect(state.hasPickupPoint, isTrue);
expect(located.lat, closeTo(11.0183, 0.0001));
expect(map().lat, located.lat);
expect(pickupAddress(tester), located.title);
// 2 — dragging the map moves the same point.
await tester.drag(find.byType(DmMapPanel), const Offset(-140, -110));
await settle(tester, 900);
final dragged = state.pickup!;
expect(dragged.lat, isNot(located.lat));
expect(dragged.hasLocation, isTrue);
expect(map().lat, dragged.lat);
expect(pickupAddress(tester), dragged.title);
// 3 — a search result overwrites it, coordinates included, and the map
// follows the address rather than staying where the drag left it.
await tester.tap(find.text('Change'));
await settle(tester);
await tester.tap(find.text('Brookefields Mall'));
await settle(tester);
final searched = state.pickup!;
expect(searched.title, 'Brookefields Mall');
expect(searched.lat, closeTo(10.9987, 0.0001));
expect(searched.lng, closeTo(76.9628, 0.0001));
expect(map().lat, searched.lat);
expect(map().lng, searched.lng);
expect(pickupAddress(tester), 'Brookefields Mall');
// …and that one point is what the booking carries forward.
await tester.tap(find.text('Next'));
await settle(tester);
expect(find.text('Where are you sending it?'), findsOneWidget);
expect(state.draftPickup!.lat, searched.lat);
expect(state.draftPickup!.lng, searched.lng);
expect(state.draftPickup!.title, 'Brookefields Mall');
await drainToasts(tester);
});
testWidgets('the customer is never shown raw coordinates', (tester) async {
final state = AppState(
location: FixedLocationService(lat: 11.0183, lng: 76.9725),
);
await signIn(tester, state: state);
await tester.tap(find.text('Start booking'));
await settle(tester);
// Whatever the pin resolves to, it reads as a place — never a decimal pair.
final coordinate = RegExp(r'-?\d{1,3}\.\d{3,}');
for (final text in tester.widgetList<Text>(find.byType(Text))) {
final data = text.data;
if (data == null) continue;
expect(
coordinate.hasMatch(data),
isFalse,
reason: 'coordinates leaked into the UI: "$data"',
);
}
expect(state.pickup!.hasLocation, isTrue);
await drainToasts(tester);
});
testWidgets('the customer sees their own position as a blue dot',
(tester) async {
final state = AppState(
location: FixedLocationService(
lat: 11.0183,
lng: 76.9725,
accuracy: 22,
),
);
await signIn(tester, state: state);
await tester.tap(find.text('Start booking'));
await settle(tester);
final dot = find.byKey(const ValueKey('deviceLocationDot'));
expect(dot, findsOneWidget);
expect(state.hasDeviceLocation, isTrue);
expect(state.deviceLat, closeTo(11.0183, 0.0001));
expect(state.deviceAccuracy, 22);
// Dragging moves the pickup pin. It must not move the customer: the dot
// stays where they actually are, which is the whole point of showing it.
await tester.drag(find.byType(DmMapPanel), const Offset(-150, -120));
await settle(tester, 900);
expect(dot, findsOneWidget);
expect(state.deviceLat, closeTo(11.0183, 0.0001));
expect(state.deviceLng, closeTo(76.9725, 0.0001));
expect(state.pickup!.lat, isNot(closeTo(11.0183, 0.0001)));
await drainToasts(tester);
});
testWidgets('no blue dot when the device refused a position',
(tester) async {
LocationService.instance =
FixedLocationService.denied(LocationDenial.serviceOff);
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Start booking'));
await settle(tester);
// A dot in the wrong place is worse than no dot.
expect(find.byKey(const ValueKey('deviceLocationDot')), findsNothing);
expect(state.hasDeviceLocation, isFalse);
await drainToasts(tester);
});
testWidgets('a refused location still lets the customer book',
(tester) async {
LocationService.instance =
FixedLocationService.denied(LocationDenial.denied);
await signIn(tester);
await tester.tap(find.text('Start booking'));
await settle(tester);
// The map falls back to the service area rather than an empty ocean, and
// the refusal is explained instead of silently swallowed.
expect(
find.textContaining('Allow location access'),
findsOneWidget,
);
expect(find.text('Search'), findsOneWidget);
// Booking is not blocked by it.
expect(
tester.widget<DmButton>(find.widgetWithText(DmButton, 'Next')).onPressed,
isNotNull,
);
await drainToasts(tester);
});
testWidgets('one destination, one package, every optional field skipped',
(tester) async {
final state = AppState();
await signIn(tester, state: state);
await openDestination(tester);
expect(find.text('Where are you sending it?'), findsOneWidget);
await pickPlace(tester, 'Tamil Nadu', 'Chennai');
// Settles into one card carrying the place and nothing else demanded.
//
// The package stepper and the optional delivery-details row were removed:
// one package to one door is the whole of the common path, so the step now
// asks for the destination and stops. `packageCount` still defaults to 1 in
// the model, which the read-only screens go on showing.
expect(find.text('Chennai, Tamil Nadu'), findsOneWidget);
expect(find.text('How many packages?'), findsNothing);
expect(find.text('Add exact delivery details'), findsNothing);
expect(find.text('Send to another place'), findsOneWidget);
await tester.tap(find.text('Next'));
await settle(tester);
expect(find.text('When should we come?'), findsOneWidget);
await tester.tap(find.text('2:00 – 4:00 PM').first);
await tester.pump();
await tester.tap(find.text('Review booking'));
await settle(tester);
expect(find.text('Review pickup details'), findsOneWidget);
expect(find.text('SENDING TO'), findsOneWidget);
expect(find.text('1 package'), findsOneWidget);
// Optional omissions are stated calmly, never as an error.
expect(
find.text('Not added — the Miler can confirm this at pickup.'),
findsOneWidget,
);
// A single destination shows no multi-destination summary.
expect(find.textContaining('destinations'), findsNothing);
await tester.tap(find.text('Book pickup'));
await settle(tester);
expect(find.text('Pickup booked'), findsOneWidget);
final booking = state.trackedBooking!;
expect(booking.destinations, hasLength(1));
expect(booking.totalPackages, 1);
expect(booking.destinations.first.trackingId, isNull);
await drainToasts(tester);
});
// ── These three used to drive the package stepper ──
//
// The stepper is gone from the destination step: one package to one door is
// the whole common path, so the screen no longer asks. The *logic* it drove
// is untouched — `setPackageCount` still clamps to `limits.maxPackages`, the
// totals still roll up, and the booking payload still carries them — so the
// counts are set on the state here and the assertions that matter (totals,
// the cap, what review and the booking show) are kept exactly as they were.
testWidgets('one destination, four packages', (tester) async {
final state = AppState();
await signIn(tester, state: state);
await openDestination(tester);
await pickPlace(tester, 'Tamil Nadu', 'Coimbatore');
state.setPackageCount(0, 4);
await tester.pump();
expect(state.draftTotalPackages, 4);
await tester.tap(find.text('Next'));
await settle(tester);
await tester.tap(find.text('2:00 – 4:00 PM').first);
await tester.pump();
await tester.tap(find.text('Review booking'));
await settle(tester);
expect(find.text('4 packages'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('three destinations with mixed counts total correctly on review',
(tester) async {
final state = AppState();
await signIn(tester, state: state);
await openDestination(tester);
await pickPlace(tester, 'Tamil Nadu', 'Chennai');
state.setPackageCount(0, 2);
await tester.pump();
await tester.tap(find.text('Send to another place'));
await settle(tester);
await pickPlace(tester, 'Kerala', 'Ernakulam');
await tester.tap(find.text('Send to another place'));
await settle(tester);
await pickPlace(tester, 'Karnataka', 'Bengaluru Urban');
state.setPackageCount(2, 3);
await tester.pump();
expect(state.draftDestinations, hasLength(3));
expect(state.draftTotalPackages, 6);
await tester.tap(find.text('Next'));
await settle(tester);
await tester.tap(find.text('2:00 – 4:00 PM').first);
await tester.pump();
await tester.tap(find.text('Review booking'));
await settle(tester);
expect(find.text('6 packages · 3 destinations'), findsOneWidget);
expect(find.text('Chennai, Tamil Nadu'), findsOneWidget);
expect(find.text('Ernakulam, Kerala'), findsOneWidget);
expect(find.text('Bengaluru Urban, Karnataka'), findsOneWidget);
await tester.tap(find.text('Book pickup'));
await settle(tester);
final booking = state.trackedBooking!;
expect(booking.destinations, hasLength(3));
expect(booking.totalPackages, 6);
expect(booking.packageSummary, '6 packages · 3 destinations');
// Before collection it is one pickup, however many places it serves.
expect(
state
.entriesOf(BookingStatus.active)
.where((e) => e.booking.reference == booking.reference)
.length,
1,
);
await drainToasts(tester);
});
testWidgets('the package cap holds', (tester) async {
final state = AppState();
await signIn(tester, state: state);
await openDestination(tester);
await pickPlace(tester, 'Tamil Nadu', 'Chennai');
// Push well past the cap; `setPackageCount` must clamp.
state.setPackageCount(0, 999);
await tester.pump();
expect(state.draftTotalPackages, state.limits.maxPackages);
// A full pickup cannot take another place either.
expect(state.canAddDestination, isFalse);
expect(
find.text('Up to ${state.limits.maxPackages} packages in one pickup.'),
findsOneWidget,
);
await drainToasts(tester);
});
testWidgets('the destination cap holds', (tester) async {
final state = AppState();
await signIn(tester, state: state);
await openDestination(tester);
const places = [
('Tamil Nadu', 'Chennai'),
('Kerala', 'Ernakulam'),
('Karnataka', 'Mysuru'),
('Tamil Nadu', 'Erode'),
('Kerala', 'Thrissur'),
];
for (var i = 0; i < places.length; i++) {
if (i > 0) {
await tester.tap(find.text('Send to another place'));
await settle(tester);
}
await pickPlace(tester, places[i].$1, places[i].$2);
}
expect(state.draftDestinations, hasLength(state.limits.maxDestinations));
expect(state.canAddDestination, isFalse);
expect(find.text('Send to another place'), findsNothing);
expect(
find.text(
'Up to ${state.limits.maxDestinations} places in one pickup. '
'Book another pickup for more.',
),
findsOneWidget,
);
await drainToasts(tester);
});
testWidgets('the picker offers serviceable districts only', (tester) async {
await signIn(tester);
await openDestination(tester);
// Nothing until a state is picked.
expect(find.text('Chennai'), findsNothing);
await tapState(tester, 'Tamil Nadu');
expect(find.text('Chennai'), findsOneWidget);
expect(find.text('Coimbatore'), findsOneWidget);
expect(find.text('Ernakulam'), findsNothing);
// Unsupported places are never disabled tiles — they are named once,
// quietly, and only because they are genuinely coming.
expect(find.text('Madurai'), findsNothing);
expect(find.text('Coming soon: Madurai'), findsOneWidget);
await tapState(tester, 'Kerala');
expect(find.text('Ernakulam'), findsOneWidget);
expect(find.text('Palakkad'), findsOneWidget);
expect(find.text('Chennai'), findsNothing);
expect(find.text('Coming soon: Kozhikode'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('a state with nothing open is not offered at all', (tester) async {
await signIn(tester);
await openDestination(tester);
// Puducherry is in the backend response but has no open district, so the
// customer never has to read past it.
expect(find.text('Puducherry'), findsNothing);
expect(find.text('Tamil Nadu'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('orders split into their own rows once collected', (tester) async {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Orders'));
await settle(tester);
// Before collection: one row, owned by the pickup.
expect(find.text('DM-482913'), findsOneWidget);
// After collection: the seeded multi-destination pickup is in transit, so
// each destination stands on its own with a quiet line back to the visit.
final booking = state.byReference('DM-471200')!;
expect(booking.hasOrders, isTrue);
expect(find.text('Pickup DM-471200'), findsNothing);
expect(find.text('Ernakulam, Kerala'), findsOneWidget);
expect(find.text('Coimbatore, Tamil Nadu'), findsOneWidget);
expect(
find.text('Collected on DM-471200'),
findsNWidgets(booking.destinations.length),
);
// Independently tracked.
expect(
booking.destinations.map((d) => d.trackingId).toSet().length,
booking.destinations.length,
);
// Opening one follows that order, not the whole visit.
await tester.tap(find.text('Ernakulam, Kerala'));
await settle(tester);
expect(find.text('To Ernakulam, Kerala'), findsOneWidget);
expect(find.text('YOUR PACKAGES'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('the customer timeline groups the operational states',
(tester) async {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Miler assigned'));
await settle(tester);
// Tracking opens on the three most recent updates; the rest of the journey
// is one tap away.
await tester.tap(find.text('All 7 stages'));
await settle(tester);
// Seven customer milestones, not nine backend states.
expect(find.text('Pickup booked'), findsWidgets);
expect(find.text('Pickup in progress'), findsOneWidget);
expect(find.text('Package collected'), findsOneWidget);
// Operational states are never timeline rows of their own.
expect(find.text('Miler arrived'), findsNothing);
expect(find.text('Order created'), findsNothing);
// Advancing to "on the way" keeps the milestone and moves the detail. It
// reads twice: once on the timeline, and once as the caption on the live
// card's own progress rail.
await serverAdvance(tester, state, 'DM-482913', JourneyStage.onTheWay);
expect(find.text('Pickup in progress'), findsNWidgets(2));
expect(find.textContaining('Miler on the way'), findsWidgets);
// The pill speaks customer language, not the operational stage.
expect(find.text('Order created'), findsNothing);
await drainToasts(tester);
});
testWidgets('packages stay together until the pickup is actually collected',
(tester) async {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Miler assigned'));
await settle(tester);
// The live card, not an eyebrow: a pickup in flight is the screen's one
// crimson surface.
expect(find.text('LIVE'), findsOneWidget);
expect(find.text('Cancel this pickup'), findsOneWidget);
// No per-package breakdown before collection.
expect(find.text('YOUR PACKAGES'), findsNothing);
await serverAdvance(tester, state, 'DM-482913', JourneyStage.pickedUp);
// Custody has changed hands: cancellation is no longer offered.
expect(find.text('Cancel this pickup'), findsNothing);
await serverAdvance(tester, state, 'DM-482913', JourneyStage.orderCreated);
final booking = state.byReference('DM-482913')!;
expect(booking.stage, JourneyStage.orderCreated);
expect(booking.destinations.first.trackingId, isNotNull);
await drainToasts(tester);
});
testWidgets('cancels the whole pickup from tracking', (tester) async {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Miler assigned'));
await settle(tester);
await tester.tap(find.text('Cancel this pickup'));
await settle(tester);
expect(find.text('Cancel this pickup?'), findsOneWidget);
await tester.tap(find.text('Package not ready'));
await tester.pump();
await tester.tap(find.text('Yes, cancel pickup'));
await settle(tester);
expect(find.text('Booking cancelled'), findsOneWidget);
final booking = state.byReference('DM-482913')!;
expect(booking.status, BookingStatus.cancelled);
await drainToasts(tester);
});
testWidgets('shows the empty service-area state', (tester) async {
await signIn(tester);
api.flags.emptyServiceAreas = true;
await openDestination(tester);
expect(find.text('No service areas available'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('shows the network error state with retry', (tester) async {
await signIn(tester);
api.flags.networkError = true;
await openDestination(tester);
expect(find.text("Couldn't load this"), findsOneWidget);
api.flags.networkError = false;
await tester.tap(find.text('Retry'));
await settle(tester);
expect(find.text('Tamil Nadu'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('a delivered order opens its receipt', (tester) async {
await signIn(tester);
await tester.tap(find.text('Orders'));
await settle(tester);
await tester.tap(find.text('Completed'));
await settle(tester);
await tester.tap(find.text('Salem, Tamil Nadu'));
await settle(tester);
expect(find.text('Delivered'), findsWidgets);
expect(find.text('Total paid'), findsOneWidget);
expect(find.text('₹64'), findsWidgets);
// What the Miler recorded at the door.
expect(find.text('COLLECTED AT YOUR DOOR'), findsOneWidget);
expect(find.text('Miler verified'), findsOneWidget);
// Once on the parcel block, once in the payment breakdown.
expect(find.text('1.6 kg'), findsNWidgets(2));
expect(find.text('Verified weight'), findsOneWidget);
expect(find.text('1 of 1'), findsOneWidget);
expect(find.textContaining('Weighed and photographed by'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('key screens survive large accessibility text', (tester) async {
// Any overflow throws, so simply building these screens is the assertion.
tester.platformDispatcher.textScaleFactorTestValue = 1.8;
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
await signIn(tester); // login + OTP
expect(find.text('Start booking'), findsOneWidget); // home
await openDestination(tester); // pickup point + destination
await pickPlace(tester, 'Tamil Nadu', 'Chennai');
expect(find.text('Chennai, Tamil Nadu'), findsOneWidget);
await tester.tap(find.text('Next'));
await settle(tester); // pickup time
await tester.tap(find.text('2:00 – 4:00 PM').first);
await tester.pump();
await tester.tap(find.text('Review booking'));
await settle(tester); // review
expect(find.text('Review pickup details'), findsOneWidget);
await tester.tap(find.text('Book pickup'));
await settle(tester); // confirmation
await tester.tap(find.text('Track pickup'));
await settle(tester); // tracking
expect(find.text('Pickup booked'), findsWidgets);
await drainToasts(tester);
});
testWidgets('sign up survives large accessibility text', (tester) async {
tester.platformDispatcher.textScaleFactorTestValue = 1.8;
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
tester.view.physicalSize = const Size(1230, 9000);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(const DoormileApp());
await settle(tester);
await tester.tap(find.text('Create account'));
await settle(tester);
// Building it is the assertion: any overflow in the hero or the form
// throws.
expect(find.text('Create your account.'), findsOneWidget);
await drainToasts(tester);
});
testWidgets('orders and account survive large accessibility text',
(tester) async {
tester.platformDispatcher.textScaleFactorTestValue = 1.8;
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
await signIn(tester);
await tester.tap(find.text('Orders'));
await settle(tester);
await tester.tap(find.text('Completed'));
await settle(tester);
await tester.tap(find.text('Salem, Tamil Nadu'));
await settle(tester); // receipt
await drainToasts(tester);
});
testWidgets('orders tabs filter by status', (tester) async {
final state = AppState();
await signIn(tester, state: state);
await tester.tap(find.text('Orders'));
await settle(tester);
// Each tab carries its own count, inside the header's segmented control —
// and the figure is the rows that tab will actually show, which for a
// collected multi-destination pickup is one per destination.
expect(
find.descendant(
of: find.byType(DmSegmented),
matching: find.text('${state.entriesOf(BookingStatus.active).length}'),
),
findsOneWidget,
);
await tester.tap(find.text('Cancelled'));
await settle(tester);
expect(find.text('Erode, Tamil Nadu'), findsOneWidget);
await drainToasts(tester);
});
}