production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View File

@@ -5,60 +5,46 @@ 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.
/// ─────────────────────────────────────────────────────────────────────────
/// THE SELECTOR STAYS PUT, AND IT STAYS ONE HEIGHT
///
/// Two things here are worth a test.
/// This file used to guard a pinned `SliverPersistentHeader`: the delegate had
/// to declare a fixed extent, [TripSelector]'s content was type that grows with
/// the system font, and the two disagreed by a few pixels at large settings —
/// so `extentFor` derived an over-estimate and these tests measured it from
/// 1.0× to 2.0× to prove it was never short.
///
/// **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×.
/// **None of that exists any more.** The selector moved into the brand header,
/// which is fixed by being the Column's first child rather than by being a
/// sliver, so there is no extent to declare and nothing to under-estimate.
///
/// **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.
/// What survives is the property the arithmetic was protecting, and it is now
/// the widget's own: the control is a **fixed height** that does not move with
/// the text scale. A header that grows a few points at 1.5× pushes the whole
/// page down and re-lays the route out under the rider's thumb. It cannot,
/// because nothing in the control is free-flowing type — the numeral is inside
/// a `FittedBox` that scales it down into the space it has.
/// ─────────────────────────────────────────────────────────────────────────
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);
Map<String, dynamic> stop(String id) => {
'orderid': id,
'pickupid': id,
'pickupcustomer': 'Sri Balaji Stores',
'pickupaddress': '19, Thadagam Road, Coimbatore',
'orderstatus': 'pending',
};
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(
Future<void> pump(
WidgetTester tester, {
required int trips,
int selected = 0,
double textScale = 1.0,
ValueChanged<int>? onSelect,
}) async {
tester.view.physicalSize = const Size(1170, 2532);
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
double? captured;
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
@@ -70,142 +56,123 @@ void main() {
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,
),
),
],
);
},
body: Padding(
padding: const EdgeInsets.all(16),
child: TripSelector(
trips: [
for (var i = 0; i < trips; i++)
Trip.fromStops(id: 't$i', stops: [stop('$i')]),
],
selectedIndex: selected,
onSelect: (_) {},
),
),
),
),
),
);
await tester.pumpAndSettle();
return captured!;
await tester.pump();
}
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);
group('the control is one height at every text scale', () {
for (final scale in const [1.0, 1.3, 1.5, 2.0]) {
testWidgets('at ${scale}x', (tester) async {
await pump(tester, trips: 2, textScale: scale);
await tester.pump(const Duration(milliseconds: 400));
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),
tester.getSize(find.byType(TripSelector)).height,
TripSelector.height,
reason:
'tabs are ${tabs.height} tall but only $extent was '
'reserved — the bar would be clipped',
'the header must not change height with the font, or the whole '
'route below it moves under the rider\'s thumb',
);
// 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);
}
expect(tester.takeException(), isNull);
});
}
});
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));
testWidgets('three slots render before the hub has filled them', (
tester,
) async {
await pump(tester, trips: 1);
for (final n in ['Trip 1', 'Trip 2', 'Trip 3']) {
expect(find.text(n), findsOneWidget);
}
});
await tester.dragFrom(inViewport, const Offset(0, -600));
await tester.pumpAndSettle();
testWidgets('exactly one segment is marked, and it is the selected one', (
tester,
) async {
await pump(tester, trips: 2, selected: 1);
await tester.pump(const Duration(milliseconds: 400));
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',
);
final marks = find.byKey(TripSelector.markerKey);
expect(marks, findsOneWidget, reason: 'one trip is current, not two');
// 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);
});
// The rule is a short centred mark under its pill rather than a full-width
// underline, so what has to hold is that it is centred on the *right*
// pill — not that it spans it.
final marker = tester.getRect(marks);
final second = tester.getRect(find.text('Trip 2'));
expect(
marker.center.dx,
closeTo(second.center.dx, 6),
reason: 'the rule sits under the pill it belongs to',
);
expect(
marker.top,
greaterThan(second.bottom),
reason: 'and under it, not through it',
);
});
testWidgets('what sits above it does scroll away', (tester) async {
await pump(tester);
expect(find.byKey(aboveKey), findsOneWidget);
testWidgets('the indicator is a rule, not a filled tab', (tester) async {
// The tabs sit straight on the brand ground: no card, no wash, no outline.
// Selection is the travelling rule and the ink weight, and nothing else.
await pump(tester, trips: 2);
await tester.pump(const Duration(milliseconds: 400));
await tester.dragFrom(inViewport, const Offset(0, -900));
await tester.pumpAndSettle();
final marker = tester.getSize(find.byKey(TripSelector.markerKey));
expect(
marker.height,
lessThanOrEqualTo(4),
reason: 'a 2-3dp rule, not a segment-sized fill',
);
});
// 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 segment is tappable and reports its own index', (
tester,
) async {
int? chosen;
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
testWidgets('a tab is still tappable once pinned', (tester) async {
var picked = -1;
await pump(tester, onSelect: (i) => picked = i);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: TripSelector(
trips: [
Trip.fromStops(id: 't0', stops: [stop('0')]),
Trip.fromStops(id: 't1', stops: [stop('1')]),
],
selectedIndex: 0,
onSelect: (i) => chosen = i,
),
),
),
),
),
);
await tester.pump();
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',
);
});
await tester.tap(find.text('Trip 2'));
expect(chosen, 1);
});
}