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

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

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

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

90 lines
3.0 KiB
Dart

import 'package:doormile_cx/data/dev_doormile_api.dart';
import 'package:doormile_cx/data/models.dart';
import 'package:doormile_cx/main.dart';
import 'package:doormile_cx/state/app_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
/// The login screen must not walk a customer to the code screen when no code
/// was sent.
///
/// `sent: false` is the server saying it accepted the request and delivered
/// nothing — the SMS gateway or the mail relay refused it. Advancing anyway
/// puts four empty boxes in front of someone for a code that does not exist,
/// and every digit they try is wrong. The failure then reads as "I typed it
/// wrong" instead of "nothing arrived", which is the wrong problem to hand
/// somebody.
///
/// The resend control on the code screen has always honoured this flag. The
/// login screen did not, which is the asymmetry these tests pin down.
/// The app runs continuous animations, so `pumpAndSettle` never returns.
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));
}
class _SendFails extends DevDoormileApi {
@override
Future<OtpChallenge> sendOtp(String identifier) async =>
const OtpChallenge(sent: false);
}
class _SendSucceeds extends DevDoormileApi {
var calls = 0;
@override
Future<OtpChallenge> sendOtp(String identifier) async {
calls++;
return const OtpChallenge(sent: true);
}
}
Future<void> _tapContinue(WidgetTester tester, AppState state) async {
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');
await settle(tester, 200);
await tester.tap(find.text('Continue'));
await settle(tester);
}
void main() {
testWidgets('a code that was never sent does not open the code screen',
(tester) async {
await _tapContinue(tester, AppState(api: _SendFails()));
expect(
find.text('Verify your number'),
findsNothing,
reason: 'no code was delivered, so there is nothing to verify',
);
expect(
// The entry screen's own headline, which now sits on the brand field
// rather than as a heading inside the sheet.
find.text('Book a pickup from your door.'),
findsOneWidget,
reason: 'the customer stays where they can try again',
);
// Drain the toast timer so the test ends with nothing pending.
await tester.pump(const Duration(seconds: 4));
});
testWidgets('a delivered code does open the code screen', (tester) async {
final api = _SendSucceeds();
await _tapContinue(tester, AppState(api: api));
expect(find.text('Verify your number'), findsOneWidget);
expect(api.calls, 1);
await tester.pump(const Duration(seconds: 4));
});
}