Files
doormile_milderapp/test/stop_card_neutral_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

322 lines
12 KiB
Dart

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_card.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
/// ── Stop type may be a mark, never a surface ──
///
/// Pending stop cards used to be *filled* with their stop-type accent at 5% and
/// bordered with the same colour at 38%, so the route came down the screen as a
/// lavender pane, then a pink one, then a blue one.
///
/// The line drawn now is between a mark and a surface. A small filled chip
/// naming the mode is a mark: it is a tag, it is bounded, and it does not colour
/// anything the rider is reading through. A card fill is a surface, and tinting
/// it puts a wash behind the name, the address and the buttons — which is where
/// Uber's Base rule bites: surfaces are white or one neutral grey, and the
/// accent is reserved for "one moment per screen".
///
/// So the type colours are allowed to appear, on a chip, and the chip fills use
/// the *darkened* variants ([ColorConstants.pickupChip] and friends) so white
/// text on them clears AA. What they may not do is fill or border a card.
///
/// These tests pin that by colour value, which is the only way it stays pinned —
/// a comment saying "keep this neutral" does not survive a redesign.
///
/// Note on coverage: `pickupAccent` is an alias of `ColorConstants.primary`, so
/// it is indistinguishable from the legitimate brand accent and cannot be
/// asserted against.
void main() {
Map<String, dynamic> stop(
String id, {
required String type,
double cash = 0,
int deliverQty = 0,
int pickupQty = 0,
}) => {
'orderid': id,
'type': type,
'pickupcustomer': 'Sri Balaji Stores',
'pickupaddress': '19, Thadagam Road, Venkitapuram, Coimbatore',
'pickupcontactno': '9876543210',
if (deliverQty > 0) 'deliveryqty': '$deliverQty',
if (pickupQty > 0) 'pickupqty': '$pickupQty',
'Quantity': '3',
if (cash > 0) 'collectionamt': '$cash',
'expected_pickup_time': DateTime.now().toIso8601String(),
};
Future<void> pumpCard(
WidgetTester tester, {
Set<String> accepted = const {},
double width = 390,
}) async {
tester.view.physicalSize = Size(width * 3, 2532);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final trip = Trip.fromStops(
id: 'mixed',
stops: [
stop('P', type: 'pickup', pickupQty: 4),
stop('D', type: 'delivery', deliverQty: 2),
stop('C', type: 'combined', deliverQty: 1, pickupQty: 2, cash: 340),
],
);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripCard(
trip: trip,
acceptedIds: accepted,
onToggleSelect: (_) {},
),
),
),
),
),
);
await tester.pumpAndSettle();
}
/// Every colour the card paints: decoration fills, border sides, icon tints
/// and text colours (including rich-text spans).
List<(String, Color)> paintedColours(WidgetTester tester) {
final found = <(String, Color)>[];
void addBorder(String where, BoxBorder? b) {
if (b is Border) {
for (final side in [b.top, b.right, b.bottom, b.left]) {
if (side.style != BorderStyle.none)
found.add(('$where.border', side.color));
}
}
}
// Walks `children` directly rather than via `visitChildren`: that visitor
// yields the span itself before its children, so recursing from inside it
// re-enters on the same node forever.
void addSpan(String where, InlineSpan? span) {
if (span == null) return;
final c = span.style?.color;
if (c != null) found.add(('$where.span', c));
if (span is TextSpan) {
for (final child in span.children ?? const <InlineSpan>[]) {
addSpan(where, child);
}
}
}
for (final w in tester.allWidgets) {
switch (w) {
case Container(:final decoration):
if (decoration is BoxDecoration) {
if (decoration.color != null) {
found.add(('Container.color', decoration.color!));
}
addBorder('Container', decoration.border);
}
case DecoratedBox(:final decoration):
if (decoration is BoxDecoration) {
if (decoration.color != null) {
found.add(('DecoratedBox.color', decoration.color!));
}
addBorder('DecoratedBox', decoration.border);
}
case Icon(:final color) when color != null:
found.add(('Icon', color));
case Text(:final style, :final textSpan):
if (style?.color != null) found.add(('Text', style!.color!));
addSpan('Text', textSpan);
case RichText(:final text):
addSpan('RichText', text);
default:
break;
}
}
return found;
}
/// The *undarkened* accents. These are the values that were washing the card
/// fills and borders; the chip uses the darkened variants instead, so seeing
/// one of these anywhere means a tint has crept back.
final bannedOutright = <String, Color>{
'deliveryAccent (royal blue)': ColorConstants.deliveryAccent,
'combinedAccent (deep indigo)': ColorConstants.combinedAccent,
};
testWidgets('the un-darkened type accents are not painted at all', (
tester,
) async {
await pumpCard(tester);
final painted = paintedColours(tester);
expect(painted, isNotEmpty, reason: 'sanity: the card painted something');
for (final entry in bannedOutright.entries) {
final hits = painted.where((p) => p.$2 == entry.value).toList();
expect(
hits,
isEmpty,
reason:
'${entry.key} is the light accent — the chip should use the '
'darkened variant. Found at ${hits.map((h) => h.$1).toSet()}',
);
}
});
testWidgets('the same holds once some stops are accepted', (tester) async {
await pumpCard(tester, accepted: {'D', 'C'});
final painted = paintedColours(tester);
for (final entry in bannedOutright.entries) {
expect(
painted.where((p) => p.$2 == entry.value),
isEmpty,
reason: '${entry.key} must not appear in the accepted state either',
);
}
});
// ── Where the type went, and why these two tests changed ──
//
// The rule this file was written for was "a stop type may be a mark, never a
// surface", and it was measured by finding the type *word* on the row and
// checking nothing was filled with its colour.
//
// The route timeline then took the word off the row as well. That was not a
// slip: a step is `who` and `which bag`, the two facts a rider matches
// against a shelf, and the type went into the detail sheet with the address,
// the phone and the parcel counts. So the rule gets **stricter** rather than
// looser — a stop type may not tint anything on the route, and it may not
// spend a line there either — and that is what is measured now.
//
// The wording itself is not lost and is not untested: [StopKind.badgeLabel]
// and [StopKind.chipLabel] still carry it, the Bookings card still prints it
// on a line of its own, and `service_card_test.dart` holds both.
testWidgets('a stop type tints nothing on the route', (tester) async {
await pumpCard(tester);
final containerColours = tester.allWidgets
.whereType<Container>()
.map((c) => c.decoration)
.whereType<BoxDecoration>()
.expand(
(d) => [
d.color,
if (d.border is Border) (d.border as Border).top.color,
],
)
.whereType<Color>()
.toList();
final textColours = tester.allWidgets
.whereType<Text>()
.map((t) => t.style?.color)
.whereType<Color>()
.toSet();
for (final chip in [
ColorConstants.pickupChip,
ColorConstants.deliveryChip,
ColorConstants.combinedChip,
]) {
expect(
containerColours.contains(chip),
isFalse,
reason: 'a stop type may not fill or border anything on the route',
);
expect(
textColours.contains(chip),
isFalse,
reason:
'nor tint type: the route is one accent — the brand, on the live '
'node and the journey action — and a per-stop hue competes with it',
);
}
});
testWidgets('a step states who it is for, and no stop type', (tester) async {
await pumpCard(tester);
// This is a parcel route, so each stop is its own node and what names it is
// the customer plus its position in the run. (The `who` + `which bag` pair
// is the kitchen-group shape and is held by `one_bag_per_order_test.dart`.)
expect(find.text('Sri Balaji Stores'), findsWidgets);
expect(find.text('Stop 1'), findsOneWidget);
// Everything the card used to shout on every row. The type is one tap away
// in the sheet, on the badge `StopDetailSheet._header` draws.
for (final label in const [
'PICKUP',
'DELIVERY',
'P&D',
'PICKUP & DELIVERY',
]) {
expect(
find.text(label),
findsNothing,
reason:
'"$label" is on the stop sheet, not on a row a rider reads forty '
'times a shift',
);
}
});
testWidgets('the type chip is never squeezed out of shape', (tester) async {
// The bug: the chip rendered as "P…". Its `Flexible` sat beside a `Spacer`,
// and a Spacer is an `Expanded` — so the two split the free width evenly and
// the chip was capped at about 23pt against a 63pt label, i.e. **36% of what
// it needed**, no matter how short the wording was.
//
// Asserted as a *ratio* to the label's own natural width rather than as a
// fixed number of points. The headless test font draws every glyph as a full
// em square — roughly twice a real font — so absolute widths here mean
// nothing, but "how much of what it asked for did it get" survives the
// difference. A flat floor would also wrongly fail "P&D", which is three
// characters and legitimately narrow.
const squeezed = 0.36; // what the broken layout gave
const acceptable = 0.6;
for (final width in [320.0, 360.0, 390.0, 430.0]) {
await pumpCard(tester, width: width);
expect(
tester.takeException(),
isNull,
reason: 'the meta row overflowed at ${width.toInt()}pt',
);
for (final label in ['PICKUP', 'DELIVERY', 'P&D']) {
final finder = find.text(label);
if (finder.evaluate().isEmpty) continue;
final box = finder.evaluate().first.renderObject! as RenderBox;
final natural = (TextPainter(
text: TextSpan(
text: label,
style: tester.widget<Text>(finder.first).style,
),
textDirection: TextDirection.ltr,
)..layout()).width;
final got = box.size.width / natural;
expect(
got,
greaterThan(acceptable),
reason:
'at ${width.toInt()}pt the "$label" chip got '
'${(got * 100).round()}% of the width its label needs; the '
'Spacer-stealing bug gave ${(squeezed * 100).round()}%',
);
}
}
});
}