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>
251 lines
9.7 KiB
Dart
251 lines
9.7 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:miler/data/miler_api.dart';
|
|
import 'package:miler/data/mock_backend.dart';
|
|
import 'package:miler/providers/pickup/pickup_provider.dart';
|
|
|
|
import 'package:miler/data/meal_run_mock.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
import 'package:miler/views/Dashboard/home/trip.dart';
|
|
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// THE MEAL DAY THAT HAS NO BACKEND
|
|
///
|
|
/// The meal line runs on a day held in memory, because the backend has no
|
|
/// kitchens, crates or subscribers to serve. That is a deliberate stand-in, and
|
|
/// it carries the risk the app has already been burned by once: a mock layer was
|
|
/// deleted from this repo after demo stops kept surfacing on live riders' tabs
|
|
/// weeks later.
|
|
///
|
|
/// So these tests are mostly about containment — the day is complete enough to
|
|
/// walk, and there is no path from a parcel login to any of it.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() {
|
|
MealRunMock.reset();
|
|
// The canned day is opt-in now, so the tests that exercise it have to ask
|
|
// for it — exactly as a bench session does. The containment group turns it
|
|
// back off for the assertions that are about the default.
|
|
MockBackend.enabled = true;
|
|
});
|
|
tearDown(() {
|
|
MealRunMock.reset();
|
|
MockBackend.enabled = kMockBackend;
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
});
|
|
|
|
group('containment', () {
|
|
test('a parcel rider cannot reach it, whatever else is true', () {
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
expect(MealRunMock.active, isFalse);
|
|
});
|
|
|
|
test('it is off unless somebody asks for it', () {
|
|
MockBackend.enabled = kMockBackend;
|
|
// ── What this assertion is for ──
|
|
//
|
|
// This switch was off, and it was off for a real reason: while it was on,
|
|
// a meal rider tapped Arrived and Picked up, watched the card advance,
|
|
// and the hub console never changed — every status write was recorded on
|
|
// the phone and reported as sent.
|
|
//
|
|
// It survives as a bench tool, but it is **opt in** now. It used to
|
|
// default to on in debug and profile, which meant the default development
|
|
// experience diverged from production silently: a rider signing in with
|
|
// his real number became the fixture's rider, and a whole class of
|
|
// contract bugs could not be seen because nothing left the device.
|
|
//
|
|
// Now it is off unless somebody types the flag, in every build type.
|
|
expect(
|
|
kMockBackend,
|
|
isFalse,
|
|
reason: 'the canned day must never be what you get by not deciding',
|
|
);
|
|
expect(MockBackend.enabled, isFalse);
|
|
// And the two are one switch, so there is no state where the day is
|
|
// fictional but the statuses are real, or the other way round.
|
|
expect(kMealMockEnabled, MockBackend.enabled);
|
|
});
|
|
|
|
test('the meal line is still the only thing that could turn it on', () {
|
|
// The containment rule survives the switch: `active` is `enabled && meal
|
|
// line`, so re-enabling it for a demo still cannot reach a parcel rider.
|
|
// Asserted on the guard itself, since `active` is false either way now.
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
expect(ServiceProfile.active.sourceIsKitchen, isFalse);
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
expect(ServiceProfile.active.sourceIsKitchen, isTrue);
|
|
});
|
|
|
|
test('every id announces itself as mock', () {
|
|
// `purgeDemoRecords()` matches on exactly this prefix, so anything that
|
|
// leaks into a rider's stores is removed at the next launch by code that
|
|
// already ships. See the note in `accepted_store.dart`.
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
for (final s in MealRunMock.stops()) {
|
|
expect(
|
|
(s['orderid'] as String).toLowerCase(),
|
|
startsWith('mock-'),
|
|
reason: 'a row that does not say it is mock cannot be purged',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('the day itself', () {
|
|
setUp(() => ServiceProfile.setActive(ServiceProfile.meals));
|
|
|
|
test('is two kitchens, so the grouping has something to group', () {
|
|
final kitchens = MealRunMock.stops()
|
|
.map((s) => stopSourceName(s))
|
|
.toSet();
|
|
// One kitchen hides every question the sections exist to answer.
|
|
expect(kitchens.length, 2);
|
|
});
|
|
|
|
test('every stop is a drop, with a bag the rider can read off a box', () {
|
|
for (final s in MealRunMock.stops()) {
|
|
expect(stopKindOf(s).isDelivery, isTrue);
|
|
expect(stopBagLabel(s), isNotEmpty);
|
|
expect((s['pickupcustomer'] as String), isNotEmpty);
|
|
expect((s['pickupaddress'] as String), contains('Coimbatore'));
|
|
}
|
|
});
|
|
|
|
test('nothing is owed at any door', () {
|
|
// A subscriber paid the client by the month. A rupee figure here would be
|
|
// a question the rider cannot answer.
|
|
for (final s in MealRunMock.stops()) {
|
|
expect(stopCollectionAmount(s), 0);
|
|
}
|
|
});
|
|
|
|
test('it starts undecided — the rider has done nothing yet', () {
|
|
for (final s in MealRunMock.stops()) {
|
|
expect(s['orderstatus'], 'assigned');
|
|
}
|
|
});
|
|
|
|
test('it groups into one trip in route order', () {
|
|
final trips = Trip.groupIntoTrips(MealRunMock.stops());
|
|
expect(trips, hasLength(1));
|
|
expect(trips.single.stops.map((s) => s['step']), [
|
|
1,
|
|
2,
|
|
3,
|
|
4,
|
|
5,
|
|
6,
|
|
7,
|
|
8,
|
|
9,
|
|
]);
|
|
});
|
|
});
|
|
|
|
group('walking it', () {
|
|
setUp(() => ServiceProfile.setActive(ServiceProfile.meals));
|
|
|
|
test('a status change sticks, and only touches its own stop', () {
|
|
MealRunMock.setStatus('MOCK-M-1001', 'accepted');
|
|
|
|
final byId = {
|
|
for (final s in MealRunMock.stops()) s['orderid']: s['orderstatus'],
|
|
};
|
|
expect(byId['MOCK-M-1001'], 'accepted');
|
|
expect(byId['MOCK-M-1002'], 'assigned');
|
|
});
|
|
|
|
test('the controllers key on the booking id they actually carry', () {
|
|
// Every status method has an int `pickupId` and often no order id at all,
|
|
// so the mock has to resolve one from the other or half the ladder writes
|
|
// nothing.
|
|
expect(MealRunMock.setStatusByPickupId(1002, 'arrived'), isTrue);
|
|
final row = MealRunMock.stops().firstWhere(
|
|
(s) => s['orderid'] == 'MOCK-M-1002',
|
|
);
|
|
expect(row['orderstatus'], 'arrived');
|
|
});
|
|
|
|
test('the whole ladder runs: assigned → accepted → arrived → picked', () {
|
|
const id = 'MOCK-M-1003';
|
|
String statusOf() =>
|
|
MealRunMock.stops().firstWhere(
|
|
(s) => s['orderid'] == id,
|
|
)['orderstatus']
|
|
as String;
|
|
|
|
expect(statusOf(), 'assigned');
|
|
for (final rung in ['accepted', 'arrived', 'picked']) {
|
|
MealRunMock.setStatus(id, rung);
|
|
expect(statusOf(), rung);
|
|
}
|
|
});
|
|
|
|
test('reset puts the day back, so a demo can be run twice', () {
|
|
MealRunMock.setStatus('MOCK-M-1001', 'picked');
|
|
MealRunMock.reset();
|
|
expect(
|
|
MealRunMock.stops().every((s) => s['orderstatus'] == 'assigned'),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('handing out rows never hands out the master copy', () {
|
|
// The screens stamp compliance and proof blocks onto stop maps in place.
|
|
// Sharing the originals would carry one run's leftovers into the next.
|
|
MealRunMock.stops().first['pickupcustomer'] = 'SCRIBBLED';
|
|
expect(MealRunMock.stops().first['pickupcustomer'], isNot('SCRIBBLED'));
|
|
});
|
|
});
|
|
|
|
// ── The wiring, not just the fixture ──
|
|
//
|
|
// The day above is only useful if the app actually receives it. These walk the
|
|
// real path — provider, transport, auth — with no network available, which is
|
|
// exactly the situation a developer running `flutter run` on a bench is in.
|
|
group('the app runs on it', () {
|
|
test(
|
|
'the queue provider serves the day without touching the network',
|
|
() async {
|
|
ServiceProfile.setActive(ServiceProfile.meals);
|
|
final stops = await PickupProvider().getPickupQueues();
|
|
|
|
expect(stops, hasLength(9));
|
|
expect(
|
|
stops.every((s) => (s['orderid'] as String).startsWith('MOCK-')),
|
|
isTrue,
|
|
reason:
|
|
'every row is self-identifying, so a leak is purged at launch',
|
|
);
|
|
},
|
|
);
|
|
|
|
test('a parcel rider still gets the real call, and no fixture', () async {
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
// The transport answers rather than the fixture: an empty booking list,
|
|
// not somebody else's kitchens. Inventing parcel work is the one thing
|
|
// this mock must not do.
|
|
final stops = await PickupProvider().getPickupQueues();
|
|
expect(stops, isEmpty);
|
|
});
|
|
|
|
test('every write is answered without a server', () async {
|
|
final res = await MilerApi.pickupComplete(1001);
|
|
expect(res.ok, isTrue);
|
|
expect(res.status, 200);
|
|
});
|
|
|
|
test('signing in works with no directory to ask', () async {
|
|
expect(MockBackend.enabled, isTrue);
|
|
// The profile the mock hands back is what puts the build on the meal
|
|
// line, so the login and the day agree about which app the rider is in.
|
|
expect(MockBackend.rider['tenantname'], 'DailyGrubs');
|
|
});
|
|
});
|
|
}
|