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>
166 lines
6.1 KiB
Dart
166 lines
6.1 KiB
Dart
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",
|
||
);
|
||
});
|
||
});
|
||
}
|