Files
doormile_milderapp/test/multidrop_shot_test.dart
Thiru-tenext d612916fe4 Session expiry, arrival geofence guard, multi-destination stops
Three fixes found by running the app on a real handset against production.

1. An expired token left the app looking signed in and unable to work.
   MilerApi.onUnauthorized was declared and called on every 401 but never
   assigned, so the token was dropped and nothing else happened: the profile
   stayed on disk, logged_out stayed false, and the rider saw his own name over
   a dashboard whose every call returned 401. He reads that as "no work today".
   The teardown now lives in endSession() and both ways out of a session — the
   Log out button and the 401 path — use it.

2. Arrived was written locally even when the rider was not there.
   updateArrivedStatus answers false for three different things and the caller
   treated all of them as "the write did not land", which is only true of one.
   A geofence refusal and a server refusal now stop the rung and hand back the
   reason; a dead network still advances, as it should.

3. A multi-destination customer pickup collapsed onto one stop.
   GET /miler/bookings returns a row per destination once collected, all with
   the same bookingid and reference. Every local store keys on that id, so the
   accepted store deduped two of three drops away and their consignment ids
   were unrecoverable. orderid is now the stop key; bookingreference stays the
   booking's name. Cards show "Stop 2 of 3" and the receiver's own name and
   number rather than the sender's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
2026-09-18 11:05:40 +05:30

171 lines
5.0 KiB
Dart

@Tags(['shots'])
library;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'support/golden_fonts.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/mock_backend.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/Dashboard/pickups/pickups.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
/// A look at the three-door card, rendered from the SAME fixture the running
/// app serves — `MockBackend.multiDropVisit` — through the SAME adapter the
/// network path uses. Nothing here hand-builds a stop map, so what is on screen
/// is what a rider sees when the backend sends a fanned-out customer pickup.
///
/// Writes PNGs to `test/shots_out/` rather than comparing goldens: the point is
/// to look at it, not to pin it.
void main() {
setUpAll(() async {
await loadGoldenFonts();
Directory('test/shots_out').createSync(recursive: true);
});
tearDown(() {
Get.reset();
ServiceProfile.setActive(ServiceProfile.parcel);
});
Future<void> shoot(
WidgetTester tester,
String name,
Widget child, {
double height = 340,
double width = 390,
}) async {
SharedPreferences.setMockInitialValues({'userid': 1});
tester.view.physicalSize = Size(width * 3, height * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
ScreenUtilInit(
designSize: Size(width, height),
builder: (_, _) => MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: ColorConstants.daylightSurface,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: child,
),
),
),
),
),
);
await tester.pumpAndSettle();
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile('shots_out/$name.png'),
);
}
/// The fixture the app serves, put through the real adapter.
List<Map<String, dynamic>> stops() => [
for (final row in MockBackend.multiDropVisit)
ApiConfig.pickupFromBooking(Map<String, dynamic>.from(row)),
];
testWidgets('door 1 of 3 — Chennai, two parcels', (tester) async {
ServiceProfile.setActive(ServiceProfile.milkMan);
final all = stops();
await shoot(
tester,
'multidrop_stop1',
PickupCard(
item: all[0],
displayStep: 1,
distanceMeters: 1400,
collectedIds: {all[0]['orderid'] as String},
outForDeliveryIds: {all[0]['orderid'] as String},
),
);
});
testWidgets('door 2 of 3 — Ernakulam', (tester) async {
ServiceProfile.setActive(ServiceProfile.milkMan);
final all = stops();
await shoot(
tester,
'multidrop_stop2',
PickupCard(
item: all[1],
displayStep: 2,
distanceMeters: 3100,
collectedIds: {all[1]['orderid'] as String},
outForDeliveryIds: {all[1]['orderid'] as String},
),
);
});
testWidgets('the three stacked, as the queue shows them', (tester) async {
ServiceProfile.setActive(ServiceProfile.milkMan);
final all = stops();
await shoot(
tester,
'multidrop_queue',
Column(
children: [
for (var i = 0; i < all.length; i++)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: PickupCard(
item: all[i],
displayStep: i + 1,
distanceMeters: 1400.0 + i * 900,
collectedIds: {all[i]['orderid'] as String},
outForDeliveryIds: {all[i]['orderid'] as String},
),
),
],
),
height: 960,
);
});
testWidgets('a single-destination booking is unchanged', (tester) async {
ServiceProfile.setActive(ServiceProfile.milkMan);
final one = ApiConfig.pickupFromBooking({
'bookingid': 900777,
'bookingreference': 'MOCK-000777',
'status': 'Converted_To_Consignment',
'consignmentid': 900777,
'consignmentstatus': 'Out_for_Delivery',
'next_action': 'deliver',
'destinationseq': 0,
'destinationcount': 1,
'pickup_source_name': 'Ravi Kumar',
'pickupaddress': '14, Cross Cut Road, Gandhipuram, Coimbatore 641012',
'deliveryaddress': '12, SNS Colony, Peelamedu, Coimbatore 641004',
'customername': 'Ravi Kumar',
'recipientname': 'Joe Mathew',
'parcels': [
{'bookingparcelid': 1, 'itemcategory': 'General'},
],
});
await shoot(
tester,
'multidrop_single',
PickupCard(
item: one,
displayStep: 1,
distanceMeters: 900,
collectedIds: {one['orderid'] as String},
outForDeliveryIds: {one['orderid'] as String},
),
);
});
}