import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:doormile_cx/data/dev_doormile_api.dart'; import 'package:doormile_cx/data/doormile_api.dart'; import 'package:doormile_cx/data/models.dart'; import 'package:doormile_cx/state/app_scope.dart'; import 'package:doormile_cx/state/app_state.dart'; import 'package:doormile_cx/ui/screens/home_screen.dart'; /// ───────────────────────────────────────────────────────────────────────── /// HOME SAYS HOW FAR DOORMILE GOES /// /// One line under the sphere, and it has now failed to appear twice for two /// different reasons — both silent, because an absent widget looks exactly /// like a widget that decided it had nothing to say: /// /// 1. `cachedCities` walked every state in `statesCache`, including the ones /// with no open districts, and `loadCities` only ever fetches districts for /// the open ones. It found a hole and reported nothing cached. `loadCities` /// returned eleven cities and the getter returned null beside it. /// 2. `loadCities` fills two caches and notifies nobody, so even once the data /// was there Home never rebuilt to read it. /// /// Neither would be caught by anything that checks the widget tree in /// isolation, which is why this drives the real state object. /// /// ── Two ways this file can hang rather than fail ── /// /// The sphere breathes and its pulses repeat forever, so Home never settles: /// `pumpAndSettle` does not fail here, it runs until the test times out ten /// minutes later, which is a slow way to learn nothing. Fixed pumps instead. /// /// And the dev API answers on a real timer while the widget binding's clock is /// fake, so awaiting a load directly inside a `testWidgets` body waits on a /// delay that will never elapse. Those go through `tester.runAsync`. /// ───────────────────────────────────────────────────────────────────────── void main() { late AppState app; setUp(() { DoormileApi.overrideInstance(DevDoormileApi()); app = AppState() ..customer = const Customer( id: 'c1', name: 'Joe Oommen', phone: '+91 98765 43210', email: 'joe@example.com', ); }); tearDown(() => DoormileApi.overrideInstance(null)); Future pumpHome(WidgetTester tester) async { tester.view.physicalSize = const Size(1170, 2532); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.reset); // Inside a Scaffold, because `HomeScreen` does not carry one — it is a tab // root and `ShellScreen` owns the Scaffold for all three. Without it there // is no `Material` for the ink responses and no bounded width for the // greeting row, and the screen fails for two reasons that have nothing to // do with what is being tested. await tester.pumpWidget( AppScope( state: app, child: const MaterialApp(home: Scaffold(body: HomeScreen())), ), ); for (var i = 0; i < 6; i++) { await tester.pump(const Duration(milliseconds: 120)); } } group('the serviceable set', () { test('cachedCities agrees with loadCities', () async { expect(app.cachedCities, isNull, reason: 'nothing fetched yet'); final loaded = await app.loadCities(); expect(loaded, isNotEmpty); expect( app.cachedCities?.length, loaded.length, reason: 'the synchronous view must be the same set as the async one', ); }); test('it offers no district the picker would not', () async { final loaded = await app.loadCities(); final cached = app.cachedCities!; final loadedCodes = {for (final c in loaded) c.district.code}; final cachedCodes = {for (final c in cached) c.district.code}; expect(cachedCodes, loadedCodes); }); }); group('the reach line', () { testWidgets('is absent until the cities are known', (tester) async { await pumpHome(tester); expect(find.textContaining('Delivering to'), findsNothing); }); testWidgets('states the reach once they are', (tester) async { // `runAsync`, not a bare await: the dev API answers on a real timer and // the widget binding's clock is fake, so awaiting it directly inside a // `testWidgets` body waits for a delay that will never elapse. await tester.runAsync(() => app.loadCities()); await pumpHome(tester); final line = find.textContaining('Delivering to'); expect(line, findsOneWidget); final text = tester.widget(line).data!; final cities = app.cachedCities!; final states = {for (final c in cities) c.state.code}.length; expect(text, contains('${cities.length}')); expect(text, contains('$states states')); }); testWidgets('a rebuild after the warm-up is what makes it appear', ( tester, ) async { // The order that used to fail: Home is already on screen when the // cities land. Nothing re-reads the cache unless the state says so. await pumpHome(tester); expect(find.textContaining('Delivering to'), findsNothing); await tester.runAsync(() => app.loadCities()); app.notifyListeners(); await tester.pump(); expect(find.textContaining('Delivering to'), findsOneWidget); }); }); }