Files
doormile_milderapp/test/home_appbar_test.dart
2026-08-11 13:16:33 +05:30

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