Picks up where 0d66627 left off. Three things.
SIGN-IN, CUT TO THE QUESTION IT ASKS
It opened with a quarter-screen crimson hero carrying a lockup, a CUSTOMER
badge, a display-size promise and a sub-line, then a Phone/Email toggle, then a
labelled field, then a card explaining what a verification code is. Six things
to read before the one thing to do.
Nobody arrives at a sign-in screen needing to be sold the product. It is a
heading, a field and a button now, which is where Uber, Bolt and Porter put
them. The toggle became one quiet line under the field — choosing a method was
the first decision on the screen, before the customer had seen what was being
asked, and almost everyone uses the phone. The privacy card went: it explained
that a code would be sent, which the next screen demonstrates a second later.
`DmTextField.label` is nullable for this — "Enter your mobile number" above a
field captioned "Phone number" is one sentence printed twice.
BOOKING, DOWN TO ONE SCREENFUL
Landmark, recipient name and recipient phone are all optional and were all
drawn at the weight of the two fields that are not, putting six rows of "you
may skip this" between the address and the button. They fold behind one row
that counts what is filled in rather than just saying "optional".
Four crimson section heads became one. An accent used five times on a screen is
not an accent; crimson now marks the destination, which is the only choice that
changes the price.
Together those put the window, the package count and the CTA above the fold.
THE OFFLINE BUILD, BACK, UNDER TWO RULES
Deleted on 15 Sep after it cost two rounds of hunting for bookings in the admin
console that had never left the phone. That was not caused by the fake
existing — it was caused by a fake that did not announce itself and that
nothing stopped from shipping. Both are closed:
* `useDevData` is false in a release whatever the defines say;
* `describe` leads with DEV DATA (offline) and shows "no network" rather than
a host the build never contacts.
It is opt-in — `flutter run` still talks to the real API — which is the
property whose absence caused the original mess. `devAutoLogin` is deliberately
false under FLUTTER_TEST so the widget tests keep driving the real entrance.
flutter run --dart-define=DM_MOCK=true
86 tests green, analyze clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
115 lines
4.9 KiB
Dart
115 lines
4.9 KiB
Dart
import 'package:doormile_cx/data/app_config.dart';
|
|
import 'package:doormile_cx/data/location_service.dart';
|
|
import 'package:doormile_cx/main.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:integration_test/integration_test.dart';
|
|
|
|
/// Proves that a build carrying a real token reaches a real, authenticated
|
|
/// booking screen — on a device, against the real backend.
|
|
///
|
|
/// ── Why this file shrank ──
|
|
///
|
|
/// It used to walk the whole booking: sign in with `9876543210` and four 1s,
|
|
/// then pickup → destination → window → review → confirmation. Both halves of
|
|
/// that stopped being true.
|
|
///
|
|
/// * **The sign-in** only ever worked under the offline `DM_MOCK` build,
|
|
/// deleted on 15 Sep 2026 after it cost two rounds of hunting for bookings
|
|
/// in the admin console that had never left the phone. There is no longer a
|
|
/// four-digit code that signs anybody in.
|
|
/// * **The flow** was rebuilt into ONE screen plus two sheets. There is no
|
|
/// "Where are you sending it?" page, no "When should we come?" page and no
|
|
/// review page left to walk; the city is a horizontal strip and the window
|
|
/// is a modal sheet.
|
|
///
|
|
/// So every finder in it referred to something that no longer exists, and it
|
|
/// could not pass. What replaces it is deliberately narrower than what it
|
|
/// claimed to do, and everything in it has been checked against the current
|
|
/// screens rather than remembered.
|
|
///
|
|
/// **What this asserts:** the app adopts a server-issued token through the same
|
|
/// session store a normal login writes to, asks the backend who that is, opens
|
|
/// the real Home, and reaches a booking screen whose empty-draft state is
|
|
/// rendered from live serviceability. Every call in that chain is a real call.
|
|
///
|
|
/// **What it does not assert:** the booking itself. Completing it needs a city
|
|
/// tapped out of a server-loaded strip and a window chosen from a sheet, and a
|
|
/// test that guesses at those without ever having run is worth less than an
|
|
/// honest gap. Do that part by hand on the device — the button tells you where
|
|
/// you are, in this order:
|
|
///
|
|
/// "Pick a city to see the price" → "Choose a pickup window" → "Book pickup"
|
|
///
|
|
/// Run it:
|
|
///
|
|
/// flutter test integration_test/book_one_test.dart \
|
|
/// -d <device> \
|
|
/// --dart-define=DM_DEV_TOKEN=eyJ...
|
|
void main() {
|
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
testWidgets('a real token opens a real, authenticated booking screen',
|
|
(tester) async {
|
|
// ── Skip rather than fail confusingly ──
|
|
//
|
|
// With no token the app correctly opens the login screen, and every finder
|
|
// below would fail on a missing widget — which reads as "the app is broken"
|
|
// when the truth is "this run was given no identity". Say the missing thing
|
|
// by name, once.
|
|
if (!AppConfig.authBypassed) {
|
|
markTestSkipped(
|
|
'No DM_DEV_TOKEN (or DM_LOGIN_AS) — this test runs against the real '
|
|
'backend and needs a real session. Pass '
|
|
'--dart-define=DM_DEV_TOKEN=<server-issued token>.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Deterministic pin, so the pickup row never stops on an OS location dialog
|
|
// and resolves the same way every run.
|
|
LocationService.instance = FixedLocationService();
|
|
|
|
await tester.pumpWidget(const DoormileApp());
|
|
|
|
// The app runs continuous animations, so pumpAndSettle never returns. Pump
|
|
// fixed slices instead.
|
|
Future<void> hold([int ms = 900]) async {
|
|
await tester.pump();
|
|
await tester.pump(Duration(milliseconds: ms));
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
}
|
|
|
|
// Longer than the other holds on purpose: this is two network calls — the
|
|
// token is adopted into the session store, then `GET /customer/auth/me` is
|
|
// asked who it belongs to — and against a real server they are not instant.
|
|
await hold(4000);
|
|
|
|
expect(
|
|
find.text('Where should we pick up?'),
|
|
findsOneWidget,
|
|
reason: 'the app did not reach Home. A token that is expired or revoked '
|
|
'lands on the login screen by design — that is the session being '
|
|
'refused, not a bug in the app.',
|
|
);
|
|
debugPrint('DRIVER: real session restored from DM_DEV_TOKEN, on Home');
|
|
|
|
await tester.tap(find.text('BOOK'));
|
|
await hold(2500);
|
|
|
|
expect(find.text('Send a parcel'), findsOneWidget);
|
|
|
|
// The empty-draft state, in the button's own words. Reaching this proves
|
|
// the screen built against live serviceability rather than a fixture: the
|
|
// city strip is an async list off the server, and this is the label the
|
|
// screen shows while nothing in it is selected.
|
|
expect(
|
|
find.text('Pick a city to see the price'),
|
|
findsOneWidget,
|
|
reason: 'the booking screen did not reach its empty-draft state',
|
|
);
|
|
debugPrint('DRIVER: on the booking screen, awaiting a city — '
|
|
'finish by hand from here');
|
|
});
|
|
}
|