Files
doormile_milderapp/test/delivery_line_test.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
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>
2026-08-22 05:40:35 +05:30

197 lines
7.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/helpers/rider_shell.dart';
import 'package:miler/widget/Bottom_page.dart' as parcel;
/// ─────────────────────────────────────────────────────────────────────────
/// THE XPRESS SUBTREE IS A REFERENCE, NOT A DESTINATION
///
/// `lib/xpress` is a verbatim port of the Xpress-rider app, kept beside this
/// one so its proven delivery UX can be read while the Miler-native milk run is
/// built. It is **not** one of the two operations the hub runs, and the Milk Man
/// flow is implemented natively in Miler — Miler screens, Miler controllers,
/// Miler API layer — rather than by routing riders into it.
///
/// ── What this file used to assert, and why it changed ──
///
/// It used to prove the opposite: that a tenant *named* "delivery"/"xpress", or
/// an account in `configid: 6`, would land the rider in that other application.
/// That was reasonable while the tenant was unknowable — the build flag and a
/// name were the only signals there were.
///
/// It is not reasonable now. `verify-pin` returns a real `tenantname`, which
/// means an arbitrary string chosen by whoever set up a client in the admin
/// console reaches this resolver. A client called "Delivery" is an entirely
/// plausible thing to create, and under the old rule that alone would have
/// dropped a live rider into a different app with a different bottom bar, a
/// different set of endpoints and no shipment desk. The failure would look, to
/// him, like the app breaking.
///
/// So the wire cannot reach it any more. Only `--dart-define=TENANT=delivery`
/// can, and that is a compile-time constant no test can set — which is the
/// point. What is tested here is the closed door.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
Future<ServiceProfile> resolveWith(Map<String, Object> prefs) async {
SharedPreferences.setMockInitialValues(prefs);
final profile = await resolveServiceProfile();
ServiceProfile.setActive(profile);
return profile;
}
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
group('the wire cannot reach the ported line', () {
test('no tenant name routes a rider out of Miler', () async {
// Every name that used to open it. Each one must now resolve to one of
// the two real operations — in practice logistics, the safe fallback.
for (final name in [
'delivery',
'xpress',
'express',
'doormile xpress',
'doormilexpress',
' XPress ',
]) {
final p = await resolveWith({'tenantname': name});
expect(
p.isDelivery,
isFalse,
reason: 'tenantname=$name must not open the ported app',
);
expect(p.line, ServiceLine.parcel, reason: 'tenantname=$name');
}
});
test('no tenant id routes a rider out of Miler', () async {
expect(TenantController.deliveryTenantIds, isEmpty);
for (final id in [1, 6, 99, 1001]) {
final p = await resolveWith({'tenantid': id});
expect(p.isDelivery, isFalse, reason: 'tenantid=$id');
}
});
test('configid 6 no longer decides anything', () async {
// Xpress-rider signed its riders into partition 6, and this resolver used
// to read that as "give him the other app". A partition is an account
// location, not a statement about which business a rider works for, and
// it was the weakest of the three signals — so it is no longer consulted.
final p = await resolveWith({'configid': 6});
expect(p.line, ServiceLine.parcel);
});
testWidgets('and no resolvable tenant produces the Xpress shell', (
tester,
) async {
for (final prefs in <Map<String, Object>>[
{'tenantname': 'xpress'},
{'tenantname': 'delivery'},
{'configid': 6},
{'tenantid': 6},
{},
]) {
await resolveWith(prefs);
expect(riderShell(), isA<parcel.BottomPage>(), reason: '$prefs');
}
});
});
group('the two operations the hub actually runs', () {
test('a milk-man tenant resolves to the milk run', () async {
final p = await resolveWith({'tenantname': 'milkman'});
expect(p.line, ServiceLine.milkMan);
expect(p.isMilkMan, isTrue);
});
test('a logistics tenant resolves to logistics', () async {
final p = await resolveWith({'tenantname': 'logistics'});
expect(p.line, ServiceLine.parcel);
expect(p.isLogistics, isTrue);
});
test('an unknown tenant falls back to logistics', () async {
final p = await resolveWith({'tenantname': 'someclient', 'tenantid': 99});
expect(p.line, ServiceLine.parcel);
});
});
group('both operations share one Miler dashboard', () {
testWidgets('logistics gets it', (tester) async {
await resolveWith({'tenantname': 'logistics'});
expect(riderShell(), isA<parcel.BottomPage>());
});
testWidgets('the milk run gets it too', (tester) async {
// Milk Man is a capability variant of the Miler screens, not a separate
// app — which is the whole point of §2 of the brief.
await resolveWith({'tenantname': 'milkman'});
expect(riderShell(), isA<parcel.BottomPage>());
});
testWidgets('the requested tab is carried through', (tester) async {
await resolveWith({'tenantname': 'milkman'});
expect(
(riderShell(initialIndex: 2) as parcel.BottomPage).initialIndex,
2,
);
await resolveWith({'tenantname': 'logistics'});
expect(
(riderShell(initialIndex: 2) as parcel.BottomPage).initialIndex,
2,
);
});
testWidgets('an override page is honoured', (tester) async {
const marker = SizedBox.shrink();
await resolveWith({'tenantname': 'milkman'});
expect(
(riderShell(overridePage: marker) as parcel.BottomPage).overridePage,
same(marker),
);
await resolveWith({'tenantname': 'logistics'});
expect(
(riderShell(overridePage: marker) as parcel.BottomPage).overridePage,
same(marker),
);
});
});
group('the ported profile still describes itself honestly', () {
// The profile object survives even though nothing off the wire selects it:
// the build flag still reaches it, and a profile that lies about the work
// would be worse than one nobody uses.
test('names the work Doormile, never the app it was ported from', () {
const p = ServiceProfile.delivery;
expect(p.label, 'Doormile');
expect(p.label.toLowerCase(), isNot(contains('nearle')));
expect(p.label.toLowerCase(), isNot(contains('xpress')));
});
test('speaks in deliveries, not bookings', () {
const p = ServiceProfile.delivery;
expect(p.jobNoun, 'delivery');
expect(p.workTabLabel, 'Deliveries');
expect(p.completionVerb, 'Delivered');
});
test('is not the milk run in disguise', () {
const p = ServiceProfile.delivery;
expect(p.collectsCash, isTrue);
expect(p.acceptsPerStop, isTrue);
expect(p.sourceIsKitchen, isFalse);
expect(p.isMilkMan, isFalse);
// And it does not carry the milk run's hinge: START DELIVERY belongs to
// the line that collects a whole load before it drives.
expect(p.startsDeliveryAfterFullLoad, isFalse);
});
});
}