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:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.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 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 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(title).data ?? ''; } Future 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)); // The splash holds for its own minimum — and the waits in front of it — before handing over, so // the entrance is waited out once, here, rather than by lengthening every // settle in the suite. await settle(tester, 4400); 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); // One field, not four boxes, and it verifies as soon as the fourth digit // lands. Entered in one go on purpose: four separate fields dropped a digit // whenever a keystroke arrived while focus was moving between them, which is // what typing at any normal speed does. await tester.enterText(find.byType(TextField).first, '1111'); 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 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 ▸ the BOOK orb ▸ the destination sheet. /// /// BOOK no longer opens the form. It opens one question — where is it going — /// and the form is what the answer leads to, so every booking test is /// [openSend] followed by [pickCity]. Future openSend(WidgetTester tester) async { await tester.tap(find.text('BOOK NOW')); await settle(tester); } /// Answers the destination sheet, then the window sheet behind it. /// /// Two sheets, because the customer now chooses their own pickup window /// instead of being handed the first one silently. [takeWindow] false leaves /// the booking without a slot, which is what dismissing the sheet does. /// /// The destination is reached by searching rather than by opening its state: /// search cuts across every state from the first step, and it is the path a /// customer who already knows where they are sending actually takes. /// A finder restricted to the open bottom sheet. /// /// Home's live card heads with the district alone — "Chennai", not /// "Chennai, Tamil Nadu" — so a bare `find.text('Chennai')` matches the screen /// behind the scrim as well as the row in the sheet. Every assertion about /// what a sheet is offering goes through this. Finder inSheet(Finder matching) => find.descendant(of: find.byType(BottomSheet), matching: matching); Future pickCity( WidgetTester tester, String name, { bool takeWindow = true, }) async { await tester.enterText(find.byType(TextField).first, name); await settle(tester, 250); // A `Text` inside the sheet, explicitly. // // `find.text` also matches `EditableText`, so once the city's name is in the // search box the plain finder returns the box as well — and Home, behind the // scrim, now heads its live card with the district alone, so an unscoped // finder can land on a widget the sheet is covering. Scoping to the modal // route fixes both. await tester.tap( find .descendant( of: find.byType(BottomSheet), matching: find.byWidgetPredicate((w) => w is Text && w.data == name), ) .first, ); await settle(tester); // Where the server allows several destinations the row ticks rather than // closing the sheet, so the choice has to be confirmed. Where it caps the // booking at one there is no button and the tap already committed. final send = find.textContaining('Send to '); if (send.evaluate().isNotEmpty) { await tester.tap(send.first); await settle(tester); } final use = find.textContaining('Use '); if (takeWindow && use.evaluate().isNotEmpty) { await tester.tap(use.first); await settle(tester); } else if (!takeWindow) { await tester.tapAt(const Offset(200, 40)); // dismiss through the scrim await settle(tester); } } /// Opens the pickup-point editor from the send screen's FROM row. Future openPickupEditor(WidgetTester tester) async { await openSend(tester); await pickCity(tester, 'Chennai'); await tester.tap(find.text('ORIGIN')); 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('the code is one field, typed a digit at a time', (tester) async { // The regression this locks: the code used to be four `TextField`s that // passed focus along, and a keystroke arriving mid-transition was dropped. // The customer typed four digits, three arrived, and the screen answered // "That code did not match" — blaming them for its own race. tester.view.physicalSize = const Size(1230, 9000); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.reset); await tester.pumpWidget(const DoormileApp()); // The splash holds for its own minimum — and the waits in front of it — before handing over, so // the entrance is waited out once, here, rather than by lengthening every // settle in the suite. await settle(tester, 4400); await tester.enterText(find.byType(TextField).first, '9876543210'); await settle(tester, 200); await tester.tap(find.text('Continue')); await settle(tester); // One field. Nothing to pass focus between, so nothing to drop it in. expect(find.byType(TextField), findsOneWidget); final field = find.byType(TextField).first; for (final sofar in ['1', '12', '123', '1234']) { await tester.enterText(field, sofar); await tester.pump(); } await settle(tester); // All four digits arrived, so it verified and moved on. expect(find.text('BOOK NOW'), findsOneWidget); await drainToasts(tester); }); testWidgets('signs in and lands on Home', (tester) async { await signIn(tester); // Home greets by name, carries the sphere, and ends on the live card. expect(find.text('Hello, Joe'), findsOneWidget); expect(find.text('BOOK NOW'), findsOneWidget); // The seeded active booking surfaces on Home. Its state is the uppercase // micro-label beside the destination, in the chip vocabulary — "Assigned", // not the rail's "Miler assigned". expect(find.text('ASSIGNED'), findsOneWidget); await drainToasts(tester); }); testWidgets('dragging the map re-resolves the pickup address', (tester) async { await signIn(tester); await openPickupEditor(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 the editor carries back to the send screen. final corrected = pickupAddress(tester); await tester.tap(find.text('Use this address')); await settle(tester); // Back on the send screen: its title is the route, not a heading. expect(find.text('DESTINATION'), findsOneWidget); expect(find.text(corrected), 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 openPickupEditor(tester); DmMapPanel map() => tester.widget(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('Use this address')); await settle(tester); expect(find.text('DESTINATION'), 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 openPickupEditor(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(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 openPickupEditor(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 openPickupEditor(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 openPickupEditor(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( find.widgetWithText(DmButton, 'Use this address'), ) .onPressed, isNotNull, ); await drainToasts(tester); }); testWidgets('one screen books a parcel, every optional field skipped', (tester) async { final state = AppState(); await signIn(tester, state: state); await openSend(tester); // BOOK asks one question, and it is the only thing on screen. expect(find.text('Where is it going?'), findsOneWidget); expect(find.text('Flat or house number'), findsNothing); await pickCity(tester, 'Chennai'); // Everything else the booking needs is on one screen, and the route to // the top of it is two rows with no labels but FROM and TO. expect(find.text('ORIGIN'), findsOneWidget); expect(find.text('DESTINATION'), findsOneWidget); // Nothing is asked for here at all any more: no address fields, and no // disclosure hiding three more. The route, the packages and the button. expect(find.byType(TextField), findsNothing); expect(find.text('Add landmark or recipient details'), findsNothing); // The customer answered the window sheet on the way in, so the booking // already carries a slot they chose. expect(state.draftSlotId, isNotNull); expect(find.text('Confirm Booking'), findsOneWidget); await tester.tap(find.text('Confirm Booking')); 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); // Skipped fields are absent, not empty strings. expect(booking.details.exactAddress, isNull); await drainToasts(tester); }); testWidgets('the pickup contact defaults to the account and can be changed', (tester) async { final state = AppState(); await signIn(tester, state: state); await openPickupEditor(tester); // Opens on the signed-in customer's own number, without the country code, // because the field carries its own prefix. final contact = find.widgetWithText(TextField, '9876543210'); expect(contact, findsOneWidget); // Left alone it stays null, so the request carries the account rather than // a copy of it that can drift. await tester.tap(find.text('Use this address')); await settle(tester); expect(state.draftContactPhone, isNull); // Changed, it is the number the Miler will ring at the door. await tester.tap(find.text('ORIGIN')); await settle(tester); await tester.enterText(find.byType(TextField).first, '9003144518'); await settle(tester, 200); await tester.tap(find.text('Use this address')); await settle(tester); expect(state.draftContactPhone, '+91 9003144518'); await drainToasts(tester); }); testWidgets('the package stepper is on the screen and clamps', (tester) async { final state = AppState(); await signIn(tester, state: state); await openSend(tester); await pickCity(tester, 'Coimbatore'); expect(find.text('1 parcel'), findsOneWidget); await tester.tap(find.byIcon(LucideIcons.plus)); await tester.pump(); await tester.tap(find.byIcon(LucideIcons.plus)); await tester.pump(); expect(find.text('3 parcels'), findsOneWidget); expect(state.draftTotalPackages, 3); // Push past the cap on the state; the clamp is the model's, not the UI's. state.setPackageCount(0, 999); await tester.pump(); expect(state.draftTotalPackages, state.limits.maxPackages); await tester.tap(find.text('Confirm Booking')); await settle(tester); expect(state.trackedBooking!.totalPackages, state.limits.maxPackages); await drainToasts(tester); }); testWidgets('the window sheet changes the window', (tester) async { final state = AppState(); await signIn(tester, state: state); await openSend(tester); await pickCity(tester, 'Chennai'); final first = state.draftSlotId; // The window is a row on the review list, under the route. It carries no // label — the clock glyph and the time are the whole row — so it is found // by the window itself. await tester.tap(find.textContaining('2:00 – 4:00 PM').first); await settle(tester); expect(find.text('Pickup window'), findsOneWidget); // A different window, then commit it. await tester.tap(find.text('4:00 – 6:00 PM').first); await tester.pump(); await tester.tap(find.textContaining('Use 4:00 – 6:00 PM')); await settle(tester); expect(state.draftSlotId, isNot(first)); expect(find.text('Pickup window'), findsNothing); await drainToasts(tester); }); testWidgets('the destination sheet browses state then district', (tester) async { await signIn(tester); await openSend(tester); // Step one is the states, not sixty district names. expect(inSheet(find.text('Tamil Nadu')), findsOneWidget); expect(inSheet(find.text('Kerala')), findsOneWidget); expect(inSheet(find.text('Chennai')), findsNothing); // Step two is that state's districts. await tester.tap(find.text('Tamil Nadu')); await settle(tester); expect(inSheet(find.text('Chennai')), findsOneWidget); expect(inSheet(find.text('Coimbatore')), findsWidgets); // Districts that are not open are never offered, and neither is a state // whose every district is closed. expect(inSheet(find.text('Madurai')), findsNothing); expect(inSheet(find.text('Puducherry')), findsNothing); // Back out, then let search cut across states: Ernakulam is in Kerala and // the customer should not have to know that to find it. await tester.tap(find.byIcon(LucideIcons.arrowLeft).first); await settle(tester); expect(inSheet(find.text('Chennai')), findsNothing); await pickCity(tester, 'Ernakulam'); expect(find.textContaining('Ernakulam'), findsWidgets); 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.bySemanticsLabel('Orders')); await settle(tester); // Before collection: one row, owned by the pickup. The reference shares a // line with the date and the contents now, so it is a span inside a // paragraph rather than a Text of its own. expect(find.textContaining('DM-482913'), findsOneWidget); // After collection: the seeded multi-destination pickup is in transit, so // each destination stands on its own row. final booking = state.byReference('DM-471200')!; expect(booking.hasOrders, isTrue); expect(find.text('Pickup DM-471200'), findsNothing); expect(find.text('Ernakulam'), findsOneWidget); expect(find.text('Coimbatore'), findsOneWidget); // Each row is identified by its OWN tracking number, not by the visit that // produced it. "Collected on DM-471200" used to run under both rows; it // answers a question asked about an order, not while scanning a list of // them, so it moved to the order's own screen. expect(find.text('Collected on DM-471200'), findsNothing); for (final group in booking.destinations) { expect(find.textContaining(group.trackingId!), findsOneWidget); } // Independently tracked. expect( booking.destinations.map((d) => d.trackingId).toSet().length, booking.destinations.length, ); // Opening one follows that order, not the whole visit: the head card's // route ends at that destination, and the reference is its own tracking id. await tester.tap(find.text('Ernakulam')); await settle(tester); expect(find.text('Ernakulam, Kerala'), findsWidgets); expect(find.text('YOUR PACKAGES'), findsOneWidget); expect(find.text('YOUR PACKAGES'), findsOneWidget); await drainToasts(tester); }); testWidgets('the timeline is the shipment, not the pickup visit', (tester) async { final state = AppState(); await signIn(tester, state: state); // The live card, by its reference. Home's quick-dispatch chips are // named after cities too, so a bare 'Chennai' finder now hits a // shortcut that starts a new booking instead. await tester.tap(find.text('DM-482913')); await settle(tester); // The whole shipment is on screen the moment tracking opens — five rows // fit where the three-row summary and its "Full timeline" action used to. expect(find.text('Full timeline'), findsNothing); // Five shipment milestones. Everything before the order exists is one // row — the whole pickup visit, which the live card above narrates by // name, with a distance and an ETA. expect(find.text('Booking created'), findsOneWidget); expect(find.text('Order created'), findsOneWidget); expect(find.text('In transit'), findsOneWidget); expect(find.text('Out for delivery'), findsOneWidget); expect(find.text('Delivered'), findsOneWidget); // The visit's own stages are never rows of their own. expect(find.text('Pickup in progress'), findsNothing); expect(find.text('Package collected'), findsNothing); expect(find.text('Miler arrived'), findsNothing); // Advancing through the visit does not add a row — it moves the detail // under the one row the whole visit occupies. await serverAdvance(tester, state, 'DM-482913', JourneyStage.onTheWay); expect(find.text('Booking created'), findsOneWidget); expect(find.textContaining('Miler on the way'), findsWidgets); await drainToasts(tester); }); testWidgets('packages stay together until the pickup is actually collected', (tester) async { final state = AppState(); await signIn(tester, state: state); // The live card, by its reference. Home's quick-dispatch chips are // named after cities too, so a bare 'Chennai' finder now hits a // shortcut that starts a new booking instead. await tester.tap(find.text('DM-482913')); await settle(tester); // The head card names the state in a pill and draws the route as a rail. // It used to be a filled crimson card headed "LIVE" — crimson is the // action colour, and tracking is a screen you read. expect(find.text('Miler assigned'), findsWidgets); expect(find.text('12 Nehru Street'), findsWidgets); 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); // The live card, by its reference. Home's quick-dispatch chips are // named after cities too, so a bare 'Chennai' finder now hits a // shortcut that starts a new booking instead. await tester.tap(find.text('DM-482913')); 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); // The head card carries the state in its pill now, not as a headline. expect(find.textContaining('Cancelled'), findsWidgets); final booking = state.byReference('DM-482913')!; expect(booking.status, BookingStatus.cancelled); await drainToasts(tester); }); testWidgets('shows the empty service-area state', (tester) async { // Set before signing in. Home's quick-dispatch row calls `loadCities()` // as soon as it builds, which warms `statesCache` — good for the sheet, // which now opens instantly, and fatal to a flag flipped afterwards. api.flags.emptyServiceAreas = true; await signIn(tester); await openSend(tester); expect(find.text('No cities open yet'), findsOneWidget); await drainToasts(tester); }); testWidgets('shows the network error state with retry', (tester) async { await signIn(tester); api.flags.networkError = true; await openSend(tester); expect(find.text("Couldn't load this"), findsOneWidget); api.flags.networkError = false; await tester.tap(find.text('Retry')); await settle(tester); // It comes back on the step it failed on: the states, not a blank sheet. expect(find.text('Tamil Nadu'), findsOneWidget); await tester.tap(find.text('Tamil Nadu')); await settle(tester); expect(find.text('Coimbatore'), findsWidgets); await drainToasts(tester); }); testWidgets('a delivered order opens its receipt', (tester) async { await signIn(tester); await tester.tap(find.bySemanticsLabel('Orders')); await settle(tester); await tester.tap(find.textContaining('Past')); await settle(tester); await tester.tap(find.text('Salem')); 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('BOOK NOW'), findsOneWidget); // home await openSend(tester); // the whole booking, one screen await pickCity(tester, 'Chennai'); await tester.tap(find.text('Confirm Booking')); await settle(tester); // confirmation await tester.tap(find.text('Track pickup')); await settle(tester); // tracking // The live card names the stage; the rail names the shipment milestone. expect(find.text('Booking created'), 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()); // The splash holds for its own minimum — and the waits in front of it — before handing over, so // the entrance is waited out once, here, rather than by lengthening every // settle in the suite. await settle(tester, 4400); 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.bySemanticsLabel('Orders')); await settle(tester); // At 1.8x the three tabs are wider than the screen, so the second one has // to be scrolled to before it can be tapped. await tester.ensureVisible(find.textContaining('Past')); await tester.pump(); await tester.tap(find.textContaining('Past')); await settle(tester); await tester.tap(find.text('Salem')); 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.bySemanticsLabel('Orders')); await settle(tester); // Each tab carries its own count — the figure is the rows that tab will // actually show, which for a collected multi-destination pickup is one // per destination. The count shares a paragraph with the label, so it is // a span rather than a Text of its own. expect( find.textContaining( 'Active (${state.entriesOf(BookingStatus.active).length})', ), findsOneWidget, ); // The tab row scrolls — three labels plus their counts do not fit a 390pt // phone at large text sizes. await tester.ensureVisible(find.textContaining('Cancelled').first); await tester.pump(); await tester.tap(find.textContaining('Cancelled').first); await settle(tester); expect(find.text('Erode'), findsOneWidget); await drainToasts(tester); }); }