Files
doormile_milderapp/test/home_structure_test.dart
2026-08-28 15:07:30 +05:30

473 lines
19 KiB
Dart

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,
String? printed,
}) => {
'orderid': id,
if (printed != null) 'baglabel': printed,
'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,
/// What each counter printed on an order, by id. Empty by default: most
/// counters print nothing, and the app invents nothing for them.
Map<String, String> printedLabels = const {},
}) 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, printed: printedLabels['1']),
meal('2', 'Arun Prakash', step: 2, printed: printedLabels['2']),
meal('3', 'Ravi Kumar', step: 3, printed: printedLabels['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',
);
// ── A rule between rows is not an outline around them ──
//
// This required `border` to be null outright, which caught the thing
// it was written for — a rectangle drawn round a kitchen — and also
// catches a one-sided hairline separating one order from the next.
// Those are opposites: a box groups by enclosing, a rule separates by
// dividing, and the second is the spine's own grammar rather than a
// competitor to it. What stays banned is an enclosure.
final border = d.border;
if (border is Border) {
expect(
border.left.style == BorderStyle.none &&
border.right.style == BorderStyle.none &&
border.top.style == BorderStyle.none,
isTrue,
reason: 'an outline around a group is the card by another name',
);
} else {
expect(
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 corner block opens details, and does not select', (
tester,
) async {
final selected = <String>[];
// The tag is the counter's printed label now — the derived `Bag 1` is
// gone — so the block is tapped by the label the kitchen really stuck on.
await pumpHome(
tester,
onToggleSelect: selected.add,
printedLabels: const {'1': 'DG-1042'},
);
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('DG-1042'));
await tester.pumpAndSettle();
expect(
selected,
isEmpty,
reason: 'the corner is the way in, not a second tick',
);
});
});
group('the metadata is metadata', () {
testWidgets('Pending is its own amber, and never the warning one', (
tester,
) async {
await pumpHome(tester);
// ── What this test used to assert, and what survives of it ──
//
// It demanded neutral grey, on the argument that amber is this app's
// exception hue and every kitchen is pending on a fresh morning — so
// spending it here opens the whole screen in warning.
//
// Half of that argument is still load-bearing and is still checked: the
// word must not be [ColorConstants.warning]. The other half was wrong —
// "waiting on you" is worth a colour of its own, and grey made the rider
// read undecided work by the *absence* of a mark. Pending has its own
// gold now; the thing that must never happen is it colliding with the
// hue reserved for a skip or a short payment.
// `Pending 3/3` — the word carries the count now, the same way Accepted
// states its share of a counter. See the chip in `route_timeline.dart`.
final word = tester.widget<Text>(find.textContaining('Pending'));
expect(word.data, 'Pending 3/3');
expect(word.style!.color, ColorConstants.pendingAmber);
expect(word.style!.color, isNot(ColorConstants.warning));
expect(word.style!.color, isNot(ColorConstants.primary));
});
testWidgets('Accepted is green and Collected is not', (tester) async {
// The two rungs a rider most needs to tell apart on one counter. They
// were both green once — accepted took the brand red only on the group
// being ridden to, and collected took [ColorConstants.acceptGreen] — so
// the pair could render as one hue and two words.
await pumpHome(tester, accepted: const {'1', '2', '3'});
final accepted = tester.widget<Text>(find.textContaining('Accepted'));
expect(accepted.style!.color, ColorConstants.acceptGreen);
expect(
ColorConstants.pickedBlue,
isNot(ColorConstants.acceptGreen),
reason: 'in-hand and taken-on must not share a colour',
);
});
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.textContaining('Pending'));
final chip = find.ancestor(
of: find.textContaining('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('a counter that prints nothing shows no tag', (tester) async {
// ── What this file used to assert here ──
//
// That every step carried `Bag 1 … Bag 3`, right-aligned on one axis so
// the phone could be read against a shelf. The numbering was the app's
// own — derived from each row's position whenever the payload printed
// nothing, which on this fixture is every row — and it sat two columns
// from a rail already numbering the same steps.
//
// Only what a counter actually printed survives. The fixture prints
// nothing, so the rows carry the person and the chevron and no tag at
// all.
await pumpHome(tester);
await openKitchen(tester);
for (final label in const ['Bag 1', 'Bag 2', 'Bag 3']) {
expect(find.text(label), findsNothing);
}
// The people are still there, one row each.
expect(find.text('Joe Mathew'), findsOneWidget);
});
testWidgets('a printed label is shown exactly as it was sent', (
tester,
) async {
// A code the kitchen stuck on the order is a fact about an object in the
// rider's hands, so it travels verbatim — never prefixed, never renamed.
await pumpHome(tester, printedLabels: const {'1': 'DG-1042'});
await openKitchen(tester);
expect(find.text('DG-1042'), findsOneWidget);
expect(find.textContaining('Bag'), findsNothing);
});
});
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);
});
});
}