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:
165
test/delivery_proof_test.dart
Normal file
165
test/delivery_proof_test.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/data/proof_store.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// PROOF OF DELIVERY
|
||||
///
|
||||
/// A photo taken at a door is evidence, and evidence that disappears is worse
|
||||
/// than none — it makes the rider believe he has cover he does not have.
|
||||
/// These pin the two properties that make it real: it outlives the OS cache
|
||||
/// `ImagePicker` hands back, and it belongs to the session that took it.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
late Directory docs;
|
||||
|
||||
setUp(() async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
SharedPreferences.setMockInitialValues({'userid': 38});
|
||||
|
||||
// path_provider has no host under the test binding.
|
||||
docs = await Directory.systemTemp.createTemp('proof_docs');
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||
(call) async => docs.path,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
if (await docs.exists()) await docs.delete(recursive: true);
|
||||
});
|
||||
|
||||
/// A real 1×1 PNG. The store tests never decode it, but the widget test
|
||||
/// does: `Image.file` on 64 bytes of nonsense never completes its frame, so
|
||||
/// `pumpAndSettle` waits for a decode that will never land and the test
|
||||
/// hangs for the full ten minutes rather than failing.
|
||||
const List<int> onePixelPng = [
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, //
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
|
||||
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
|
||||
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
|
||||
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
|
||||
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
|
||||
0x42, 0x60, 0x82,
|
||||
];
|
||||
|
||||
/// Stands in for what the camera hands back: a file in a volatile place.
|
||||
Future<File> cameraShot(String name) async {
|
||||
final cache = await Directory.systemTemp.createTemp('picker_cache');
|
||||
final f = File('${cache.path}/$name.jpg');
|
||||
await f.writeAsBytes(onePixelPng);
|
||||
return f;
|
||||
}
|
||||
|
||||
group('the store', () {
|
||||
test('the photo is copied out of the camera cache', () async {
|
||||
final shot = await cameraShot('a');
|
||||
final saved = await ProofStore.save('ORDER-1', shot.path);
|
||||
|
||||
expect(saved, isNotNull);
|
||||
expect(
|
||||
saved,
|
||||
isNot(shot.path),
|
||||
reason:
|
||||
'ImagePicker returns an OS cache path the system reclaims — '
|
||||
'a record pointing at it loses its evidence',
|
||||
);
|
||||
expect(File(saved!).existsSync(), isTrue);
|
||||
|
||||
// The original can vanish and the proof survives, which is the point.
|
||||
await shot.parent.delete(recursive: true);
|
||||
expect(File(saved).existsSync(), isTrue);
|
||||
expect(await ProofStore.pathFor('ORDER-1'), saved);
|
||||
});
|
||||
|
||||
test('a retake replaces rather than accumulates', () async {
|
||||
final first = await ProofStore.save(
|
||||
'ORDER-1',
|
||||
(await cameraShot('a')).path,
|
||||
);
|
||||
final second = await ProofStore.save(
|
||||
'ORDER-1',
|
||||
(await cameraShot('b')).path,
|
||||
);
|
||||
expect(second, first, reason: 'one stop, one proof');
|
||||
expect(await ProofStore.pathFor('ORDER-1'), second);
|
||||
});
|
||||
|
||||
test('a proof whose file has gone is not proof', () async {
|
||||
final saved = await ProofStore.save(
|
||||
'ORDER-1',
|
||||
(await cameraShot('a')).path,
|
||||
);
|
||||
await File(saved!).delete();
|
||||
expect(
|
||||
await ProofStore.pathFor('ORDER-1'),
|
||||
isNull,
|
||||
reason: 'returning a dead path would draw a broken image as evidence',
|
||||
);
|
||||
});
|
||||
|
||||
test('nothing is invented from nothing', () async {
|
||||
expect(await ProofStore.save('', 'whatever'), isNull);
|
||||
expect(await ProofStore.save('ORDER-1', ''), isNull);
|
||||
expect(await ProofStore.save('ORDER-1', '/no/such/file.jpg'), isNull);
|
||||
expect(await ProofStore.pathFor('ORDER-NONE'), isNull);
|
||||
});
|
||||
|
||||
test('a device path is never offered as a remote url', () async {
|
||||
await ProofStore.save('ORDER-1', (await cameraShot('a')).path);
|
||||
expect(
|
||||
await ProofStore.remoteUrlFor('ORDER-1'),
|
||||
'',
|
||||
reason:
|
||||
'the Miler API has no upload route; a filesystem path written '
|
||||
"into the hub's photourl would look like evidence and resolve to "
|
||||
'nothing',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ownership', () {
|
||||
test("another rider cannot open this rider's doorstep photos", () async {
|
||||
await ProofStore.save('ORDER-1', (await cameraShot('a')).path);
|
||||
expect(await ProofStore.pathFor('ORDER-1'), isNotNull);
|
||||
|
||||
SharedPreferences.setMockInitialValues({'userid': 99});
|
||||
expect(
|
||||
await ProofStore.pathFor('ORDER-1'),
|
||||
isNull,
|
||||
reason: 'proof is scoped like every other record — see WorkScope',
|
||||
);
|
||||
});
|
||||
|
||||
test('the other line cannot either', () async {
|
||||
await ProofStore.save('ORDER-1', (await cameraShot('a')).path);
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
expect(await ProofStore.pathFor('ORDER-1'), isNull);
|
||||
});
|
||||
|
||||
test('logout deletes the photos, not just the index', () async {
|
||||
final saved = await ProofStore.save(
|
||||
'ORDER-1',
|
||||
(await cameraShot('a')).path,
|
||||
);
|
||||
await ProofStore.clearScope();
|
||||
|
||||
expect(await ProofStore.pathFor('ORDER-1'), isNull);
|
||||
expect(
|
||||
File(saved!).existsSync(),
|
||||
isFalse,
|
||||
reason: "a doorstep photo must not outlive the session that took it",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user