786 lines
28 KiB
Dart
786 lines
28 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/helpers/widgets/slide_action.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'package:miler/data/accepted_store.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
|
|
import 'package:miler/Models/stop_status.dart';
|
|
import 'package:miler/data/order_manifest.dart';
|
|
import 'package:miler/views/Dashboard/home/stop_action_sheet.dart';
|
|
import 'package:miler/views/Dashboard/home/trip.dart';
|
|
import 'package:miler/views/Dashboard/home/selection_bar.dart';
|
|
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
|
|
import 'package:miler/views/Dashboard/home/trip_card.dart';
|
|
|
|
/// P3/P4 — the collect-then-deliver run.
|
|
///
|
|
/// The rule the whole flow hangs on: **Home holds an order until it is in the
|
|
/// rider's hands, Bookings holds it after.** Every test here is a way that rule
|
|
/// can break.
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
|
|
|
Map<String, dynamic> meal({
|
|
required String id,
|
|
required int step,
|
|
required String kitchen,
|
|
String kitchenId = '',
|
|
String status = 'ASSIGNED',
|
|
}) => {
|
|
'orderid': id,
|
|
'step': step,
|
|
'pickupcustomer': 'Customer $id',
|
|
'pickupaddress': '$step Main Road, Anna Nagar',
|
|
'orderstatus': status,
|
|
'type': 'delivery',
|
|
'quantity': 1,
|
|
'kitchenname': kitchen,
|
|
if (kitchenId.isNotEmpty) 'kitchenid': kitchenId,
|
|
'baglabel': 'DG-$id',
|
|
'pickuplat': 13.08,
|
|
'pickuplon': 80.21,
|
|
};
|
|
|
|
group('the collected state', () {
|
|
final stop = meal(id: '1', step: 1, kitchen: 'Kitchen 1');
|
|
|
|
test('carrying it beats a server view that still says accepted', () {
|
|
// The refetch is always a poll or two behind the rider's own hands. If
|
|
// the server won here the card would bounce back onto Home after he had
|
|
// already left the kitchen with the food.
|
|
final state = stopStateOf(
|
|
{...stop, 'orderstatus': 'accepted'},
|
|
acceptedIds: {'1'},
|
|
rejectedIds: const {},
|
|
collectedIds: {'1'},
|
|
);
|
|
expect(state, StopState.collected);
|
|
});
|
|
|
|
test('but a delivered stop is still delivered', () {
|
|
// `picked` is the hand-over at the door and it is terminal — nothing
|
|
// local may drag it backwards into "still carrying it".
|
|
final state = stopStateOf(
|
|
{...stop, 'orderstatus': 'picked'},
|
|
acceptedIds: const {},
|
|
rejectedIds: const {},
|
|
collectedIds: {'1'},
|
|
);
|
|
expect(state, StopState.done);
|
|
});
|
|
|
|
test('a parcel route never sees the state at all', () {
|
|
final state = stopStateOf(
|
|
stop,
|
|
acceptedIds: {'1'},
|
|
rejectedIds: const {},
|
|
// Default empty — no Doormile call site passes this.
|
|
);
|
|
expect(state, StopState.accepted);
|
|
});
|
|
|
|
test('it counts as committed but not as resolved', () {
|
|
// Committed: he owes somebody a delivery. Not resolved: the trip is not
|
|
// finished, and a ring that read 100% with ten meals in the box would be
|
|
// telling him he could go home.
|
|
expect(StopState.collected.isCommitted, isTrue);
|
|
expect(StopState.collected.isResolved, isFalse);
|
|
});
|
|
});
|
|
|
|
group('Home holds it until it is in his hands', () {
|
|
test('an accepted meal stays on Home; an accepted parcel does not', () {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
final trip = Trip.fromStops(
|
|
id: 't',
|
|
stops: [meal(id: '1', step: 1, kitchen: 'Kitchen 1')],
|
|
);
|
|
final card = TripCard(trip: trip, acceptedIds: const {'1'});
|
|
// Rendering is asserted below; this is the rule underneath it.
|
|
expect(ServiceProfile.active.handsOffAtCollection, isTrue);
|
|
expect(card.trip.stops.length, 1);
|
|
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
expect(ServiceProfile.active.handsOffAtCollection, isFalse);
|
|
});
|
|
|
|
testWidgets('the row is on Home when accepted, gone once collected', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final trip = Trip.fromStops(
|
|
id: 't',
|
|
stops: [meal(id: '1', step: 1, kitchen: 'Kitchen 1')],
|
|
);
|
|
|
|
Future<void> pump(Set<String> collected) async {
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: TripCard(
|
|
trip: trip,
|
|
acceptedIds: const {'1'},
|
|
collectedIds: collected,
|
|
).buildStops(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
await pump(const {});
|
|
expect(find.text('Customer 1'), findsOneWidget);
|
|
expect(find.text('Accepted'), findsOneWidget);
|
|
|
|
// Collected → it belongs to Bookings now.
|
|
await pump({'1'});
|
|
expect(find.text('Customer 1'), findsNothing);
|
|
});
|
|
});
|
|
|
|
group('kitchen grouping', () {
|
|
testWidgets('one heading per kitchen, in route order', (tester) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
tester.view.physicalSize = const Size(1080, 3000);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final trip = Trip.fromStops(
|
|
id: 't',
|
|
stops: [
|
|
meal(id: '1', step: 1, kitchen: 'Kitchen 1', kitchenId: 'K1'),
|
|
meal(id: '2', step: 2, kitchen: 'Kitchen 1', kitchenId: 'K1'),
|
|
meal(id: '3', step: 3, kitchen: 'Kitchen 2', kitchenId: 'K2'),
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: TripCard(
|
|
trip: trip,
|
|
acceptedIds: const {'1', '2', '3'},
|
|
).buildStops(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Two kitchens, named once each however many meals hang off them.
|
|
expect(find.text('Kitchen 1'), findsOneWidget);
|
|
expect(find.text('Kitchen 2'), findsOneWidget);
|
|
// No second bulk control up here: the heading gathers the counter's
|
|
// orders for the bar, and the bar is the only thing that commits.
|
|
expect(find.textContaining('Collect '), findsNothing);
|
|
// One order, one bag — the two numbers the rider checks against the
|
|
// shelf, and they are the same number by definition. See [OrderManifest].
|
|
expect(find.text('2 orders'), findsOneWidget);
|
|
expect(find.text('1 order'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a heading gathers one rung, never a mixed batch', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
List<String>? picked;
|
|
final trip = Trip.fromStops(
|
|
id: 't',
|
|
stops: [
|
|
meal(id: '1', step: 1, kitchen: 'Kitchen 1'),
|
|
meal(id: '2', step: 2, kitchen: 'Kitchen 1'),
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: TripCard(
|
|
trip: trip,
|
|
// Only one of the two taken on.
|
|
acceptedIds: const {'1'},
|
|
// The group's select-all is a *selection-mode* control now —
|
|
// it appears once the rider has ticked something and leaves
|
|
// when he has not, rather than sitting in every kitchen all
|
|
// day. Order 2 is ticked here, which puts the run in that
|
|
// mode without touching the accepted rung this test is about.
|
|
selectedIds: const {'2'},
|
|
onToggleSelect: (_) {},
|
|
onSelectAll: (ids) => picked = ids,
|
|
).buildStops(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// The header still says how much of the counter is not his yet.
|
|
expect(find.text('2 orders · 1 to accept'), findsOneWidget);
|
|
|
|
// And its tick gathers only the orders on one rung. He cannot be handed
|
|
// food for an order he has not taken on, so a heading that swept both
|
|
// into one selection would offer a verb that is wrong for half of them —
|
|
// see `_selectionAction`, which falls back to Accept on a mixed set.
|
|
// Ported: the kitchen heading's tick became the group's one selection
|
|
// control. Same job, same rule — it gathers the counter, on one rung.
|
|
await tester.tap(find.textContaining('Select all'));
|
|
await tester.pumpAndSettle();
|
|
expect(picked, ['1'], reason: 'only the accepted one');
|
|
});
|
|
});
|
|
|
|
// ── The pickup confirmation is the only place a handover is confirmed ──
|
|
//
|
|
// There used to be two bulk flows at a counter: this ladder, and a separate
|
|
// `Collect N` sheet with its own tick-list. They wrote different state and
|
|
// taught two mental models for one physical act. The ladder won; these tests
|
|
// are the load sheet's contracts, moved onto the sheet that absorbed them —
|
|
// the short pick, the empty crate, and the split between what he took and
|
|
// what he did not.
|
|
group('the pickup confirmation', () {
|
|
Future<RungOutcome?> open(
|
|
WidgetTester tester,
|
|
int count, {
|
|
List<String> markMissing = const [],
|
|
}) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
RungOutcome? seen;
|
|
final stops = [
|
|
for (var i = 1; i <= count; i++)
|
|
meal(id: '$i', step: i, kitchen: 'Kitchen 1'),
|
|
];
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: StopActionSheet(
|
|
stage: StopStatus.arrived,
|
|
placeName: 'Kitchen 1',
|
|
count: stops.length,
|
|
lines: OrderManifest.forGroup(stops),
|
|
onConfirm: (outcome) async {
|
|
seen = outcome;
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
if (markMissing.isNotEmpty) {
|
|
await tester.tap(find.text('Report a missing order'));
|
|
await tester.pumpAndSettle();
|
|
for (final name in markMissing) {
|
|
await tester.tap(find.text(name));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
}
|
|
return seen;
|
|
}
|
|
|
|
testWidgets('nothing is marked by default — the exception is what he marks', (
|
|
tester,
|
|
) async {
|
|
await open(tester, 3);
|
|
|
|
// A screen that makes him tick three boxes to record "nothing was wrong"
|
|
// is a screen he learns to dismiss without reading. So the common case is
|
|
// a list he reads, and the shortfall is one tap away.
|
|
expect(find.text('3 orders'), findsOneWidget);
|
|
expect(find.text('Report a missing order'), findsOneWidget);
|
|
expect(find.text('Missing'), findsNothing);
|
|
expect(
|
|
find.text('Make sure all 3 orders are loaded before continuing.'),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
|
|
testWidgets('marking one reports a short pick, and says so', (
|
|
tester,
|
|
) async {
|
|
await open(tester, 3, markMissing: ['Customer 2']);
|
|
|
|
expect(find.text('2 orders · 1 missing'), findsOneWidget);
|
|
expect(find.text('Missing'), findsOneWidget);
|
|
expect(
|
|
find.text('1 of 3 not supplied — they will leave your route.'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('Slide to confirm pickup'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('taking nothing at all is not a collection', (tester) async {
|
|
await open(tester, 2, markMissing: ['Customer 1', 'Customer 2']);
|
|
|
|
// An empty crate is a wasted trip and a call to the hub, not a state to
|
|
// file silently. The gesture goes dead and the sheet says why.
|
|
expect(find.text('Nothing to collect'), findsOneWidget);
|
|
expect(
|
|
find.text('Nothing was handed over — call your office before you leave.'),
|
|
findsOneWidget,
|
|
);
|
|
// ── Disabled is a different shape, not a dimmed one ──
|
|
//
|
|
// It used to be the slide control wrapped in an `IgnorePointer`, which
|
|
// is a control that still looks draggable and silently is not: the rider
|
|
// drags, nothing happens, and he learns the sheet is broken. With
|
|
// nothing to collect there is no slide control at all — just an inert
|
|
// bar carrying the reason.
|
|
expect(
|
|
find.byType(MilerSlideAction),
|
|
findsNothing,
|
|
reason: 'a gesture that cannot fire must not be offered',
|
|
);
|
|
});
|
|
|
|
testWidgets('the outcome splits what he took from what he did not', (
|
|
tester,
|
|
) async {
|
|
RungOutcome? captured;
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final stops = [
|
|
meal(id: '1', step: 1, kitchen: 'Kitchen 1'),
|
|
meal(id: '2', step: 2, kitchen: 'Kitchen 1'),
|
|
];
|
|
final sheet = StopActionSheet(
|
|
stage: StopStatus.arrived,
|
|
placeName: 'Kitchen 1',
|
|
count: stops.length,
|
|
lines: OrderManifest.forGroup(stops),
|
|
onConfirm: (outcome) async {
|
|
captured = outcome;
|
|
return null;
|
|
},
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(home: Scaffold(body: sheet)),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('Report a missing order'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Customer 2'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Drive the confirmation the way the slide does, without simulating a
|
|
// 300px drag: what is pinned here is the *payload*, not the gesture.
|
|
final state = tester.state(find.byType(StopActionSheet));
|
|
// ignore: avoid_dynamic_calls
|
|
await (state as dynamic).confirmForTest();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(captured, isNotNull);
|
|
expect(captured!.missingOrderIds, ['2']);
|
|
expect(captured!.isClean, isFalse);
|
|
});
|
|
});
|
|
|
|
group('the collected store', () {
|
|
test('records, then clears once delivered', () async {
|
|
await addCollectedOrderIds(['1', '2']);
|
|
expect(await getCollectedOrderIds(), {'1', '2'});
|
|
|
|
await removeCollectedOrderIds(['1']);
|
|
expect(await getCollectedOrderIds(), {'2'});
|
|
});
|
|
|
|
test('a short pick is its own record, not a skip', () async {
|
|
await addNotLoadedOrderIds(['9']);
|
|
expect(await getNotLoadedOrderIds(), {'9'});
|
|
// Nobody was visited and the customer did nothing wrong, so it must not
|
|
// land in the skip store where it would read as a failed doorstep.
|
|
expect(await getSkippedOrderIds(), isEmpty);
|
|
});
|
|
|
|
test('a day can be wiped so ids never leak into tomorrow', () async {
|
|
await addCollectedOrderIds(['1']);
|
|
await addNotLoadedOrderIds(['2']);
|
|
await clearServiceRunState();
|
|
expect(await getCollectedOrderIds(), isEmpty);
|
|
expect(await getNotLoadedOrderIds(), isEmpty);
|
|
});
|
|
});
|
|
|
|
group('accept does not hand the meal over to Bookings', () {
|
|
/// The gate Bookings applies to decide what the rider is working now.
|
|
/// Mirrors the filter in `pickups.dart` so the rule can be asserted without
|
|
/// standing up the whole tab.
|
|
Future<Set<String>> workableIds() async =>
|
|
ServiceProfile.active.handsOffAtCollection
|
|
? await getCollectedOrderIds()
|
|
: await getAcceptedOrderIds();
|
|
|
|
test(
|
|
'accepted-but-not-collected is not workable on a service run',
|
|
() async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
// Accepting writes to the accepted store — that is how the stop knows it
|
|
// has been taken on, and Home needs it to render "Accepted".
|
|
await addAcceptedBookings([
|
|
{'orderid': '1', 'orderstatus': 'accepted'},
|
|
]);
|
|
|
|
// But he is carrying nothing yet, so Bookings must be empty. Letting
|
|
// accept fill that tab hands him a delivery list for food still being
|
|
// cooked, and empties Home where the collection is organised.
|
|
expect(await workableIds(), isEmpty);
|
|
|
|
await addCollectedOrderIds(['1']);
|
|
expect(await workableIds(), {'1'});
|
|
},
|
|
);
|
|
|
|
test('a parcel is workable the moment it is accepted', () async {
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
await addAcceptedBookings([
|
|
{'orderid': '9', 'orderstatus': 'accepted'},
|
|
]);
|
|
// Unchanged behaviour: for a parcel, accepting IS the hand-off.
|
|
expect(await workableIds(), {'9'});
|
|
});
|
|
|
|
test('the two stores stay independent', () async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await addAcceptedBookings([
|
|
{'orderid': '1', 'orderstatus': 'accepted'},
|
|
{'orderid': '2', 'orderstatus': 'accepted'},
|
|
]);
|
|
await addCollectedOrderIds(['1']);
|
|
|
|
// Both taken on; only one in his hands. Home keeps the second, Bookings
|
|
// takes the first.
|
|
expect(await getAcceptedOrderIds(), {'1', '2'});
|
|
expect(await getCollectedOrderIds(), {'1'});
|
|
});
|
|
});
|
|
|
|
group('the ladder, worked from the selection bar', () {
|
|
// A rider at a kitchen with five parcels takes one handover, so he ticks
|
|
// the five and slides once per rung. The row briefly carried its own
|
|
// Arrived / Picked up buttons — five buttons pressed twice for one event —
|
|
// and they are gone.
|
|
|
|
test('each rung names itself in the rider\'s voice', () {
|
|
// "Mark as…" rather than "I've…": the button opens a sheet that then
|
|
// asks him to confirm the same words, so the two read as one action.
|
|
expect(SelectionAction.accept.label, 'Accept');
|
|
expect(SelectionAction.arrive.label, 'Mark as Arrived');
|
|
expect(SelectionAction.pickUp.label, 'Mark as Picked');
|
|
});
|
|
|
|
test('each rung writes the right status token', () {
|
|
expect(SelectionAction.accept.status, 'ACCEPTED');
|
|
expect(SelectionAction.arrive.status, 'ARRIVED');
|
|
expect(SelectionAction.pickUp.status, 'PICKED');
|
|
});
|
|
|
|
test(
|
|
'the two that move parcels are confirmed in a sheet; accepting is not',
|
|
() {
|
|
// Accepting is reversible and happens in bulk at the start of a shift, so
|
|
// it commits from the bar. "Picked" is the rider asserting he is holding
|
|
// somebody's lunch — that one gets a sheet with a slide in it, where
|
|
// there is room to say which counter and how many orders he is speaking
|
|
// for. The slide used to be squeezed inline onto the bar itself.
|
|
expect(SelectionAction.accept.confirmsInSheet, isFalse);
|
|
expect(SelectionAction.arrive.confirmsInSheet, isTrue);
|
|
expect(SelectionAction.pickUp.confirmsInSheet, isTrue);
|
|
},
|
|
);
|
|
|
|
Future<void> pumpRow(
|
|
WidgetTester tester, {
|
|
required String status,
|
|
Set<String> accepted = const {'1'},
|
|
}) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final trip = Trip.fromStops(
|
|
id: 't',
|
|
stops: [meal(id: '1', step: 1, kitchen: 'Kitchen 1', status: status)],
|
|
);
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: TripCard(
|
|
trip: trip,
|
|
acceptedIds: accepted,
|
|
onToggleSelect: (_) {},
|
|
).buildStops(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
// ── The rung is pressed on the bar and *committed* in a sheet ──
|
|
//
|
|
// The bar's job at this point in the shift is to be unmissable: he is
|
|
// holding a crate at a counter. The commit's job is to be deliberate. Those
|
|
// are opposite requirements, which is why they are now two surfaces —
|
|
// a full-width button that opens [StopActionSheet], and a slide inside it.
|
|
Future<void> pumpBar(
|
|
WidgetTester tester, {
|
|
required SelectionAction action,
|
|
required VoidCallback onAdvance,
|
|
}) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: SelectionBar(
|
|
count: 3,
|
|
action: action,
|
|
onAccept: null,
|
|
onClear: () {},
|
|
onAdvance: () async => onAdvance(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('the bar names the rung and how many it covers', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await pumpBar(tester, action: SelectionAction.arrive, onAdvance: () {});
|
|
|
|
expect(find.text('Mark as Arrived (3)'), findsOneWidget);
|
|
// Nothing shares the bar with it: a stop already taken on cannot be
|
|
// rejected from here, and the count is on the button itself.
|
|
expect(find.text('Reject'), findsNothing);
|
|
expect(find.textContaining('selected'), findsNothing);
|
|
});
|
|
|
|
testWidgets('the pick-up rung is the same control, renamed', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await pumpBar(tester, action: SelectionAction.pickUp, onAdvance: () {});
|
|
expect(find.text('Mark as Picked (3)'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('pressing it hands off rather than writing anything', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
var opened = 0;
|
|
await pumpBar(
|
|
tester,
|
|
action: SelectionAction.arrive,
|
|
onAdvance: () => opened++,
|
|
);
|
|
|
|
await tester.tap(find.text('Mark as Arrived (3)'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// The press opens the confirmation; the slide inside it is what commits.
|
|
expect(opened, 1);
|
|
});
|
|
|
|
testWidgets('a kitchen heading ticks every order off that counter', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
List<String>? picked;
|
|
final trip = Trip.fromStops(
|
|
id: 't',
|
|
stops: [
|
|
meal(
|
|
id: '1',
|
|
step: 1,
|
|
kitchen: 'Kitchen 1',
|
|
kitchenId: 'K1',
|
|
status: 'accepted',
|
|
),
|
|
meal(
|
|
id: '2',
|
|
step: 2,
|
|
kitchen: 'Kitchen 1',
|
|
kitchenId: 'K1',
|
|
status: 'accepted',
|
|
),
|
|
meal(
|
|
id: '3',
|
|
step: 3,
|
|
kitchen: 'Kitchen 2',
|
|
kitchenId: 'K2',
|
|
status: 'accepted',
|
|
),
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: TripCard(
|
|
trip: trip,
|
|
acceptedIds: const {'1', '2', '3'},
|
|
// Selection mode, entered on the *other* kitchen — which also
|
|
// sharpens the assertion below: Kitchen 2 has a live tick of
|
|
// its own, and gathering Kitchen 1 must not disturb it.
|
|
selectedIds: const {'3'},
|
|
onToggleSelect: (_) {},
|
|
onSelectAll: (ids) => picked = ids,
|
|
).buildStops(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// He arrives at a kitchen, not at an order — so the group's own control
|
|
// is where the whole counter is gathered for the bar underneath.
|
|
await tester.tap(find.textContaining('Select all'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(picked, ['1', '2'], reason: 'the other kitchen is left alone');
|
|
});
|
|
|
|
testWidgets('the row states where the stop is and offers no control', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await pumpRow(tester, status: 'accepted');
|
|
expect(find.text('Customer 1'), findsOneWidget);
|
|
expect(find.text('Mark as Arrived'), findsNothing);
|
|
expect(find.text('Arrived'), findsNothing);
|
|
expect(find.text('Picked up'), findsNothing);
|
|
});
|
|
|
|
testWidgets('the tick survives the accept, so the set can be slid', (
|
|
tester,
|
|
) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await pumpRow(tester, status: 'accepted');
|
|
// Without this the bar disappears the instant he accepts the very stops
|
|
// he is about to walk into a kitchen with.
|
|
expect(find.byType(Checkbox), findsNothing); // it is a custom box
|
|
// Ported: the card's tick box became the timeline row itself, which
|
|
// announces as a selection control while one is meaningful. Same
|
|
// contract — the affordance outlives the accept on a meal run.
|
|
expect(find.bySemanticsLabel(RegExp('^Select ')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a parcel row loses its tick once accepted', (tester) async {
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
await pumpRow(tester, status: 'accepted');
|
|
// Ticking exists only to choose what to take on.
|
|
expect(find.bySemanticsLabel(RegExp('^Select ')), findsNothing);
|
|
});
|
|
|
|
testWidgets('picked up leaves Home', (tester) async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
await pumpRow(tester, status: 'picked');
|
|
expect(find.text('Customer 1'), findsNothing);
|
|
});
|
|
});
|
|
|
|
group('the doorstep chain', () {
|
|
// "I've handed over" → next order. Everything that used to sit between
|
|
// those two is gated off, and each gate is a different mechanism, so they
|
|
// are asserted together rather than trusted individually.
|
|
|
|
test('nothing stands between the hand-over and the next order', () {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
final p = ServiceProfile.active;
|
|
|
|
// The proof-of-work page — parcel ticks, weight, condition, OTP, review.
|
|
expect(p.needsVerification, isFalse);
|
|
// The camera on the final rung.
|
|
expect(p.needsProofPhoto, isFalse);
|
|
// The payment page, and the amount that would open it.
|
|
expect(p.collectsCash, isFalse);
|
|
expect(stopCollectionAmount({'collectionamt': 450}), 0);
|
|
expect(stopCollectionAmount({'codamount': '₹1,200'}), 0);
|
|
});
|
|
|
|
test('a parcel stop keeps every one of them', () {
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
final p = ServiceProfile.active;
|
|
expect(p.needsVerification, isTrue);
|
|
expect(p.needsProofPhoto, isTrue);
|
|
expect(p.collectsCash, isTrue);
|
|
expect(stopCollectionAmount({'collectionamt': 450}), 450);
|
|
});
|
|
});
|
|
}
|