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

411 lines
16 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/data/service_profile.dart';
import 'package:miler/views/Dashboard/home/route_timeline.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';
/// ─────────────────────────────────────────────────────────────────────────
/// CONTENT LEADS, CONTAINERS RECEDE
///
/// Home's grouping unit is a **pickup place**, and for three iterations it was
/// drawn as a box. First a card per order — fifteen boxes on a route. Then a
/// card per place, which was the right correction to the wrong object and
/// still the wrong object: a route already says that two things belong
/// together, with a spine, a node and an indent, so the rectangle said it a
/// second time in a louder voice. The orders then needed a *tinted* rectangle
/// inside that one to separate themselves from their own container, which is
/// the point at which the furniture is arguing with itself.
///
/// These pin the third answer: no surface at all. The group is the spine plus
/// whitespace plus type, the orders sit on Home's own ground, and the three
/// things a rider does to a kitchen have three separate targets.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
Map<String, dynamic> meal(String id, String who, {int step = 1}) => {
'orderid': id,
'type': 'delivery',
'step': step,
'pickupcustomer': who,
'kitchenid': 'K1',
'kitchenname': 'Vidhya Kitchen',
'sourcename': 'Vidhya Kitchen',
'pickupcontactno': '9787698259',
'pickupaddress': '4 Cross, RS Puram, Coimbatore',
'dropaddress': '12 SNS Colony, Peelamedu, Coimbatore 641004',
'Quantity': '1',
};
Future<void> pumpHome(
WidgetTester tester, {
Set<String> accepted = const {},
Set<String> selected = const {},
void Function(String)? onToggleSelect,
double width = 390,
double textScale = 1.0,
}) async {
ServiceProfile.setActive(ServiceProfile.milkMan);
tester.view.physicalSize = Size(width * 3, 2600 * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
final trip = Trip.fromStops(
id: 'run',
stops: [
meal('1', 'Joe Mathew', step: 1),
meal('2', 'Arun Prakash', step: 2),
meal('3', 'Ravi Kumar', step: 3),
],
);
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(
backgroundColor: ColorConstants.daylightSurface,
body: SingleChildScrollView(
child: TripCard(
trip: trip,
acceptedIds: accepted,
selectedIds: selected,
onToggleSelect: onToggleSelect ?? (_) {},
onSelectAll: (_) {},
onStopTap: (_) {},
onCallStop: (_) {},
),
),
),
),
),
);
await tester.pumpAndSettle();
}
/// Makes sure the kitchen's orders are on screen.
///
/// The run's first place opens itself — a rider arriving at work should see
/// the counter he is riding to, not a closed list — so this only taps when
/// it is actually shut. Tapping unconditionally would *collapse* it, which
/// is how a row assertion ends up looking like a broken row.
Future<void> openKitchen(WidgetTester tester) async {
if (find.text('Joe Mathew').evaluate().isEmpty) {
await tester.tap(find.text('Vidhya Kitchen'));
await tester.pumpAndSettle();
}
}
group('the kitchen is not a box', () {
testWidgets('no surface, border, shadow or radius groups a kitchen', (
tester,
) async {
await pumpHome(tester);
// Every decoration inside the group, at any depth. A grouping rectangle
// is exactly what this screen may not draw — see the header note.
final decorations = tester
.widgetList<Container>(
find.descendant(
of: find.byType(PickupTimelineGroup),
matching: find.byType(Container),
),
)
.map((c) => c.decoration)
.whereType<BoxDecoration>();
// Circles are exempt: the sequence node and the tick are round marks on
// the spine, which is the language doing the grouping rather than a
// rectangle competing with it.
final rectangles = decorations.where((d) => d.shape != BoxShape.circle);
for (final d in rectangles) {
expect(
d.boxShadow,
anyOf(isNull, isEmpty),
reason: 'a shadow lifts a group off the page; the spine groups it',
);
expect(
d.border,
isNull,
reason: 'an outline around a group is the card by another name',
);
}
// ── What a fill may be here, since the route moved onto a panel ──
//
// The original rule was "no filled rectangle at all": the orders sat on
// a white page, so any panel behind them was a card by another name.
// The route now stands on a white [MilerPanel], which re-created the
// exact flatness that rule was written against — an open kitchen was
// white-on-white. Two fills are now legitimate, and only these two:
//
// • exactly one canvas-toned well behind the open orders — the
// ladder's single permitted inset, reading as the card opening
// down to the page;
// • small white tags (the bag labels) standing on that well.
//
// What stays banned is what the rule always meant: a white panel wide
// enough to be a row's or the group's own card.
final fillBoxes = find
.descendant(
of: find.byType(PickupTimelineGroup),
matching: find.byType(DecoratedBox),
)
.evaluate()
.where((e) {
final d = (e.widget as DecoratedBox).decoration;
return d is BoxDecoration &&
d.shape != BoxShape.circle &&
(d.color == ColorConstants.pureSurface ||
d.color == ColorConstants.daylightSurface);
})
.toList();
final wells = fillBoxes
.where(
(e) =>
((e.widget as DecoratedBox).decoration as BoxDecoration)
.color ==
ColorConstants.daylightSurface,
)
.toList();
expect(
wells.length,
lessThanOrEqualTo(1),
reason: 'one well behind the open orders, never a stack of them',
);
for (final e in fillBoxes) {
final d = (e.widget as DecoratedBox).decoration as BoxDecoration;
if (d.color == ColorConstants.pureSurface) {
final w = e.renderObject!.paintBounds.width;
expect(
w,
lessThan(120),
reason:
'a white fill inside the group must be a small tag — at '
'${w.toStringAsFixed(1)}pt wide it is a row panel, which is '
'the card by another name',
);
}
}
});
testWidgets('an order row carries no tile of its own', (tester) async {
await pumpHome(tester, accepted: const {'1'}, selected: const {'1'});
// Selection and acceptance are carried by the node — shape and fill —
// and by the name's weight. Painting whole rows made a list of five
// steps read as five loose objects on open ground.
final steps = find.byType(PickupTimelineStep);
expect(steps, findsWidgets);
final tinted = tester
.widgetList<AnimatedContainer>(
find.descendant(
of: steps,
matching: find.byType(AnimatedContainer),
),
)
.map((c) => c.decoration)
.whereType<BoxDecoration>()
// The node is a filled disc when selected — that is the channel
// carrying the state, and it is deliberately not a rectangle.
.where((d) => d.shape != BoxShape.circle)
.where((d) => d.color != null && d.color != Colors.transparent);
expect(tinted, isEmpty, reason: 'no wash across a row');
});
});
group('three targets, three jobs', () {
testWidgets('the kitchen header only expands — it never selects', (
tester,
) async {
final selected = <String>[];
await pumpHome(tester, onToggleSelect: selected.add);
// The header is the identity block. Tapping it opens and closes the
// place, and that is the whole of its contract: a gesture that also
// ticked an order would mean the same object did two things depending
// on data the rider cannot see.
await tester.tap(find.text('Vidhya Kitchen'));
await tester.pumpAndSettle();
expect(selected, isEmpty, reason: 'disclosure is not selection');
});
testWidgets('an order row selects, and does not open anything', (
tester,
) async {
final selected = <String>[];
final opened = <String>[];
await pumpHome(tester, onToggleSelect: selected.add);
await openKitchen(tester);
await tester.tap(find.text('Joe Mathew'));
await tester.pumpAndSettle();
expect(selected, ['1']);
expect(opened, isEmpty);
});
testWidgets('the bag block opens details, and does not select', (
tester,
) async {
final selected = <String>[];
await pumpHome(tester, onToggleSelect: selected.add);
await openKitchen(tester);
// Siblings, never nested: two recognisers over the same pixels resolve
// to neither, which is how the chevron was once dead for a whole build.
await tester.tap(find.text('Bag 1'));
await tester.pumpAndSettle();
expect(
selected,
isEmpty,
reason: 'the bag corner is the way in, not a second tick',
);
});
});
group('the metadata is metadata', () {
testWidgets('Pending is neutral, not a warning', (tester) async {
await pumpHome(tester);
// Amber is this app's exception hue. On a fresh morning every kitchen is
// pending, so spending it here opens the whole screen in warning and
// leaves nothing to say with when a stop genuinely goes wrong.
final word = tester.widget<Text>(find.text('Pending'));
expect(word.style!.color, ColorConstants.secondaryText);
expect(word.style!.color, isNot(ColorConstants.warning));
});
testWidgets('a partly accepted kitchen says so as a fraction', (
tester,
) async {
await pumpHome(tester, accepted: const {'1'});
// Two of five accepted must never render the same word as five of five.
expect(find.text('Accepted 1/3'), findsOneWidget);
expect(find.text('Accepted'), findsNothing);
});
testWidgets('the state wears no capsule', (tester) async {
await pumpHome(tester);
// ── Only the boxes that hug the word ──
//
// This walked *every* `DecoratedBox` above the text and demanded none of
// them be filled, which held only while Home had no filled surfaces at
// all. The route now stands on a white `MilerPanel` — layer 1, the page
// having a ground is the point — and that panel is an ancestor of every
// word on the screen, so the test began failing on a surface it was
// never about.
//
// A capsule is a box drawn *around a word*: it is the width of the word
// plus its padding. A page surface is not. Filtering by width keeps the
// original claim — the state is set, not stamped — and makes it immune
// to whatever the page is standing on.
final word = tester.getRect(find.text('Pending'));
final chip = find.ancestor(
of: find.text('Pending'),
matching: find.byType(DecoratedBox),
);
for (final e in chip.evaluate()) {
final box = (e.widget as DecoratedBox).decoration;
if (box is! BoxDecoration) continue;
final r = tester.getRect(find.byWidget(e.widget));
if (r.width > word.width + 48) continue; // a surface, not a capsule
expect(
box.color,
anyOf(isNull, Colors.transparent),
reason: 'operational metadata is set, not stamped',
);
}
});
});
group('the order row', () {
testWidgets('shows the area, never a truncated full address', (
tester,
) async {
await pumpHome(tester);
await openKitchen(tester);
// `Peelamedu`, not `12 SNS Colony, Peelamedu, Coimb…`. The full address
// is a detail-sheet fact; what the rider decides on here is the area.
expect(find.textContaining('641004'), findsNothing);
expect(find.textContaining('12 SNS Colony'), findsNothing);
expect(find.text('Peelamedu'), findsWidgets);
});
testWidgets('one order, one bag — and the column is stable', (
tester,
) async {
await pumpHome(tester);
await openKitchen(tester);
for (final bag in const ['Bag 1', 'Bag 2', 'Bag 3']) {
expect(find.text(bag), findsOneWidget);
}
// Right-aligned on one axis, so the phone can be read against a shelf.
final rights = [
for (final bag in const ['Bag 1', 'Bag 2', 'Bag 3'])
tester.getRect(find.text(bag)).right,
];
for (final r in rights) {
expect(r, closeTo(rights.first, 0.5));
}
});
testWidgets('the name outranks the bag, and the bag yields first', (
tester,
) async {
await pumpHome(tester, textScale: 2.0, width: 320);
await openKitchen(tester);
// `Bag 1` is compared against a shelf, not read as prose, so it stops
// scaling before the customer's name does. Left uncapped it took half a
// 320pt row at 2× and the name — the one item that must yield last —
// was what ellipsised.
final bag = tester.getRect(find.text('Bag 1'));
expect(
bag.width,
lessThan(120),
reason: 'the bag column does not grow into the name',
);
expect(tester.takeException(), isNull, reason: 'and nothing overflows');
});
});
group('the offer states its own number', () {
testWidgets('resting reads "Select all 3"', (tester) async {
await pumpHome(tester);
await openKitchen(tester);
// The count is what makes the offer an offer. It used to be bare, on the
// argument that the card above said `3 orders` — and the card is gone.
expect(find.text('Select all 3'), findsOneWidget);
});
testWidgets('partial and full both state the fraction', (tester) async {
await pumpHome(tester, selected: const {'1'});
await openKitchen(tester);
expect(find.text('1 of 3 selected'), findsOneWidget);
await pumpHome(tester, selected: const {'1', '2', '3'});
await openKitchen(tester);
expect(find.text('All 3 selected · Clear'), findsOneWidget);
});
});
}