Files
doormile_milderapp/test/service_profile_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

123 lines
4.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
/// One app, two lines of work. The tests that matter here are about the
/// direction the resolver fails in: an unconfigured or unrecognised tenant must
/// land on the parcel line, because that is the larger business and the failure
/// it cannot afford is a rider losing the screen that collects a customer's
/// cash. Showing a meal rider a payment prompt he can dismiss is the cheap
/// mistake.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
/// Resolves through the free function, not the GetX controller — see the
/// note on [ServiceProfile.active] for why data code must not need a
/// container. [ServiceProfile.setActive] is what `TenantController.load`
/// would have done.
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('tenant resolution', () {
test(
'an unknown tenant falls back to parcel, not to the meal flow',
() async {
final p = await resolveWith({'tenantid': 4242});
expect(p.line, ServiceLine.parcel);
expect(p.collectsCash, isTrue);
expect(p.needsVerification, isTrue);
},
);
test('a login with no tenant at all still gets a working app', () async {
final p = await resolveWith({});
expect(p.line, ServiceLine.parcel);
});
test('the tenant name selects meals, case and spacing aside', () async {
final p = await resolveWith({
'tenantid': 0,
'tenantname': ' DailyGrubs ',
});
expect(p.line, ServiceLine.milkMan);
});
test(
'a meal rider is not asked for money, proof or a verification form',
() async {
final p = await resolveWith({'tenantname': 'meals'});
expect(p.collectsCash, isFalse);
expect(p.needsVerification, isFalse);
expect(p.needsProofPhoto, isFalse);
// He cannot decline one subscriber's lunch.
expect(p.acceptsPerStop, isFalse);
expect(p.sourceIsKitchen, isTrue);
},
);
test(
'an unrecognised name falls through to the id rather than deciding',
() async {
// A client the app has never been told about must not guess its way onto
// the meal flow.
final p = await resolveWith({
'tenantname': 'someclient',
'tenantid': 1,
});
expect(p.line, ServiceLine.parcel);
},
);
});
group('the money gate', () {
final stop = <String, dynamic>{'collectionamt': 450};
test(
'a parcel route still collects what the stop says it collects',
() async {
await resolveWith({'tenantid': 1});
expect(stopCollectionAmount(stop), 450);
},
);
test('a meal tenant collects nothing, whatever the payload says', () async {
await resolveWith({'tenantname': 'meals'});
// The one gate every "is anything owed?" branch in the app reads.
expect(stopCollectionAmount(stop), 0);
expect(stopCollectionAmount({'codamount': '₹1,200'}), 0);
});
test('the alternate spellings still work on a parcel route', () async {
await resolveWith({'tenantid': 1});
expect(stopCollectionAmount({'pickupamt': '₹1,200'}), 1200);
expect(stopCollectionAmount({'codamount': 75.5}), 75.5);
});
});
group('the tenant id sent at login', () {
test('an ordinary build declares none, and the field is omitted', () {
// `--dart-define=TENANT_ID` is unset here, as it is in every default
// build. `hasTenantId` is what gates the field onto the request body, so
// a plain APK asks the server to decide rather than asserting a tenant.
expect(MilerApi.tenantId, 0);
expect(MilerApi.hasTenantId, isFalse);
});
test('zero is never a tenant', () {
// Sending `tenantid: 0` would look like a real lookup key to the server.
// The only way to say "you decide" is to leave the field out, so the
// guard is > 0 rather than a null check.
expect(MilerApi.hasTenantId, MilerApi.tenantId > 0);
});
});
}