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>
This commit is contained in:
140
test/provider_dedupe_test.dart
Normal file
140
test/provider_dedupe_test.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user