Files
doormile_milderapp/test/trip_tabs_pinned_test.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 05:40:35 +05:30

212 lines
7.0 KiB
Dart
Raw 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',
);
});
});
}