269 lines
10 KiB
Dart
269 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.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';
|
|
|
|
/// A control a rider reaches for without looking has to be in the same place on
|
|
/// every row of the route.
|
|
///
|
|
/// ── What this used to measure, and why it moved ──
|
|
///
|
|
/// The call button and the tick box on a Home stop *card* were conditional
|
|
/// children of one meta row — `if (phone.isNotEmpty)` and `if (pending &&
|
|
/// onToggleSelect != null)` — so whenever one was missing the other slid across
|
|
/// to take its place. A stop with no phone number pushed its tick a tap-target
|
|
/// to the right; an already-decided stop pushed its call button the same way.
|
|
/// Down a route of mixed states the two zig-zagged, and a rider reaching for
|
|
/// "the tick" had to locate it again on every row.
|
|
///
|
|
/// Both of those controls are gone with the cards. The tick *is* the row now,
|
|
/// and the phone moved into the detail sheet with the rest of the progressive
|
|
/// disclosure. So the principle is measured where it now lives: the timeline's
|
|
/// spine. Every node — a place, an order, whatever state it is in — sits on one
|
|
/// vertical axis, and every row's content starts on one left edge. That is a
|
|
/// stronger version of the same contract, because it holds across two kinds of
|
|
/// row rather than one, and it is what makes a run scannable in a glance.
|
|
///
|
|
/// This is the kind of thing that is obvious in a screenshot and invisible in
|
|
/// code review, so it is measured against a route deliberately built to mix
|
|
/// every state Home still lists.
|
|
void main() {
|
|
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
|
|
|
Map<String, dynamic> stop(
|
|
String id, {
|
|
required String type,
|
|
String? phone,
|
|
String? kitchen,
|
|
}) => {
|
|
'orderid': id,
|
|
'type': type,
|
|
'pickupcustomer': 'Sri Balaji Stores',
|
|
'pickupaddress': '19, Thadagam Road, Coimbatore',
|
|
if (phone != null) 'pickupcontactno': phone,
|
|
if (kitchen != null) 'kitchenid': kitchen,
|
|
if (kitchen != null) 'kitchenname': kitchen,
|
|
'Quantity': '2',
|
|
'expected_pickup_time': DateTime.now().toIso8601String(),
|
|
};
|
|
|
|
/// A route covering every trailing-slot combination.
|
|
///
|
|
/// The states are the three Home still lists — pending, rejected, skipped:
|
|
///
|
|
/// 1 pending + phone → tick, and a way into the sheet
|
|
/// 2 pending, no phone → the same, with nothing to call
|
|
/// 3 rejected + phone → no tick; it is already decided
|
|
/// 4 skipped, no phone → not listed at all
|
|
/// [uniform] forces every stop to one kind, for the tests that are about a
|
|
/// row's **state** rather than its shape.
|
|
///
|
|
/// A mixed route no longer draws one shape: drops gather under the hub they
|
|
/// were loaded at and collections stay their own doors, so a rung and a node
|
|
/// legitimately sit at different indents. See `hub_pickup_group_test.dart`.
|
|
/// Comparing across the two measures the grouping, not the thing under test.
|
|
Future<void> pumpRoute(
|
|
WidgetTester tester, {
|
|
String? kitchen,
|
|
String? uniform,
|
|
bool secondAddress = false,
|
|
}) async {
|
|
tester.view.physicalSize = const Size(1170, 2532);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final trip = Trip.fromStops(
|
|
id: 'mixed',
|
|
stops: [
|
|
stop(
|
|
'A',
|
|
type: uniform ?? 'pickup',
|
|
phone: '9876543210',
|
|
kitchen: kitchen,
|
|
),
|
|
{
|
|
...stop('B', type: uniform ?? 'delivery', kitchen: kitchen),
|
|
if (secondAddress) ...{
|
|
'pickupaddress': '4, Avinashi Rd, Coimbatore',
|
|
'kitchenid': 'K2',
|
|
'kitchenname': 'Second Counter',
|
|
},
|
|
},
|
|
stop(
|
|
'C',
|
|
type: uniform ?? 'combined',
|
|
phone: '9876500000',
|
|
kitchen: kitchen,
|
|
),
|
|
{
|
|
...stop('D', type: uniform ?? 'pickup', kitchen: kitchen),
|
|
'orderstatus': 'skipped',
|
|
},
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: TripCard(
|
|
trip: trip,
|
|
rejectedIds: const {'C'},
|
|
onToggleSelect: (_) {},
|
|
onCallStop: (_) {},
|
|
onStopTap: (_) {},
|
|
onUnrejectStop: (_) async {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('every node sits on one vertical axis', (tester) async {
|
|
ServiceProfile.setActive(ServiceProfile.milkMan);
|
|
await pumpRoute(tester, kitchen: 'K1', secondAddress: true);
|
|
|
|
// ── A logistics run is one node, so the axes are compared on a milk round ──
|
|
//
|
|
// Home groups the parcel line's whole day under the rider's base now, which
|
|
// is the point of the change but leaves a single node with nothing to
|
|
// compare against. A kitchen line still draws one node per counter, which
|
|
// is the shape this test was written for.
|
|
final nodes = find.byType(PickupTimelineGroup);
|
|
expect(nodes.evaluate().length, greaterThan(1), reason: 'two counters');
|
|
|
|
// The spine is a fixed-width rail with the node centred in it, so this
|
|
// holds by construction — which is exactly why it is worth pinning. The
|
|
// moment a state draws a wider node or a row grows a leading control, the
|
|
// rail bends and the run stops being scannable.
|
|
final centres = <double>[
|
|
for (var i = 0; i < nodes.evaluate().length; i++)
|
|
tester.getRect(nodes.at(i)).left,
|
|
];
|
|
for (final c in centres) {
|
|
expect(c, closeTo(centres.first, 0.5));
|
|
}
|
|
});
|
|
|
|
testWidgets('the rail does not move when a row changes state', (
|
|
tester,
|
|
) async {
|
|
// One kind throughout: this is about **state**, and a mixed route now draws
|
|
// two shapes on purpose — see [pumpRoute]'s `uniform`.
|
|
await pumpRoute(tester, uniform: 'pickup');
|
|
|
|
// Two of the four are listed now. The skipped one has always been
|
|
// Activity's row; the **rejected** one joined it — a declined stop leaves
|
|
// Home the moment it is declined rather than sitting struck through with an
|
|
// UNDO beside it. What is left on the route is the undecided work.
|
|
final names = find.text('Sri Balaji Stores');
|
|
expect(names.evaluate().length, 2);
|
|
|
|
final lefts = <double>[
|
|
for (var i = 0; i < 2; i++) tester.getRect(names.at(i)).left,
|
|
];
|
|
for (final l in lefts) {
|
|
expect(
|
|
l,
|
|
closeTo(lefts.first, 0.5),
|
|
reason: 'a rejected row must not indent differently from a pending one',
|
|
);
|
|
}
|
|
});
|
|
|
|
testWidgets('every order in a place shares one axis', (tester) async {
|
|
// ── What this used to say, and why it changed ──
|
|
//
|
|
// It pinned the *place* node and the *order* nodes to one axis, because a
|
|
// spine ran from the header down through the steps and a stepped rail would
|
|
// have bent it.
|
|
//
|
|
// Two things removed that line. The place and its orders are grouped by a
|
|
// card now rather than by a thread, so the header draws no connector below
|
|
// it; and the orders sit in an inset panel *inside* that card, which is what
|
|
// makes them read as its contents. A panel that ignored its own container's
|
|
// inset to keep a nonexistent line straight would just look broken.
|
|
//
|
|
// What still matters — and what actually makes a run scannable — is that
|
|
// every step in a place shares one left edge, whatever state it is in. That
|
|
// is what is pinned here, plus the panel's own inset, so the nesting cannot
|
|
// drift a point at a time.
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await pumpRoute(tester, kitchen: 'Vidhya Kitchen');
|
|
|
|
final group = tester.getRect(find.byType(PickupTimelineGroup).first);
|
|
final steps = find.byType(PickupTimelineStep);
|
|
expect(steps.evaluate(), isNotEmpty, reason: 'the group is open');
|
|
|
|
final lefts = <double>[
|
|
for (var i = 0; i < steps.evaluate().length; i++)
|
|
tester.getRect(steps.at(i)).left,
|
|
];
|
|
for (final l in lefts) {
|
|
expect(l, closeTo(lefts.first, 0.5), reason: 'one axis for every order');
|
|
}
|
|
|
|
expect(
|
|
lefts.first - group.left,
|
|
greaterThan(0),
|
|
reason: 'the orders sit inside the card, not flush against its edge',
|
|
);
|
|
expect(
|
|
lefts.first - group.left,
|
|
// ≤ 22: the panel inset plus the well's own padding. The well returned
|
|
// when the route moved onto a white MilerPanel — open orders were
|
|
// white-on-white — so the deliberate indent is two small steps now, and
|
|
// this bound is what keeps it from becoming three.
|
|
lessThanOrEqualTo(22),
|
|
reason:
|
|
'measured ${lefts.first - group.left} — a nested well, not a second '
|
|
'indent level growing a point at a time',
|
|
);
|
|
});
|
|
|
|
testWidgets('the phone is not on an order row', (tester) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await pumpRoute(tester, kitchen: 'Vidhya Kitchen');
|
|
|
|
// ── Re-scoped, and the contract is unchanged ──
|
|
//
|
|
// This asserted no call control anywhere on the route. That was right while
|
|
// there was nowhere sensible to put one — but ringing a kitchen is an action
|
|
// on the *place*, and it now lives on the pickup card beside Navigate, where
|
|
// the rider can reach it without opening a list of five customers he is not
|
|
// looking for.
|
|
//
|
|
// What the test was really protecting is the thing it is still protecting: a
|
|
// **step** carries who and which bag, and nothing else. Five call buttons
|
|
// dialling one counter is the meta row coming back one control at a time.
|
|
for (
|
|
var i = 0;
|
|
i < find.byType(PickupTimelineStep).evaluate().length;
|
|
i++
|
|
) {
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(PickupTimelineStep).at(i),
|
|
matching: find.byIcon(LucideIcons.phone),
|
|
),
|
|
findsNothing,
|
|
reason: 'step $i grew a call control',
|
|
);
|
|
}
|
|
|
|
// And exactly one on the place that owns it.
|
|
expect(find.byIcon(LucideIcons.phone), findsOneWidget);
|
|
});
|
|
}
|