production
This commit is contained in:
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user