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

@@ -86,13 +86,27 @@ void main() {
//
// So: no rule, no disclosure, one card that is always in the state the
// rider previously had to discover. That costs height, and the budget says
// how much — enough for a head, a divider and one row of four figures, and
// how much — enough for a head, a bar, and the four facts two-by-two, and
// not enough for a fifth thing to be added without a decision.
//
// ── 190 → 245 ──
//
// Raised deliberately, once, when the four journey figures came out of the
// fold and onto the face. It buys the thing the fold was costing: a rider
// could not see the day's duration, distance, parcel count or cash without
// pressing something, and the press was rare enough that the figures were
// effectively shipped and unreachable.
//
// The 55 points are paid for elsewhere in the same change — the header
// collapses to 52 on scroll where it used to be a permanent 160, and the
// run's ratio came off this card because the header states it. The guard
// stays because the failure mode is unchanged: a card that grows a line at
// a time until the route is below the fold again.
testWidgets('stays within the card budget', (tester) async {
final h = await briefHeight(tester);
expect(
h,
lessThan(190),
lessThan(245),
reason:
'measured $h — the brief is the permanent furniture above the '
'route, so every point of it is charged at every scroll position',
@@ -121,46 +135,41 @@ void main() {
),
);
// ── Reversed, on an explicit product call ──
// ── Reversed twice, and this is the third position ──
//
// This once required the four figures ABSENT until tapped; then it was
// flipped to require them present, on the argument that a once-a-shift
// read behind a chevron nobody presses is information the app has and
// the rider does not. That argument is about *discoverability* and it
// was answered by a measurement: on a 390×844 phone the four columns
// plus the trip tabs plus the app bar reach the fold before the first
// kitchen, so Home opened on a dashboard about the run instead of on
// the run.
// The four figures were once required ABSENT until tapped, on a
// measurement: four columns plus the trip tabs plus the app bar reached
// the fold before the first kitchen, so Home opened on a dashboard about
// the run instead of on the run.
//
// The founder's call is that the figures are reference material and the
// route is the screen. What stays out is the progress face — stops left,
// and cash in hand — which are the two a rider acts on mid-shift.
// What changed is the space, not the argument. The header collapses now,
// the tabs are a row of pills rather than a strip, and the figures are
// two-by-two rather than four across — so the same information costs
// about half the vertical it did when it was banished, and the fold it
// was pushing the route past is no longer where it was.
//
// So they are on the face, in sentence case, with the run's ratio taken
// OFF the card entirely — the header states that now.
for (final label in const [
'DURATION',
'DISTANCE',
'PARCELS',
'PAYMENT',
'Duration',
'Distance',
'Parcels',
'Payment',
]) {
expect(
find.text(label),
findsNothing,
reason: '"$label" is reference; it is one tap away, not permanent',
findsOneWidget,
reason: '"$label" is on the face, not one tap away',
);
}
// And the face itself is still out, unasked.
// The face's own headline is still the thing a rider acts on.
expect(find.textContaining('stop'), findsWidgets);
// One tap brings the whole set back.
// The fold keeps what it is actually for: the shift and slot windows,
// read once in the morning.
await tester.tap(find.byType(TripBriefStrip));
await tester.pumpAndSettle();
for (final label in const ['DURATION', 'DISTANCE', 'PARCELS']) {
expect(
find.text(label),
findsOneWidget,
reason: '$label after the tap',
);
}
expect(
find.text('CURRENT SHIFT'),
findsNothing,
@@ -217,11 +226,18 @@ void main() {
});
});
group('Trip tabs', () {
testWidgets('fit in one compact row', (tester) async {
// ── The trip selector moved into the header ──
//
// Its height and its three-slot rule are pinned in `trip_tabs_pinned_test`,
// against the widget's own declared height rather than against a sliver
// extent it no longer has. What is left here is the density guard: it is
// chrome the rider sees at every scroll position, so its height is charged
// continuously and must not creep.
group('Trip selector', () {
testWidgets('stays inside its own height budget', (tester) async {
await pump(
tester,
TripTabs(
TripSelector(
trips: [
Trip.fromStops(id: 't1', stops: [stop('A')]),
Trip.fromStops(id: 't2', stops: [stop('B')]),
@@ -231,37 +247,25 @@ void main() {
),
);
final h = tester.getSize(find.byType(TripTabs)).height;
// ── This bound was raised deliberately, and it cost something ──
//
// The strip went from one line (~41pt) to a segmented control carrying a
// label, a status line and a running-indicator (~70pt). That is ~30pt of
// *permanent* height, and it is worse than it sounds because the bar is
// now a pinned sliver: it is on screen at every scroll position, not just
// at the top.
//
// What it buys is the separation of "the trip I am looking at" from "the
// trip that is running", which the one-line version could not express at
// all, plus per-trip progress in words rather than an ambiguous bar. That
// is a real trade, taken on purpose — but the guard stays, because the
// failure mode here is a strip that keeps growing a line at a time until
// it crowds out the route it is supposed to be navigating.
final h = tester.getSize(find.byType(TripSelector)).height;
// The old segmented strip carried a numbered disc, the word "Trip" and a
// status line, and came to ~70pt. This carries a numeral and a bar, and
// is bigger *per target* at the same overall height — the budget did not
// move, the content did.
expect(
h,
lessThan(80),
reason:
'the tab strip is pinned furniture — it is on screen at '
'every scroll position, so its height is charged continuously',
'the selector is header furniture — it is on screen at every '
'scroll position, so its height is charged continuously',
);
expect(tester.takeException(), isNull);
});
testWidgets('still show three slots before the hub assigns them', (
tester,
) async {
testWidgets('names three trips, and says only that', (tester) async {
await pump(
tester,
TripTabs(
TripSelector(
trips: [
Trip.fromStops(id: 't1', stops: [stop('A')]),
],
@@ -269,9 +273,13 @@ void main() {
onSelect: (_) {},
),
);
expect(find.text('Trip 1'), findsOneWidget);
expect(find.text('Trip 2'), findsOneWidget);
expect(find.text('Trip 3'), findsOneWidget);
for (final n in ['Trip 1', 'Trip 2', 'Trip 3']) {
expect(find.text(n), findsOneWidget);
}
// The run's count belongs to the card below, in type four times the
// size. A filter says what it filters to. See [TripSelector].
expect(find.text('0 / 1'), findsNothing);
expect(find.text('Not set'), findsNothing);
});
});
@@ -599,7 +607,7 @@ void main() {
);
});
testWidgets('only on START · HUB, never on RETURN · HUB', (tester) async {
testWidgets('only on START · BASE, never on RETURN · BASE', (tester) async {
await pump(
tester,
TripCard(
@@ -614,16 +622,26 @@ void main() {
);
expect(
tester.getRect(find.text('Map')).top,
lessThan(tester.getRect(find.text('RETURN · HUB')).top),
lessThan(tester.getRect(find.text('RETURN · BASE')).top),
);
});
});
group('Trip tab states', () {
// ── The per-segment colour test is gone with the word it probed ──
//
// It found each segment by the shared text "Trip" and read that Text's
// colour. There is no shared word any more — a segment is a numeral and a
// bar — and what the test was really guarding, that the selected trip reads
// as picked, is now carried by the travelling marker rather than by ink on a
// noun. `trip_tabs_pinned_test` measures the marker against the numeral it
// belongs to, which is the same guarantee stated in the terms the control
// now uses.
group('Trip selector marker', () {
Future<void> pumpTabs(WidgetTester tester, {required int selected}) async {
await pump(
tester,
TripTabs(
TripSelector(
trips: [
Trip.fromStops(id: 't1', stops: [stop('A')]),
Trip.fromStops(id: 't2', stops: [stop('B')]),
@@ -634,58 +652,7 @@ void main() {
);
}
Color colourOf(WidgetTester tester, String label) =>
tester.widget<Text>(find.text(label)).style!.color!;
testWidgets('the selected tab takes the brand accent', (tester) async {
// The bug: the pill moved but the label stayed slate, so standing on
// Trip 2 looked no different from Trip 2 sitting idle.
await pumpTabs(tester, selected: 1);
expect(
colourOf(tester, 'Trip 2'),
ColorConstants.primary,
reason: 'the tab the rider is on must read as picked',
);
expect(colourOf(tester, 'Trip 1'), isNot(ColorConstants.primary));
await pumpTabs(tester, selected: 0);
expect(colourOf(tester, 'Trip 1'), ColorConstants.primary);
expect(
colourOf(tester, 'Trip 3'),
isNot(ColorConstants.primary),
reason: 'an unassigned slot stays quiet',
);
});
testWidgets('the strip stays compact after the state work', (tester) async {
await pumpTabs(tester, selected: 0);
final h = tester.getSize(find.byType(TripTabs)).height;
expect(
h,
lessThan(66),
reason:
'measured $h — the segmented strip is pinned furniture, so '
'every point of it is charged at every scroll position',
);
});
});
group('Trip tab selection marker', () {
Future<void> pumpTabs(WidgetTester tester, {required int selected}) async {
await pump(
tester,
TripTabs(
trips: [
Trip.fromStops(id: 't1', stops: [stop('A')]),
Trip.fromStops(id: 't2', stops: [stop('B')]),
],
selectedIndex: selected,
onSelect: (_) {},
),
);
}
/// Same tree, but leaves the pill mid-flight so it can be sampled.
/// Same tree, but left mid-flight so the marker can be sampled.
Future<void> pumpTabsNoSettle(
WidgetTester tester, {
required int selected,
@@ -696,7 +663,7 @@ void main() {
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripTabs(
child: TripSelector(
trips: [
Trip.fromStops(id: 't1', stops: [stop('A')]),
Trip.fromStops(id: 't2', stops: [stop('B')]),
@@ -714,128 +681,101 @@ void main() {
/// The travelling selection marker.
///
/// Found by key, not by colour. It moved out of the segments and into the
/// pill when it was made to travel, and colour cannot identify it any more:
/// crossing an unassigned trip it is deliberately grey rather than the brand
/// red, so a colour finder loses it exactly during the journey these tests
/// exist to measure.
Rect underline(WidgetTester tester) {
final marks = find.byKey(TripTabs.markerKey);
/// Found by key, not by colour. It moved out of the segments and into a
/// single travelling object when it was made to travel, and colour cannot
/// identify it: the segments themselves fade between two inks as it passes,
/// so a colour finder loses it exactly during the journey these tests exist
/// to measure.
Rect marker(WidgetTester tester) {
final marks = find.byKey(TripSelector.markerKey);
expect(
marks,
findsOneWidget,
reason: 'exactly one tab may be marked at a time',
reason: 'exactly one trip may be marked at a time',
);
return tester.getRect(marks);
}
/// One tab's label.
Rect numeral(WidgetTester tester, int slot) =>
tester.getRect(find.text('Trip ${slot + 1}'));
// The marker followed a separate "this trip is running" signal for a while,
// which left Trip 1 permanently underlined while the rider stood on Trip 2 —
// so a tap appeared to do nothing. The marker belongs to selection.
testWidgets('the underline sits under the selected tab', (tester) async {
await pumpTabs(tester, selected: 0);
final first = underline(tester);
final trip1 = tester.getRect(find.text('Trip 1'));
expect(
first.center.dx,
closeTo(trip1.center.dx, 2),
reason: 'the marker should be under Trip 1',
);
});
testWidgets('and travels when a different tab is picked', (tester) async {
await pumpTabs(tester, selected: 0);
final before = underline(tester).center.dx;
// which left trip 1 permanently marked while the rider stood on trip 2 — so
// a tap appeared to do nothing. The marker belongs to selection.
testWidgets('the marker sits under the selected pill', (tester) async {
await pumpTabs(tester, selected: 1);
await tester.pumpAndSettle();
final after = underline(tester).center.dx;
final trip2 = tester.getRect(find.text('Trip 2'));
expect(
after,
greaterThan(before),
reason:
'the marker moved from $before to $after — it must follow '
'the selection rightward to Trip 2',
);
expect(after, closeTo(trip2.center.dx, 2));
final m = marker(tester);
final second = numeral(tester, 1);
// A short centred rule, not a full-width underline — so it is centred on
// its pill rather than spanning it.
expect(m.center.dx, closeTo(second.center.dx, 6));
expect(m.top, greaterThan(second.bottom));
});
testWidgets('Trip 3 → Trip 1 slides across Trip 2 rather than jumping', (
testWidgets('and travels when a different segment is picked', (
tester,
) async {
// The pill used to belong to each segment: one faded out on the right
// while a *different* one faded in on the left, so nothing ever occupied
// the space between and there was no object to cross Trip 2 with.
await pumpTabs(tester, selected: 2);
await tester.pumpAndSettle();
final trip2 = tester.getRect(find.text('Trip 2'));
final start = underline(tester).center.dx;
expect(
start,
greaterThan(trip2.center.dx),
reason: 'sanity: the marker starts to the right of Trip 2',
);
await pumpTabs(tester, selected: 0);
final first = marker(tester).center.dx;
await pumpTabs(tester, selected: 2);
final third = marker(tester).center.dx;
expect(
third,
greaterThan(first),
reason: 'the marker did not move with the selection',
);
});
testWidgets('3 → 1 slides across 2 rather than jumping', (tester) async {
// The regression this whole mechanism exists for: the marker used to be
// each segment's own fill, so crossing two slots faded one out on the
// right and a different one in on the left. Nothing ever occupied the
// space between.
await pumpTabs(tester, selected: 2);
final middle = numeral(tester, 1);
// `pumpTabs` pumps and settles; drive the frames by hand instead so the
// journey can be sampled.
await pumpTabsNoSettle(tester, selected: 0);
final samples = <double>[];
for (var elapsed = 0; elapsed < 420; elapsed += 20) {
for (var i = 0; i < 20; i++) {
await tester.pump(const Duration(milliseconds: 20));
samples.add(underline(tester).center.dx);
samples.add(marker(tester).center.dx);
}
final half = trip2.width / 2;
final half = middle.width;
expect(
samples.any((x) => (x - trip2.center.dx).abs() < half),
samples.any((x) => (x - middle.center.dx).abs() < half * 3),
isTrue,
reason: 'the marker never passed over Trip 2; samples: $samples',
reason: 'the marker never passed over tab 2; samples: \$samples',
);
for (var i = 1; i < samples.length; i++) {
expect(
samples[i],
lessThanOrEqualTo(samples[i - 1] + 0.5),
reason: 'the marker moved backwards at sample $i: $samples',
reason: 'the marker moved backwards at sample \$i: \$samples',
);
}
await tester.pumpAndSettle();
expect(
underline(tester).center.dx,
closeTo(tester.getRect(find.text('Trip 1')).center.dx, 2),
);
});
testWidgets('the pill is the brand tint, not a white card', (tester) async {
// It was a white pill with a drop shadow, which made this the one
// selected-state in the app that was not the brand colour — the two
// controls a rider uses most looked like they came from different apps.
await pumpTabs(tester, selected: 0);
final fills = tester
.widgetList<Container>(find.byType(Container))
.map((c) => c.decoration)
.whereType<BoxDecoration>()
.toList();
final glass = ColorConstants.primary.withValues(alpha: 0.12);
expect(
fills.map((d) => d.color),
contains(glass),
reason:
'the selection pill should be the light glass red the bottom '
'nav bar uses',
);
expect(
fills.where(
(d) => d.color == Colors.white && (d.boxShadow?.isNotEmpty ?? false),
),
isEmpty,
reason: 'no raised white pill should remain',
);
testWidgets('exactly one marker, whatever is selected', (tester) async {
// ── This assertion has been on both sides, and the principle held ──
//
// It began as "no raised white pill" — the bar was bare words with a
// travelling tint behind them, and a white card would have been the
// loudest object on a page whose brief was that containers recede. Then
// the bar became a segmented control on the canvas, and the pill was the
// design rather than the bug. It is now a white marker on the brand
// header, where white is simply the only mark that reads.
//
// What has to hold through all three is the part that always did: **one**
// mark of selection, never three.
for (final selected in const [0, 1, 2]) {
await pumpTabs(tester, selected: selected);
expect(find.byKey(TripSelector.markerKey), findsOneWidget);
}
});
});
@@ -936,10 +876,23 @@ void main() {
// chip, and there is no bound on how many of those a shift produces. Six
// stops in, the rider was scrolling past six rows he could do nothing with to
// reach the one he could. They move to the Activity tab — but the *trip*
// keeps them, because the brief's remaining count and its parcel figure are
// counted from it.
// object keeps them, because the numbering and the progress head are
// measured against a total that must not move.
//
// ── What the brief's four figures do NOT do ──
//
// They used to be counted from that same total, so duration, distance and
// the parcel count described the slot as assigned and never moved while the
// rider worked it. `20 stops left` counted down above four figures that
// still described the whole morning, including everything already collected
// and handed to Deliveries.
//
// They are counted from [Trip.outstanding] now — the collections still owed
// — so the two halves of the card finally answer the same question. The
// trip's own totals are untouched, which is what the first two assertions
// below are for.
group('completed stops', () {
testWidgets('are not listed, but still count towards the trip', (
testWidgets('leave the list and the figures, but not the trip', (
tester,
) async {
final trip = Trip.fromStops(
@@ -971,34 +924,44 @@ void main() {
// a trip of one with none done both say. The parcel count can only reach
// 2 if the finished stop is still part of the trip, which is the whole
// contract here.
// The hero sets the figure and its caption as two texts — numeral at
// headline scale, words as its caption — so the claim is asserted on
// the pair rather than on one sentence.
// The headline is one string now — `1 stop left` — rather than a numeral
// at headline scale beside its caption at body scale. Two sizes and a
// manual baseline nudge to make a figure and its unit look like one
// thing, when they are one thing.
expect(
find.descendant(
of: find.byType(TripBriefStrip),
matching: find.text('1'),
matching: find.text('1 stop left'),
),
findsOneWidget,
);
expect(find.text('stop left'), findsOneWidget);
final singleStop = Trip.fromStops(id: 't2', stops: [stop('B')]);
expect(
trip.totalParcels,
greaterThan(singleStop.totalParcels),
reason: 'the collected stop still contributes its load',
reason: 'the TRIP still carries the collected stop and its load',
);
// The parcel figure is reference now, so it is behind the fold — but it
// must still be the *trip's* total rather than the visible one, which is
// the contract this test exists for.
// ── But the brief states what is left, not what the slot held ──
//
// Stop A is `picked`, so it is across the boundary and belongs to
// Deliveries. One collection is still owed, and the parcel figure says
// one — the trip's own total of two is deliberately NOT what is drawn.
final outstanding = trip.outstanding();
expect(
outstanding.totalParcels,
lessThan(trip.totalParcels),
reason: 'a collected stop is no longer work in front of the rider',
);
await openBrief(tester);
expect(
find.descendant(
of: find.byType(TripBriefStrip),
matching: find.text('${trip.totalParcels}'),
matching: find.text('${outstanding.totalParcels}'),
),
findsOneWidget,
reason: 'and the card states that total, not the visible one',
reason: 'the card states the outstanding count, not the slot total',
);
});
@@ -1041,12 +1004,16 @@ void main() {
//
// The brief and the route now stand on [MilerPanel], layer 1 of the
// surface ladder, because Home's ground is the canvas and nothing white
// was standing on it. The panel owns the inset: `panelGutter` 12 outside
// the white plus `panelPad` 14 inside it, and `TripCard` no longer adds a
// gutter of its own. So both land on 26, and — the part that matters —
// they land on the *same* number. `home_gutter_test` holds them together.
const homeGutter = 26.0;
const stopRowInset = 26.0;
// was standing on it. The panel owns the inset, and `TripCard` no longer
// adds a gutter of its own — so both land on the *same* number, which is
// the part that matters. `home_gutter_test` holds them together.
//
// 26 → 29: `panelGutter` 12 outside the white, `panelPad` 16 inside it,
// and one point for the hairline the card carries now (see [Narrative] —
// the border is half of what buys a card its separation on the lighter
// ground). A pixel of border is a pixel of inset.
const homeGutter = 29.0;
const stopRowInset = 29.0;
testWidgets('the summary cards and the stop rows each keep their inset', (
tester,
@@ -1401,15 +1368,23 @@ void main() {
);
});
// ── The disclosure has to LOOK like one ──
// ── The header's right edge belongs to the journey ──
//
// The chevron was removed once when the header became the toggle, and its
// signal went with it: a collapsed kitchen showed nothing that said "there
// is a list under me". It is back as a passive indicator inside the
// header's own gesture — one arena, one owner — and this holds the three
// facts that make it honest: it exists, it turns with the state, and a
// flat stop (whose tap navigates instead) wears the pointing variant.
testWidgets('the header wears a chevron that turns with the state', (
// A chevron has been on and off this header three times, and the argument
// each way was about what the right edge was *for*. It came back once as a
// passive indicator, because a collapsed kitchen showed nothing that said
// "there is a list under me".
//
// The corner is not empty any more: the distance and the ride time live
// there, and they are the reason a rider looks at it — down a multi-kitchen
// run they form the column that answers *which of these is nearest*. A
// glyph between them and the card's edge pushed that column ~24 points off
// the margin so it lined up with nothing.
//
// What is asserted now is the trade: no fold mark on a group header, and
// the journey column sitting on the card's own right edge. The fold state
// is still legible — an open group has its orders on screen under it.
testWidgets('the group header ends on its journey, not a chevron', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.milkMan);
@@ -1426,43 +1401,45 @@ void main() {
'kitchenname': 'Vidhya Kitchen',
'Quantity': 1,
'orderstatus': 'new',
'pickuplat': 11.04,
'pickuplon': 76.98,
},
];
await pump(
tester,
TripCard(
trip: Trip.fromStops(id: 't', stops: stops),
riderLat: 11.0168,
riderLng: 76.9558,
onToggleSelect: (_) {},
onStopTap: (_) {},
),
);
// Scoped to the route. The trip brief above it also folds, and since
// Home moved to one icon family both disclosure marks are the same
// glyph — correctly, they mean the same thing — so a bare `byIcon` here
// matches the brief's chevron as well and stops being a statement about
// the group header at all.
final chevron = find.descendant(
of: find.byType(RouteTimeline),
matching: find.byIcon(LucideIcons.chevronDown),
// Scoped to the route: the trip brief above it still folds and wears the
// same glyph, so an unscoped finder would be a claim about the whole card.
expect(
find.descendant(
of: find.byType(RouteTimeline),
matching: find.byIcon(LucideIcons.chevronDown),
),
findsNothing,
reason: 'the corner is the journey column, not a fold mark',
);
expect(chevron, findsOneWidget);
double turns() => tester
.widget<AnimatedRotation>(
find.ancestor(of: chevron, matching: find.byType(AnimatedRotation)),
)
.turns;
final openTurns = turns();
// And the group still folds — the gesture was always the header's, and
// removing the glyph must not have taken the behaviour with it.
// Measured by height rather than by the rows' presence: the body
// collapses through an `AnimatedSize`, which keeps its child mounted at
// zero height, so a `findsNothing` here would be asserting an
// implementation detail of the animation rather than the fold.
final open = tester.getSize(find.byType(RouteTimeline)).height;
await tester.tap(find.text('Vidhya Kitchen'));
await tester.pumpAndSettle();
expect(
turns(),
isNot(openTurns),
reason:
'the chevron must turn when the list opens or closes — a '
'static disclosure mark is a printed one',
tester.getSize(find.byType(RouteTimeline)).height,
lessThan(open),
reason: 'tapping the header still closes its orders',
);
});