@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 _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 _settle(WidgetTester tester, [int ms = 800]) async { await tester.pump(); await tester.pump(Duration(milliseconds: ms)); await tester.pump(const Duration(milliseconds: 400)); } Future _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: one screen, plus the two escape hatches. await tester.tap(find.text('Start booking')); await _settle(tester); await _shot(tester, '07-send-empty'); await tester.tap(find.text('Chennai').first); await _settle(tester); await _shot(tester, '08-send-ready'); // The window row is below the fold on a phone, and a ListView does not // build what it has not reached. // The window already carries the first available slot, so scroll to the // package row beneath it — a string that is there either way. await tester.dragUntilVisible( find.text('1 package'), find.byType(Scrollable).first, const Offset(0, -220), ); await _settle(tester); await tester.tap(find.text('Change').last); await _settle(tester); await _shot(tester, '09-window-sheet'); await tester.tap(find.textContaining('Use ').first); await _settle(tester); 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)); }); }