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

141 lines
5.2 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/mock_backend.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/data/work_repository.dart';
import 'package:miler/providers/pickup/pickup_provider.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// FOUR SCREENS, ONE REQUEST
///
/// Home, Bookings, Activity and the post-delivery screen all read the day, and
/// each produced its own `GET /miler/bookings` — on mount, on tab switch, and
/// on every poll. Four timers, four copies, four ideas of what had been
/// accepted, reconciled only by whichever refreshed last.
///
/// The provider still returns a list or throws, which is what every call site
/// was written against. What changed is that they now share one request.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late int requests;
final provider = PickupProvider();
void stub({List<Map<String, dynamic>> rows = const [], int code = 200}) {
requests = 0;
MilerApi.client = MockClient((req) async {
// The repository also reads `/miler/assignments` for the hub's stop
// sequence; it is not the day's request and is not counted. See the same
// note in `work_repository_test.dart`.
if (req.url.path.endsWith('/miler/assignments')) {
return http.Response(
json.encode({'success': true, 'data': const []}),
200,
headers: const {'content-type': 'application/json'},
);
}
requests++;
return http.Response(
json.encode({'success': code < 400, 'data': rows}),
code,
headers: const {'content-type': 'application/json'},
);
});
}
Map<String, dynamic> booking(String id) => {
'bookingid': id,
'orderid': id,
'pickupcustomer': 'Stop $id',
'pickupaddress': '12 Main Rd',
'status': 'Miler_Assigned',
};
setUp(() async {
MockBackend.enabled = false;
ServiceProfile.setActive(ServiceProfile.parcel);
SharedPreferences.setMockInitialValues({'authtoken': 't'});
await ApiConfig.setToken('t');
WorkRepository.instance.resetForTest();
stub(rows: [booking('A')]);
});
tearDown(() {
MilerApi.client = http.Client();
MockBackend.enabled = kMockBackend;
ServiceProfile.setActive(ServiceProfile.parcel);
WorkRepository.instance.resetForTest();
});
test('the three screen entry points share one request', () async {
final results = await Future.wait([
provider.getPickupQueues(),
provider.getCurrentPickups(),
provider.getPickupQueuesPicked(),
]);
expect(requests, 1, reason: 'three screens, one GET /miler/bookings');
for (final r in results) {
expect(r, hasLength(1));
}
});
test('a tab switch moments later does not re-fetch', () async {
await provider.getCurrentPickups();
await provider.getPickupQueuesPicked();
expect(requests, 1);
});
test(
'every caller gets the same objects, so no screen has its own copy',
() async {
final a = await provider.getCurrentPickups();
final b = await provider.getPickupQueuesPicked();
expect(identical(a, b), isTrue);
},
);
test('an empty day is an empty list, not an error', () async {
stub(rows: const []);
expect(await provider.getCurrentPickups(), isEmpty);
});
// ── The milk-man line is dispatched, not written off ──
//
// This expected a [LineNotServedException] after zero requests, on the belief
// that the backend had no endpoint for that line. It has the same one: the
// console assigns the round on /admin/bookings and the rider reads it back
// from /miler/bookings. While the two disagreed, a rider on tenant 13 with a
// booking assigned to him got an empty screen and no error to explain it.
test('a milk-man rider is asked for, not refused', () async {
stub(rows: [booking('MILK-1')]);
ServiceProfile.setActive(ServiceProfile.milkMan);
WorkRepository.instance.resetForTest();
final day = await provider.getCurrentPickups();
expect(day, hasLength(1));
expect(requests, 1, reason: 'his work has to be asked for to arrive');
});
test('a server failure throws rather than reading as an empty day', () async {
stub(code: 500);
await expectLater(provider.getCurrentPickups(), throwsA(isA<Exception>()));
});
// A filtered read is a different question and must not be served the shared
// copy of the whole day.
test('a status-filtered read gets its own request', () async {
await provider.getCurrentPickups();
await provider.getPickupQueues(orderstatus: 'Picked_Up');
expect(requests, 2);
});
}