Redesign on the Stitch reference, in Plus Jakarta Sans
Four passes, and the shape they landed on. The type face is Plus Jakarta Sans (variable, wght 200-800), which brings a fix with it: it carries the rupee glyph and Switzer does not, so prices stop being set in Geist Mono to work around a missing character. Mono stays where it is earned - references and phone numbers, read digit by digit. Surfaces lift rather than outline. Cards carry two very soft shadow layers instead of a hairline, because eight outlined boxes down a screen read as a wireframe. The tab bar floats as a pill again for the same reason it was right to: it is now the same kind of object as everything above it. Screens: * Home is the greeting, the address, the sphere and one card. The card lost its progress bar - a filling line says "wait", and a parcel two days into a journey is not something anyone is waiting through - and gained the size that buys. * Orders cards are four bands: identity, destination, route, and whatever is happening right now. Plus a search field, because the list is the archive. * Tracking leads with the state at display size, then TRIP MILESTONES with a step counter, then the courier. * Review is a route thread over two particular cards. * Account opens on the person: avatar, name, and two counted figures. Three real bugs the redesign surfaced: * Quick dispatch handed `loadCities()` straight to a FutureBuilder, so the catalogue was refetched on every rebuild and Home never settled. * Order cards showed the whole visit's weight on one destination's row - somebody else's parcel. Per group now, and only once actually weighed. * The pickup window was printed beside "In transit", where it reads as a delivery time nobody promised. Nothing invented. The reference shows EXPRESS PRIORITY, CARBON OFFSET, CONCIERGE ELITE and hub-to-hub routing; this backend sends none of them, so they are absent rather than mocked up. flutter analyze: clean. flutter test: 88 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
@@ -7,7 +7,6 @@ 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:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -86,7 +85,7 @@ Future<void> serverAdvance(
|
||||
/// and the form is what the answer leads to, so every booking test is
|
||||
/// [openSend] followed by [pickCity].
|
||||
Future<void> openSend(WidgetTester tester) async {
|
||||
await tester.tap(find.text('BOOK'));
|
||||
await tester.tap(find.text('BOOK NOW'));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
@@ -99,6 +98,15 @@ Future<void> openSend(WidgetTester tester) async {
|
||||
/// 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<void> pickCity(
|
||||
WidgetTester tester,
|
||||
String name, {
|
||||
@@ -106,11 +114,20 @@ Future<void> pickCity(
|
||||
}) async {
|
||||
await tester.enterText(find.byType(TextField).first, name);
|
||||
await settle(tester, 250);
|
||||
// A `Text`, explicitly: `find.text` also matches `EditableText`, so once the
|
||||
// city's name is in the search box the plain finder returns two widgets and
|
||||
// `.first` is the box itself. Tapping that only refocuses it.
|
||||
// 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.byWidgetPredicate((w) => w is Text && w.data == name).first,
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(BottomSheet),
|
||||
matching: find.byWidgetPredicate((w) => w is Text && w.data == name),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
await settle(tester);
|
||||
|
||||
@@ -137,7 +154,7 @@ Future<void> pickCity(
|
||||
Future<void> openPickupEditor(WidgetTester tester) async {
|
||||
await openSend(tester);
|
||||
await pickCity(tester, 'Chennai');
|
||||
await tester.tap(find.text('FROM'));
|
||||
await tester.tap(find.text('ORIGIN'));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
@@ -192,17 +209,20 @@ void main() {
|
||||
await settle(tester);
|
||||
|
||||
// All four digits arrived, so it verified and moved on.
|
||||
expect(find.text('BOOK'), findsOneWidget);
|
||||
expect(find.text('BOOK NOW'), findsOneWidget);
|
||||
await drainToasts(tester);
|
||||
});
|
||||
|
||||
testWidgets('signs in and lands on Home', (tester) async {
|
||||
await signIn(tester);
|
||||
|
||||
expect(find.text('Send a parcel'), findsOneWidget);
|
||||
expect(find.text('BOOK'), findsOneWidget);
|
||||
// The seeded active booking surfaces on Home.
|
||||
expect(find.text('Miler assigned'), findsOneWidget);
|
||||
// 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);
|
||||
});
|
||||
|
||||
@@ -233,7 +253,7 @@ void main() {
|
||||
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('TO'), findsOneWidget);
|
||||
expect(find.text('DESTINATION'), findsOneWidget);
|
||||
expect(find.text(corrected), findsOneWidget);
|
||||
await drainToasts(tester);
|
||||
});
|
||||
@@ -282,7 +302,7 @@ void main() {
|
||||
// …and that one point is what the booking carries forward.
|
||||
await tester.tap(find.text('Use this address'));
|
||||
await settle(tester);
|
||||
expect(find.text('TO'), findsOneWidget);
|
||||
expect(find.text('DESTINATION'), findsOneWidget);
|
||||
expect(state.draftPickup!.lat, searched.lat);
|
||||
expect(state.draftPickup!.lng, searched.lng);
|
||||
expect(state.draftPickup!.title, 'Brookefields Mall');
|
||||
@@ -398,8 +418,8 @@ void main() {
|
||||
|
||||
// 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('FROM'), findsOneWidget);
|
||||
expect(find.text('TO'), findsOneWidget);
|
||||
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);
|
||||
@@ -408,9 +428,9 @@ void main() {
|
||||
// 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('Book pickup'), findsOneWidget);
|
||||
expect(find.text('Confirm Booking'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Book pickup'));
|
||||
await tester.tap(find.text('Confirm Booking'));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.text('Pickup booked'), findsOneWidget);
|
||||
@@ -441,7 +461,7 @@ void main() {
|
||||
expect(state.draftContactPhone, isNull);
|
||||
|
||||
// Changed, it is the number the Miler will ring at the door.
|
||||
await tester.tap(find.text('FROM'));
|
||||
await tester.tap(find.text('ORIGIN'));
|
||||
await settle(tester);
|
||||
await tester.enterText(find.byType(TextField).first, '9003144518');
|
||||
await settle(tester, 200);
|
||||
@@ -458,12 +478,12 @@ void main() {
|
||||
await openSend(tester);
|
||||
await pickCity(tester, 'Coimbatore');
|
||||
|
||||
expect(find.text('1 package'), findsOneWidget);
|
||||
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 packages'), findsOneWidget);
|
||||
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.
|
||||
@@ -471,7 +491,7 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(state.draftTotalPackages, state.limits.maxPackages);
|
||||
|
||||
await tester.tap(find.text('Book pickup'));
|
||||
await tester.tap(find.text('Confirm Booking'));
|
||||
await settle(tester);
|
||||
expect(state.trackedBooking!.totalPackages, state.limits.maxPackages);
|
||||
await drainToasts(tester);
|
||||
@@ -485,9 +505,10 @@ void main() {
|
||||
await pickCity(tester, 'Chennai');
|
||||
final first = state.draftSlotId;
|
||||
|
||||
// The window lives in the footer now, beside the price it is quoted
|
||||
// against, and the whole left column opens the sheet.
|
||||
await tester.tap(find.text('PICKUP'));
|
||||
// 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);
|
||||
|
||||
@@ -508,26 +529,26 @@ void main() {
|
||||
await openSend(tester);
|
||||
|
||||
// Step one is the states, not sixty district names.
|
||||
expect(find.text('Tamil Nadu'), findsOneWidget);
|
||||
expect(find.text('Kerala'), findsOneWidget);
|
||||
expect(find.text('Chennai'), findsNothing);
|
||||
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(find.text('Chennai'), findsOneWidget);
|
||||
expect(find.text('Coimbatore'), findsWidgets);
|
||||
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(find.text('Madurai'), findsNothing);
|
||||
expect(find.text('Puducherry'), findsNothing);
|
||||
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(find.text('Chennai'), findsNothing);
|
||||
expect(inSheet(find.text('Chennai')), findsNothing);
|
||||
|
||||
await pickCity(tester, 'Ernakulam');
|
||||
expect(find.textContaining('Ernakulam'), findsWidgets);
|
||||
@@ -538,19 +559,21 @@ void main() {
|
||||
final state = AppState();
|
||||
await signIn(tester, state: state);
|
||||
|
||||
await tester.tap(find.text('Orders'));
|
||||
await tester.tap(find.bySemanticsLabel('Orders'));
|
||||
await settle(tester);
|
||||
|
||||
// Before collection: one row, owned by the pickup.
|
||||
expect(find.text('DM-482913'), findsOneWidget);
|
||||
// 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, Kerala'), findsOneWidget);
|
||||
expect(find.text('Coimbatore, Tamil Nadu'), findsOneWidget);
|
||||
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
|
||||
@@ -558,7 +581,7 @@ void main() {
|
||||
// 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.text(group.trackingId!), findsOneWidget);
|
||||
expect(find.textContaining(group.trackingId!), findsOneWidget);
|
||||
}
|
||||
// Independently tracked.
|
||||
expect(
|
||||
@@ -566,10 +589,12 @@ void main() {
|
||||
booking.destinations.length,
|
||||
);
|
||||
|
||||
// Opening one follows that order, not the whole visit.
|
||||
await tester.tap(find.text('Ernakulam, Kerala'));
|
||||
// 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('To Ernakulam, Kerala'), findsOneWidget);
|
||||
expect(find.text('Ernakulam, Kerala'), findsWidgets);
|
||||
expect(find.text('YOUR PACKAGES'), findsOneWidget);
|
||||
expect(find.text('YOUR PACKAGES'), findsOneWidget);
|
||||
await drainToasts(tester);
|
||||
});
|
||||
@@ -579,7 +604,10 @@ void main() {
|
||||
final state = AppState();
|
||||
await signIn(tester, state: state);
|
||||
|
||||
await tester.tap(find.text('Miler assigned'));
|
||||
// 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
|
||||
@@ -613,12 +641,17 @@ void main() {
|
||||
final state = AppState();
|
||||
await signIn(tester, state: state);
|
||||
|
||||
await tester.tap(find.text('Miler assigned'));
|
||||
// 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 live card, not an eyebrow: a pickup in flight is the screen's one
|
||||
// crimson surface.
|
||||
expect(find.text('LIVE'), findsOneWidget);
|
||||
// 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);
|
||||
@@ -638,7 +671,10 @@ void main() {
|
||||
final state = AppState();
|
||||
await signIn(tester, state: state);
|
||||
|
||||
await tester.tap(find.text('Miler assigned'));
|
||||
// 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'));
|
||||
@@ -650,15 +686,19 @@ void main() {
|
||||
await tester.tap(find.text('Yes, cancel pickup'));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.text('Booking cancelled'), findsOneWidget);
|
||||
// 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 {
|
||||
await signIn(tester);
|
||||
// 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);
|
||||
|
||||
@@ -688,12 +728,12 @@ void main() {
|
||||
testWidgets('a delivered order opens its receipt', (tester) async {
|
||||
await signIn(tester);
|
||||
|
||||
await tester.tap(find.text('Orders'));
|
||||
await tester.tap(find.bySemanticsLabel('Orders'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('Completed'));
|
||||
await tester.tap(find.textContaining('Past'));
|
||||
await settle(tester);
|
||||
|
||||
await tester.tap(find.text('Salem, Tamil Nadu'));
|
||||
await tester.tap(find.text('Salem'));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.text('Delivered'), findsWidgets);
|
||||
@@ -717,12 +757,12 @@ void main() {
|
||||
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
|
||||
|
||||
await signIn(tester); // login + OTP
|
||||
expect(find.text('BOOK'), findsOneWidget); // home
|
||||
expect(find.text('BOOK NOW'), findsOneWidget); // home
|
||||
|
||||
await openSend(tester); // the whole booking, one screen
|
||||
await pickCity(tester, 'Chennai');
|
||||
|
||||
await tester.tap(find.text('Book pickup'));
|
||||
await tester.tap(find.text('Confirm Booking'));
|
||||
await settle(tester); // confirmation
|
||||
await tester.tap(find.text('Track pickup'));
|
||||
await settle(tester); // tracking
|
||||
@@ -760,11 +800,15 @@ void main() {
|
||||
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
|
||||
|
||||
await signIn(tester);
|
||||
await tester.tap(find.text('Orders'));
|
||||
await tester.tap(find.bySemanticsLabel('Orders'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('Completed'));
|
||||
// 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, Tamil Nadu'));
|
||||
await tester.tap(find.text('Salem'));
|
||||
await settle(tester); // receipt
|
||||
|
||||
await drainToasts(tester);
|
||||
@@ -774,22 +818,26 @@ void main() {
|
||||
final state = AppState();
|
||||
await signIn(tester, state: state);
|
||||
|
||||
await tester.tap(find.text('Orders'));
|
||||
await tester.tap(find.bySemanticsLabel('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.
|
||||
// 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.descendant(
|
||||
of: find.byType(DmSegmented),
|
||||
matching: find.text('${state.entriesOf(BookingStatus.active).length}'),
|
||||
find.textContaining(
|
||||
'Active (${state.entriesOf(BookingStatus.active).length})',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Cancelled'));
|
||||
// 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, Tamil Nadu'), findsOneWidget);
|
||||
expect(find.text('Erode'), findsOneWidget);
|
||||
await drainToasts(tester);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,12 +28,8 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
/// everything in Ahem's black boxes and the pictures are worthless.
|
||||
Future<void> _loadFonts() async {
|
||||
for (final (family, asset) in const [
|
||||
// Every cut, because Poppins is static: a FontLoader given only one file
|
||||
// renders w800 headings by smearing the one weight it has.
|
||||
('Poppins', 'assets/fonts/Poppins-Medium.ttf'),
|
||||
('Poppins', 'assets/fonts/Poppins-SemiBold.ttf'),
|
||||
('Poppins', 'assets/fonts/Poppins-Bold.ttf'),
|
||||
('Poppins', 'assets/fonts/Poppins-ExtraBold.ttf'),
|
||||
('PlusJakartaSans', 'assets/fonts/PlusJakartaSans-Variable.ttf'),
|
||||
('Switzer', 'assets/fonts/Switzer-Variable.ttf'),
|
||||
('GeistMono', 'assets/fonts/GeistMono-VF.ttf'),
|
||||
// The icon font too, or every glyph in the picture is a hollow box.
|
||||
('packages/lucide_icons_flutter/Lucide',
|
||||
@@ -152,25 +148,31 @@ void main() {
|
||||
await tester.tapAt(const Offset(200, 40)); // through the scrim
|
||||
await _settle(tester);
|
||||
|
||||
await tester.tap(find.text('Orders'));
|
||||
await tester.tap(find.bySemanticsLabel('Orders'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '04-orders');
|
||||
|
||||
await tester.tap(find.text('Account'));
|
||||
await tester.tap(find.bySemanticsLabel('Account'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '05-account');
|
||||
|
||||
await tester.tap(find.text('Home'));
|
||||
await tester.tap(find.bySemanticsLabel('Home'));
|
||||
await _settle(tester);
|
||||
await tester.tap(find.text('Miler assigned'));
|
||||
// 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 _shot(tester, '06-tracking');
|
||||
|
||||
await tester.dragUntilVisible(
|
||||
find.text('Cancel this pickup'),
|
||||
find.byType(Scrollable).first,
|
||||
const Offset(0, -200),
|
||||
);
|
||||
// `ensureVisible`, not `dragUntilVisible`.
|
||||
//
|
||||
// `IndexedStack` keeps all three tab roots alive under the pushed route,
|
||||
// so a positional `Scrollable` finder picks Home's list — which the
|
||||
// tracking screen is covering. `ensureVisible` finds the target's own
|
||||
// scrollable ancestor and needs no finder at all.
|
||||
await tester.ensureVisible(find.text('Cancel this pickup'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Cancel this pickup'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '06b-cancel-sheet');
|
||||
@@ -181,7 +183,7 @@ void main() {
|
||||
await _settle(tester);
|
||||
|
||||
// Booking: state, district, window, then one screen.
|
||||
await tester.tap(find.text('BOOK'));
|
||||
await tester.tap(find.text('BOOK NOW'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '07-destination-states');
|
||||
|
||||
@@ -189,7 +191,10 @@ void main() {
|
||||
await _settle(tester);
|
||||
await _shot(tester, '07b-destination-districts');
|
||||
|
||||
await tester.tap(find.text('Chennai').first);
|
||||
// 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 dev backend allows several destinations, so the row ticked rather
|
||||
// than closing the sheet — the choice is confirmed.
|
||||
@@ -204,25 +209,25 @@ void main() {
|
||||
|
||||
// The map editor behind the FROM row: the one screen in the flow whose
|
||||
// subject is a picture.
|
||||
await tester.tap(find.text('FROM'));
|
||||
await tester.tap(find.text('ORIGIN'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '10-pickup-map');
|
||||
await tester.tap(find.byIcon(LucideIcons.arrowLeft).first);
|
||||
await _settle(tester);
|
||||
|
||||
await tester.tap(find.text('Book pickup'));
|
||||
await tester.tap(find.text('Confirm Booking'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '11-booked');
|
||||
|
||||
// A finished order, for the receipt treatment.
|
||||
await tester.tap(find.text('Back to home'));
|
||||
await _settle(tester);
|
||||
await tester.tap(find.text('Orders'));
|
||||
await tester.tap(find.bySemanticsLabel('Orders'));
|
||||
await _settle(tester);
|
||||
await tester.tap(find.text('Completed'));
|
||||
await tester.tap(find.textContaining('Past'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '04b-orders-completed');
|
||||
await tester.tap(find.text('Salem, Tamil Nadu'));
|
||||
await tester.tap(find.text('Salem'));
|
||||
await _settle(tester);
|
||||
await _shot(tester, '12-receipt');
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:http/testing.dart';
|
||||
/// against the customer API document.
|
||||
///
|
||||
/// These are not parser tests. Every one of them exists because a plausible,
|
||||
/// readable, wrong spelling — `otp` for `code`, `lat` for `latitude`, `min` for
|
||||
/// readable, wrong spelling — `otp` for `code`, `latitude` for `lat`, `min` for
|
||||
/// `minRupees` — costs a whole feature at runtime and nothing at compile time.
|
||||
///
|
||||
/// Where the written contract and the running server disagree, these tests
|
||||
@@ -96,9 +96,18 @@ void main() {
|
||||
],
|
||||
);
|
||||
|
||||
// ── `lat`/`lng`, not `latitude`/`longitude` ──
|
||||
//
|
||||
// The written contract says the long spelling and this app sent it. The
|
||||
// server reads the short one, so it saw a pickup with no coordinates, could
|
||||
// not place it in a serviceable area, and answered every booking with
|
||||
// 422 `unserviceable`. Verified against production on 2026-09-23 one
|
||||
// request apart — same pickup, same slot: long spelling 422, short
|
||||
// spelling 201 and booking DM-252803.
|
||||
final pickup = t.sent.body['pickup'] as Map<String, dynamic>;
|
||||
expect(pickup['latitude'], 13.0827);
|
||||
expect(pickup['longitude'], 80.2707);
|
||||
expect(pickup['lat'], 13.0827);
|
||||
expect(pickup['lng'], 80.2707);
|
||||
expect(pickup.containsKey('latitude'), isFalse);
|
||||
|
||||
final destination =
|
||||
(t.sent.body['destinations'] as List).single as Map<String, dynamic>;
|
||||
@@ -176,7 +185,7 @@ void main() {
|
||||
|
||||
final body = t.sent.body;
|
||||
final pickup = body['pickup'] as Map<String, dynamic>;
|
||||
expect(pickup['latitude'], 13.0827);
|
||||
expect(pickup['lat'], 13.0827);
|
||||
expect(pickup['contactName'], 'Alex Kumar');
|
||||
expect(pickup['contactPhone'], '+919876543210');
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 633 KiB After Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 494 KiB After Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 377 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 183 KiB |