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

430 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',
);
// ── 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 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);
});
});
}