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>
This commit is contained in:
2026-09-15 16:07:33 +05:30
parent 6c7d656de5
commit 0d66627c3c
305 changed files with 21930 additions and 11056 deletions

799
test/booking_flow_test.dart Normal file
View File

@@ -0,0 +1,799 @@
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);
});
}