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>
197 lines
7.8 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|