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

418
test/home_header_test.dart Normal file
View File

@@ -0,0 +1,418 @@
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/home_header.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// THE HOME HEADER
///
/// Two things this file is for, and the second is the one that will save
/// somebody later.
///
/// **The budget.** The header's whole justification is that the rider's first
/// stop was below the fold. That is an arithmetic claim, so it is measured
/// here: expanded content stays inside 150–175dp, and the collapsed state is
/// 52. A header that creeps back up a line at a time is exactly how the
/// original one got to 160.
///
/// **The extent contract.** A `SliverPersistentHeader` declares a fixed height
/// and anything longer than it is *silently clipped* — no exception, no
/// overflow stripe, just a trip selector with its bottom cut off. Two of the
/// lines in here are type that grows with the system font, so the extent is
/// derived rather than hardcoded, and these tests walk it from 1.0× to 2.0× on
/// four screen widths to prove it is never short.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
/// Pumps the header as the page does: a pinned sliver at the head of a
/// `CustomScrollView`, with something long under it to scroll.
Future<ScrollController> pump(
WidgetTester tester, {
String greeting = 'Good afternoon, Rajan',
String subtitle = '17 stops remaining',
String collapsed = 'Trip 1 · 17 left · 33/50',
bool onDuty = true,
bool locked = false,
bool busy = false,
VoidCallback? onToggle,
double width = 390,
double textScale = 1.0,
double topInset = 47,
}) async {
final controller = ScrollController();
addTearDown(controller.dispose);
tester.view.physicalSize = Size(width * 3, 844 * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
debugShowCheckedModeBanner: false,
home: MediaQuery(
data: MediaQueryData(
size: Size(width, 844),
padding: EdgeInsets.only(top: topInset),
textScaler: TextScaler.linear(textScale),
),
child: Builder(
builder: (context) => Scaffold(
body: CustomScrollView(
controller: controller,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: HomeHeaderDelegate(
topInset: topInset,
expandedContent: HomeHeaderDelegate.expandedContentFor(
context,
),
greeting: greeting,
subtitle: subtitle,
onDuty: onDuty,
dutyLocked: locked,
dutyBusy: busy,
onToggleDuty: onToggle ?? () {},
collapsedSummary: collapsed,
),
),
SliverList.list(
children: [
for (var i = 0; i < 40; i++)
SizedBox(height: 60, child: Text('row $i')),
],
),
],
),
),
),
),
),
),
);
await tester.pump();
return controller;
}
group('1 · the height budget', () {
testWidgets('expanded content is inside 95–120dp', (tester) async {
late double content;
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Builder(
builder: (context) {
content = HomeHeaderDelegate.expandedContentFor(context);
return const SizedBox();
},
),
),
),
);
// ── The budget moved when the tabs did ──
//
// It was 150–175, for a header that carried the trip selector. The
// selector sits on the page under it now, so what is left up here is a
// wordmark row, a greeting and one operational line — and the budget is
// the sum of those rather than a number inherited from the old shape.
//
// The tabs are still charged, just not to this. Header + tabs together
// are about 124 + 46 + their padding, against the 160 the fixed block
// cost before — and unlike that block, 72 of it scrolls away.
// ── The budget moved again when the duplicate line went ──
//
// It was 110–135 with `N stops remaining` under the greeting. That line
// was the brief card's headline said twice, forty points apart, smaller
// and greyer the first time — so it is gone and the header is ~20 points
// cheaper. Every one of those points is the rider's first stop moving up
// the screen, which is the whole reason this test exists.
expect(
content,
inInclusiveRange(95, 120),
reason:
'measured $content — the header exists to get the first stop above '
'the fold, and every point it grows takes one back',
);
});
testWidgets('collapsed content is 52dp', (tester) async {
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => const MaterialApp(home: SizedBox()),
),
);
expect(HomeHeaderDelegate.collapsedContent, closeTo(52, 0.5));
});
});
group('2 · the extent is never short', () {
for (final width in const [320.0, 360.0, 390.0, 430.0]) {
for (final scale in const [1.0, 1.3, 1.5, 2.0]) {
testWidgets('${width.toInt()}dp at ${scale}x', (tester) async {
await pump(tester, width: width, textScale: scale);
// A clipped sliver throws nothing, so the assertion that matters is
// that the layout itself did not complain — an overflowing Column
// inside the header WOULD throw, and that is the failure mode these
// widths and scales are hunting.
expect(tester.takeException(), isNull);
expect(find.text('ON'), findsOneWidget);
});
}
}
testWidgets('a long rider name does not push anything out', (tester) async {
await pump(
tester,
width: 320,
greeting: 'Good afternoon, Venkateswara Subramaniam',
);
expect(tester.takeException(), isNull);
// The greeting ellipsises to one line rather than wrapping and pushing
// the header past its declared extent.
expect(find.text('Good afternoon, Venkateswara Subramaniam'),
findsOneWidget);
});
});
group('3 · what each state says', () {
testWidgets('off duty reads OFF and keeps the same shape', (tester) async {
await pump(tester, onDuty: false);
expect(find.text('OFF'), findsOneWidget);
expect(find.text('ON'), findsNothing);
});
testWidgets('the stop count is NOT repeated up here', (tester) async {
// ── It was said twice, forty points apart ──
//
// `17 stops remaining` sat under the greeting in grey 13pt, and the
// brief card immediately below leads with `17 stops left` as its
// headline at 17pt against a progress bar. Two sizes, two wordings, one
// figure — and the smaller, greyer, vaguer one came first.
//
// The header carries identity and duty. The count belongs to the card
// that also shows the duration, distance and parcels behind it.
await pump(tester);
expect(find.text('Good afternoon, Rajan'), findsOneWidget);
expect(find.text('17 stops remaining'), findsNothing);
expect(find.textContaining('stops remaining'), findsNothing);
});
testWidgets('but the collapsed line still carries it', (tester) async {
// The one place the figure is still the header's job: collapsed, the
// brief card has scrolled away and this is all that is left on screen.
final c = await pump(tester);
c.jumpTo(400);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Trip 1 · 17 left · 33/50'), findsOneWidget);
});
});
group('4 · duty', () {
testWidgets('the tap target clears 48dp even though the pill is smaller', (
tester,
) async {
await pump(tester);
final target = tester.getSize(find.byType(DutyPill));
expect(target.height, greaterThanOrEqualTo(DutyPill.tapTarget));
});
testWidgets('tapping asks the page, it does not decide', (tester) async {
var asked = 0;
await pump(tester, onToggle: () => asked++);
await tester.tap(find.byType(DutyPill));
expect(asked, 1);
});
testWidgets('a write in flight refuses a second press', (tester) async {
var asked = 0;
await pump(tester, busy: true, onToggle: () => asked++);
await tester.tap(find.byType(DutyPill), warnIfMissed: false);
expect(asked, 0, reason: 'two duty writes for one intent');
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('locked mid-pickup refuses, and says so', (tester) async {
var asked = 0;
await pump(tester, locked: true, onToggle: () => asked++);
await tester.tap(find.byType(DutyPill), warnIfMissed: false);
expect(asked, 0);
// The state is still readable — the control dims rather than vanishing.
expect(find.text('ON'), findsOneWidget);
});
});
group('5 · collapsing', () {
testWidgets('the greeting goes and the operational line arrives', (
tester,
) async {
final c = await pump(tester);
expect(find.text('Good afternoon, Rajan'), findsOneWidget);
expect(find.text('Trip 1 · 17 left · 33/50'), findsNothing);
c.jumpTo(400);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
// Each layout is dropped entirely once its side of the cross-fade
// reaches zero, rather than kept at `Opacity(0)`. A fully transparent
// subtree still lays out and still paints into a saved layer on every
// frame of a scroll, and this one holds a wordmark, a greeting and a
// duty control — which is a lot of nothing to composite forty times a
// second.
expect(find.text('Good afternoon, Rajan'), findsNothing);
expect(find.text('Trip 1 · 17 left · 33/50'), findsOneWidget);
});
testWidgets('duty stays reachable in the collapsed state', (tester) async {
var asked = 0;
final c = await pump(tester, onToggle: () => asked++);
c.jumpTo(400);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
// The expanded copy is gone by now, so this can only be the collapsed
// one — which is the point: duty is the one control that must never
// scroll away.
expect(find.byType(DutyPill), findsOneWidget);
await tester.tap(find.byType(DutyPill));
expect(asked, 1);
});
testWidgets('the header holds the top of the screen', (tester) async {
final c = await pump(tester);
c.jumpTo(600);
await tester.pump();
// The pinned extent is exactly the collapsed one plus the inset.
final material = tester.getSize(
find.byKey(HomeHeaderDelegate.surfaceKey),
);
expect(
material.height,
closeTo(47 + HomeHeaderDelegate.collapsedContent, 1),
);
});
});
group('6 · rebuild scope', () {
test('an unrelated change does not rebuild the header', () {
HomeHeaderDelegate make({String subtitle = '17 stops remaining'}) =>
HomeHeaderDelegate(
topInset: 47,
expandedContent: 170,
greeting: 'Good afternoon, Rajan',
subtitle: subtitle,
onDuty: true,
dutyLocked: false,
dutyBusy: false,
onToggleDuty: () {},
collapsedSummary: 'Trip 1 · 17 left · 33/50',
);
// Same inputs — a row further down the list changing touches none of
// them, so the sliver does not repaint for it.
expect(make().shouldRebuild(make()), isFalse);
// A figure it actually draws does.
expect(
make().shouldRebuild(make(subtitle: '16 stops remaining')),
isTrue,
);
});
});
// ══════════════════════════════════════════════════════════════════════
// THE COLLAPSE ITSELF
//
// The budget tests above measure the header at its two REST states. The bug
// this group exists for lived entirely in between them.
//
// `_ExpandedBody` was a plain child of a `StackFit.expand` stack, so it was
// handed the header's *current* height — which, mid-collapse, is less than
// the content. Its Column cannot compress, so from the first pixel of scroll
// Flutter drew `BOTTOM OVERFLOWED BY 28 PIXELS` in yellow and black across
// the top of Home, over the greeting, on a rider's phone.
//
// Neither rest state shows it: fully expanded there is room, fully collapsed
// the body has faded to nothing. Only the travel between them does — which
// is why every assertion below walks the scroll rather than sampling its
// ends.
// ══════════════════════════════════════════════════════════════════════
group('7 · nothing overflows while it collapses', () {
/// Every offset from rest to fully pinned, finely enough that no fade
/// threshold (0.55 / 0.6) can be stepped over.
Future<void> walk(
WidgetTester tester,
ScrollController controller,
double range,
) async {
for (var i = 0; i <= 40; i++) {
controller.jumpTo(range * i / 40);
await tester.pump();
expect(
tester.takeException(),
isNull,
reason:
'overflowed at scroll offset ${range * i / 40} of $range — the '
'header must clip its content, never squeeze it',
);
}
}
testWidgets('at 1.0x on a 390pt phone', (tester) async {
final c = await pump(tester);
await walk(tester, c, 200);
});
testWidgets('at every text scale a rider can set', (tester) async {
for (final scale in const [1.0, 1.3, 1.5, 2.0]) {
final c = await pump(tester, textScale: scale);
await walk(tester, c, 200);
}
});
testWidgets('on a narrow phone, where the greeting wraps', (tester) async {
// 320pt with a long name is where the greeting is most likely to take a
// second line and push the Column past the extent.
final c = await pump(
tester,
width: 320,
greeting: 'Good afternoon, Rajanikanth Subramaniam',
subtitle: '20 stops remaining',
);
await walk(tester, c, 200);
});
testWidgets('the expanded body keeps its full height as it goes', (
tester,
) async {
// The fix, stated as behaviour rather than as a widget choice: the
// greeting does not shrink on the way out, it slides out. If it ever
// reflows again, this catches it before the overflow banner does.
final c = await pump(tester);
final full = tester.getSize(find.text('Good afternoon, Rajan')).height;
c.jumpTo(30);
await tester.pump();
expect(tester.takeException(), isNull);
expect(
tester.getSize(find.text('Good afternoon, Rajan')).height,
full,
reason: 'the body is measured at its own height, not the header\'s',
);
});
});
}