Files
doormile_customer_app/test/design_snapshot_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

171 lines
5.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@Tags(['snapshot'])
library;
import 'package:doormile_cx/data/dev_doormile_api.dart';
import 'package:doormile_cx/data/doormile_api.dart';
import 'package:doormile_cx/data/location_service.dart';
import 'package:doormile_cx/main.dart';
import 'package:doormile_cx/state/app_state.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
/// Renders each screen at phone size and writes it to `test/snapshots/`.
///
/// This is a **design** check, not a regression check. A deliberate redesign
/// would fail every one of these, which is worthless as a test signal, so the
/// `snapshot` tag is skipped in a normal run (see `dart_test.yaml`) and the
/// pictures are rendered on demand:
///
/// flutter test test/design_snapshot_test.dart --run-skipped --update-goldens
///
/// It exists because the only honest way to review a visual rebuild is to look
/// at it, and this produces the same pictures on any machine, with the real
/// fonts, without a device attached.
///
/// The real fonts are loaded first — without that, Flutter's test harness draws
/// everything in Ahem's black boxes and the pictures are worthless.
Future<void> _loadFonts() async {
for (final (family, asset) in const [
('Manrope', 'assets/fonts/Manrope-VF.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',
'packages/lucide_icons_flutter/assets/lucide.ttf'),
]) {
final loader = FontLoader(family)..addFont(rootBundle.load(asset));
await loader.load();
}
}
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));
}
Future<void> _shot(WidgetTester tester, String name) async {
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile('snapshots/$name.png'),
);
}
void main() {
late DevDoormileApi api;
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('plugins.flutter.io/path_provider'),
(call) async => '/tmp',
);
api = DevDoormileApi();
DoormileApi.overrideInstance(api);
LocationService.instance = FixedLocationService();
});
tearDown(() => DoormileApi.overrideInstance(null));
testWidgets('screens', (tester) async {
await _loadFonts();
// One phone, portrait: 390x844 at 3x, the frame the design was drawn in.
tester.view.physicalSize = const Size(1170, 2532);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
final state = AppState();
await tester.pumpWidget(DoormileApp(initialState: state));
await _settle(tester);
// Image.asset resolves off the frame loop, so in a test it never paints
// unless it is decoded first — without this the wordmark is a blank gap in
// every picture of the entrance.
await tester.runAsync(() async {
await precacheImage(
const AssetImage('assets/images/doormile-logo.png'),
tester.element(find.byType(MaterialApp)),
);
});
await _settle(tester);
await _shot(tester, '01-sign-in');
await tester.tap(find.text('Create account'));
await _settle(tester);
await _shot(tester, '01b-sign-up');
await tester.tap(find.byIcon(LucideIcons.arrowLeft).first);
await _settle(tester);
await tester.enterText(find.byType(TextField).first, '9876543210');
await _settle(tester, 200);
await tester.tap(find.text('Continue'));
await _settle(tester);
await _shot(tester, '02-verify');
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);
await _shot(tester, '03-home');
await tester.tap(find.text('Orders'));
await _settle(tester);
await _shot(tester, '04-orders');
await tester.tap(find.text('Account'));
await _settle(tester);
await _shot(tester, '05-account');
await tester.tap(find.text('Home'));
await _settle(tester);
await tester.tap(find.text('Miler assigned'));
await _settle(tester);
await _shot(tester, '06-tracking');
await tester.tap(find.byIcon(LucideIcons.arrowLeft).first);
await _settle(tester);
// Booking flow.
await tester.tap(find.text('Start booking'));
await _settle(tester);
await _shot(tester, '07-pickup-point');
await tester.tap(find.text('Next'));
await _settle(tester);
await tester.tap(find.text('Tamil Nadu'));
await _settle(tester);
await tester.tap(find.text('Chennai'));
await _settle(tester);
await _shot(tester, '08-destination');
await tester.tap(find.text('Next'));
await _settle(tester);
await tester.tap(find.text('2:00 – 4:00 PM').first);
await _settle(tester);
await _shot(tester, '09-slot');
await tester.tap(find.text('Review booking'));
await _settle(tester);
await _shot(tester, '10-review');
await tester.tap(find.text('Book pickup'));
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 _settle(tester);
await tester.tap(find.text('Completed'));
await _settle(tester);
await tester.tap(find.text('Salem, Tamil Nadu'));
await _settle(tester);
await _shot(tester, '12-receipt');
await tester.pump(const Duration(seconds: 3));
});
}