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

184 lines
6.8 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_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/views/Dashboard/home/trip.dart';
import 'package:miler/views/Dashboard/home/trip_tabs.dart';
/// The Trip 1 / 2 / 3 bar is a **pinned** sliver on Home: it scrolls up with the
/// page, then holds at the top while the stops pass underneath, so switching
/// trips never costs a scroll back up.
///
/// Two things here are worth a test.
///
/// **The extent is computed, and computed height is the classic way to clip a
/// control.** A pinned sliver must declare a fixed height, and [TripTabs]
/// deliberately has none — its own comment records that a hard `54.h` overflowed
/// by a few pixels at large font settings. `extentFor` therefore derives the
/// height from the text scale and over-estimates on purpose. If that estimate is
/// ever short, the tabs are clipped inside the header, silently. So it is
/// measured here from 1.0× to 2.0×.
///
/// **Pinning itself.** That the bar holds position while content scrolls under
/// it is the entire point of the change, and a one-word edit (`pinned: false`)
/// undoes it invisibly.
void main() {
// The view is forced to 390x844 logical below, so gestures must aim inside
// that box — x=400 would land outside the viewport and hit nothing.
const inViewport = Offset(195, 500);
const aboveKey = ValueKey('above');
const belowKey = ValueKey('below');
final trips = [
Trip.fromStops(id: 't1', stops: [
{'orderid': 'A', 'type': 'pickup', 'pickupcustomer': 'Sri Balaji'},
]),
Trip.fromStops(id: 't2', stops: [
{'orderid': 'B', 'type': 'delivery', 'pickupcustomer': 'Anand'},
]),
];
/// Pumps the real delegate inside a scroll view, at a given text scale, and
/// hands back the extent it reserved.
Future<double> pump(
WidgetTester tester, {
double textScale = 1.0,
ValueChanged<int>? onSelect,
}) async {
tester.view.physicalSize = const Size(1170, 2532);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
double? captured;
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context)
.copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
home: Scaffold(
body: Builder(
builder: (context) {
captured = TripTabsHeader.extentFor(context);
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
key: aboveKey,
height: 300,
color: Colors.amber,
),
),
SliverPersistentHeader(
pinned: true,
delegate: TripTabsHeader(
extent: captured!,
trips: trips,
selectedIndex: 1,
onSelect: onSelect ?? (_) {},
),
),
SliverToBoxAdapter(
child: Container(
key: belowKey,
height: 2000,
color: Colors.grey,
),
),
],
);
},
),
),
),
),
);
await tester.pumpAndSettle();
return captured!;
}
group('reserved extent fits the tabs', () {
// 2.0 is beyond Android's slider but within what a determined user can set,
// and it is the case the original 54.h bug was reported against.
for (final scale in [1.0, 1.15, 1.3, 1.5, 1.75, 2.0]) {
testWidgets('at ${scale}x text scale', (tester) async {
final extent = await pump(tester, textScale: scale);
expect(tester.takeException(), isNull,
reason: 'the tabs must not overflow the reserved extent');
final tabs = tester.getSize(find.byType(TripTabs));
expect(tabs.height, lessThanOrEqualTo(extent + 0.01),
reason: 'tabs are ${tabs.height} tall but only $extent was '
'reserved — the bar would be clipped');
// Over-reserving is the safe direction, but not by a wasteful amount.
expect(extent - tabs.height, lessThan(14),
reason: 'reserving $extent for ${tabs.height} of content is '
'more dead space than the safety margin intends');
// All three slots are still rendered and readable at this scale.
for (final label in ['Trip 1', 'Trip 2', 'Trip 3']) {
expect(find.text(label), findsOneWidget);
}
});
}
});
group('pinning', () {
testWidgets('the bar holds at the top while content scrolls under it',
(tester) async {
await pump(tester);
final atRest = tester.getRect(find.byType(TripTabs));
// It starts below the 300pt block above it, not stuck to the top.
expect(atRest.top, greaterThan(200));
await tester.dragFrom(inViewport, const Offset(0, -600));
await tester.pumpAndSettle();
final pinned = tester.getRect(find.byType(TripTabs));
expect(pinned.top, lessThan(atRest.top),
reason: 'it should travel up with the page first');
expect(pinned.top, lessThan(40),
reason: 'and then hold near the top of the viewport');
// Scrolling further does not move it again.
await tester.dragFrom(inViewport, const Offset(0, -600));
await tester.pumpAndSettle();
expect(tester.getRect(find.byType(TripTabs)), pinned);
});
testWidgets('what sits above it does scroll away', (tester) async {
await pump(tester);
expect(find.byKey(aboveKey), findsOneWidget);
await tester.dragFrom(inViewport, const Offset(0, -900));
await tester.pumpAndSettle();
// The summary / shift / metrics block is meant to leave; only the control
// stays. A sliver that has left the viewport and its cache is unmounted.
expect(find.byKey(aboveKey), findsNothing);
expect(find.byType(TripTabs), findsOneWidget);
});
testWidgets('a tab is still tappable once pinned', (tester) async {
var picked = -1;
await pump(tester, onSelect: (i) => picked = i);
await tester.dragFrom(inViewport, const Offset(0, -900));
await tester.pumpAndSettle();
await tester.tap(find.text('Trip 1'));
await tester.pumpAndSettle();
expect(picked, 0,
reason: 'switching trips from deep in the list is the whole point');
});
});
}