153 lines
5.5 KiB
Dart
153 lines
5.5 KiB
Dart
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<void> 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<void>.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<void>.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();
|
||
});
|
||
}
|