import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; /// Home's app bar carries the rider's identity **and the ON/OFF duty toggle**, /// and it used to be the first sliver inside the page's `CustomScrollView` — so /// it scrolled away the moment he looked at his second stop, taking the duty /// control with it. /// /// The fix is structural: the bar becomes the first child of a `Column` and the /// scrollable takes the rest via `Expanded`. `_HomePageState` cannot be built in /// a test (Get, SharedPreferences, Geolocator, live fetches), so — as in /// `card_stripe_layout_test.dart` — these pin the *idiom* rather than the widget. void main() { const barKey = ValueKey('appbar'); const contentKey = ValueKey('tall-content'); // The default test viewport is 800×600 and the bar is 96 tall, so this point // is always inside the scroll area below it. Gestures are aimed here rather // than at the content's centre: the content is 2000pt tall, so its centre is // off-screen and a drag there hit-tests against nothing. const inScrollArea = Offset(400, 350); /// The shape Home now uses. Widget pinned({Future Function()? onRefresh}) { final scroller = CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverToBoxAdapter( child: Container(key: contentKey, height: 2000, color: Colors.grey), ), ], ); return MaterialApp( home: Scaffold( body: Stack( children: [ Column( children: [ Container(key: barKey, height: 96, color: Colors.white), Expanded( child: onRefresh == null ? scroller : RefreshIndicator(onRefresh: onRefresh, child: scroller), ), ], ), ], ), ), ); } /// The shape it used to use — bar as a sliver. Widget scrolling() { return MaterialApp( home: Scaffold( body: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverToBoxAdapter( child: Container(key: barKey, height: 96, color: Colors.white), ), SliverToBoxAdapter( child: Container(key: contentKey, height: 2000, color: Colors.grey), ), ], ), ), ); } testWidgets('as a sliver, the bar scrolls off the top', (tester) async { await tester.pumpWidget(scrolling()); expect(find.byKey(barKey), findsOneWidget); await tester.dragFrom(inScrollArea, const Offset(0, -300)); await tester.pumpAndSettle(); // Not merely pushed off-screen — a sliver that leaves the viewport and its // cache extent is unmounted outright, so the duty toggle stopped existing // rather than just moving. This is the behaviour being fixed. expect(find.byKey(barKey), findsNothing, reason: 'this is the behaviour that hid the duty toggle'); }); testWidgets('as a Column child, the bar does not move', (tester) async { await tester.pumpWidget(pinned()); final before = tester.getRect(find.byKey(barKey)); final contentBefore = tester.getRect(find.byKey(contentKey)).top; await tester.dragFrom(inScrollArea, const Offset(0, -300)); await tester.pumpAndSettle(); expect(tester.getRect(find.byKey(barKey)), before, reason: 'the app bar is fixed'); // And the page really did scroll underneath it. expect(tester.getRect(find.byKey(contentKey)).top, lessThan(contentBefore - 200)); }); testWidgets('the bar stays put through a long scroll and back', (tester) async { await tester.pumpWidget(pinned()); final rect = tester.getRect(find.byKey(barKey)); for (final dy in [-600.0, -600.0, 400.0, -900.0, 1200.0]) { await tester.dragFrom(inScrollArea, Offset(0, dy)); await tester.pumpAndSettle(); expect(tester.getRect(find.byKey(barKey)), rect); } }); testWidgets('pull to refresh still fires below the fixed bar', (tester) async { var fired = 0; await tester.pumpWidget(pinned(onRefresh: () async { fired++; await Future.delayed(const Duration(milliseconds: 50)); })); final rect = tester.getRect(find.byKey(barKey)); await tester.flingFrom(inScrollArea, const Offset(0, 320), 1200); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(fired, 1, reason: 'the gesture still reaches the scroll view'); // The spinner must not drag the bar down with it, which is what the old // unbounded-overscroll page did to the whole header. expect(tester.getRect(find.byKey(barKey)), rect); await tester.pumpAndSettle(); }); testWidgets('the refresh spinner comes down below the bar, not behind it', (tester) async { await tester.pumpWidget(pinned(onRefresh: () async { await Future.delayed(const Duration(milliseconds: 400)); })); await tester.flingFrom(inScrollArea, const Offset(0, 320), 1200); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); final spinner = find.byType(RefreshProgressIndicator); expect(spinner, findsOneWidget); // `edgeOffset` is no longer needed precisely because the scroll viewport now // starts under the bar — so the indicator is already in clear space. expect(tester.getRect(spinner).top, greaterThanOrEqualTo(tester.getRect(find.byKey(barKey)).bottom - 0.01)); await tester.pumpAndSettle(); }); }