import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/pickups/stop_verify.dart'; /// ── The line this page's own review still belongs to ── /// /// [StopVerificationPage] ends with a review only where nothing downstream /// subsumes it. On **logistics** something does: the shipment desk opens /// straight after, and its Review & Pay screen covers the parcel, the /// addresses, the fare and the payment on one page — so a review here would be /// the same facts twice, two screens apart. /// /// The review is therefore kept for a line that verifies without pricing, and /// this is that line. Built here rather than shipped as a profile because no /// signed client needs it yet; the day one does, this is the shape it takes. /// See `ServiceProfile.capturesShipmentAddresses`. const ServiceProfile _verifyOnlyLine = ServiceProfile( line: ServiceLine.parcel, label: 'Doormile', jobNoun: 'booking', jobNounPlural: 'bookings', completionVerb: 'Picked up', collectsCash: true, needsVerification: true, needsProofPhoto: true, acceptsPerStop: true, sourceIsKitchen: false, handoffAt: HandoffPoint.accepted, capturesShipmentAddresses: false, ); /// The review step used to be untestable — "it sits behind a camera gate no /// widget test can pass" — so the only thing held about it was the pure packing /// rule in [packReviewFacts]. That left the screen itself unwatched, and it /// broke: the fact rows were `Row(crossAxisAlignment: stretch)` inside a /// `SingleChildScrollView`, which asks the row to stretch to an infinite height. /// Layout threw, the exception propagated up through the leg panel's Column, and /// the last screen of the flow — the one the rider signs — showed no figures at /// all. /// /// The gate is passable: [ImagePickerPlatform.instance] is the seam. Swapping in /// a fake that returns a real file on disk lets the whole flow run to the review /// and assert what the rider is actually shown. class _FakeCamera extends ImagePickerPlatform { final String path; _FakeCamera(this.path); @override Future getImageFromSource({ required ImageSource source, ImagePickerOptions options = const ImagePickerOptions(), }) async => XFile(path); } void main() { setUp(() => ServiceProfile.setActive(_verifyOnlyLine)); tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); setUpAll(() async { final dir = await Directory.systemTemp.createTemp('miler_review'); final png = File('${dir.path}/proof.png') // A 1×1 PNG: Image.file needs bytes it can decode, not a real photo. ..writeAsBytesSync(const [ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, // 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 250, 207, 192, 240, 31, 0, 5, 5, 2, 0, 141, 88, 175, 91, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, ]); ImagePickerPlatform.instance = _FakeCamera(png.path); }); Future pump( WidgetTester tester, Map stop, { double width = 390, double textScale = 1.0, }) async { tester.view.physicalSize = Size(width * 3, 844 * 3); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( builder: (context, child) => MediaQuery( data: MediaQuery.of( context, ).copyWith(textScaler: TextScaler.linear(textScale)), child: child!, ), home: StopVerificationPage(pickup: stop), ), ), ); await tester.pumpAndSettle(); } /// Taps by visible label, scrolling it into view first — every one of these /// controls sits below the fold on a phone. Future tap(WidgetTester tester, String label) async { final target = find.text(label).first; await tester.ensureVisible(target); await tester.pumpAndSettle(); await tester.tap(target); await tester.pumpAndSettle(); } Future type(WidgetTester tester, String hint, String value) async { await tester.enterText( find.byWidgetPredicate( (w) => w is TextField && w.decoration?.hintText == hint, ), value, ); await tester.pumpAndSettle(); } Map stopOf({ required String type, int deliveryQty = 0, int pickupQty = 0, double cash = 0, }) => { 'orderid': 'T-1', 'type': type, 'pickupcustomer': 'Priya Sharma', 'pickupaddress': '45, Lake View Apartments, Coimbatore', if (deliveryQty > 0) 'deliveryqty': '$deliveryQty', if (pickupQty > 0) 'pickupqty': '$pickupQty', 'Quantity': '${pickupQty > 0 ? pickupQty : deliveryQty}', if (cash > 0) 'collectionamt': '$cash', }; testWidgets('a pickup stop shows every collected figure on the review', ( tester, ) async { await pump(tester, stopOf(type: 'pickup', pickupQty: 3, cash: 2450)); await type(tester, 'Total weight in kg', '4.5'); await tap(tester, 'Take photo'); // The pickup code is six boxes over one hidden field, not a hinted one. await tester.enterText(find.byType(TextField).last, '482156'); await tester.pumpAndSettle(); await tap(tester, 'Review'); // The heading is the screen's own voice, not another section title. expect(find.text('Check before you confirm'), findsOneWidget); // Every figure the rider is signing for. Before the layout fix this whole // block rendered as nothing. expect(find.text('COLLECTED'), findsOneWidget); expect(find.text('3 of 3'), findsOneWidget); expect(find.text('4.5 kg'), findsOneWidget); expect(find.text('Sealed & intact'), findsOneWidget); expect(find.text('482156'), findsOneWidget); // The photo is shown, not asserted as a tick the rider cannot check. expect(find.byType(Image), findsWidgets); expect(find.text('Tap to enlarge'), findsOneWidget); // Money is flagged before the commit, not after it. expect( find.text('Collect ₹2450 from the customer on the next screen'), findsOneWidget, ); expect(find.text('Confirm pickup'), findsOneWidget); }); testWidgets('a combined stop reviews both legs separately', (tester) async { await pump(tester, stopOf(type: 'combined', deliveryQty: 2, pickupQty: 3)); // Hand over. await tap(tester, 'Parcel 1 of 2'); await tap(tester, 'Parcel 2 of 2'); await tap(tester, 'Security'); await type(tester, "Person's name", 'Ramesh Kumar'); await tester.enterText(find.byType(TextField).last, '482156'); await tester.pumpAndSettle(); await tap(tester, 'Take photo'); await tap(tester, 'Continue to collect'); // Collect, with a damaged seal so the note takes its own wide row. await tap(tester, 'Damaged'); await type(tester, 'Total weight in kg', '12.75'); await type( tester, 'e.g. corner of the box is crushed', 'Corner of the second box is crushed', ); await tap(tester, 'Take photo'); await tap(tester, 'Review'); // Two panels, each with its own facts and its own photo — the pairing is // the whole reason both shots are taken. expect(find.text('HANDED OVER'), findsOneWidget); expect(find.text('2 of 2'), findsOneWidget); expect(find.text('Ramesh Kumar · Security'), findsOneWidget); expect(find.text('COLLECTED'), findsOneWidget); expect(find.text('3 of 3'), findsOneWidget); expect(find.text('12.75 kg'), findsOneWidget); expect(find.text('Damaged'), findsOneWidget); expect(find.text('Corner of the second box is crushed'), findsOneWidget); expect(find.text('Tap to enlarge'), findsNWidgets(2)); // Both legs are complete, and each says so on itself — on a combined stop // the page-level verdict does not say which job is short. expect(find.text('Complete'), findsNWidgets(2)); // No pickup code on a combined stop: the delivery OTP already proved the // door, so it is never collected and must not be reviewed. expect(find.text('Pickup code'), findsNothing); }); testWidgets('tapping a proof photo opens it full frame', (tester) async { await pump(tester, stopOf(type: 'pickup', pickupQty: 1)); await type(tester, 'Total weight in kg', '4.5'); await tap(tester, 'Take photo'); await tester.enterText(find.byType(TextField).last, '482156'); await tester.pumpAndSettle(); await tap(tester, 'Review'); await tap(tester, 'Tap to enlarge'); expect(find.byType(InteractiveViewer), findsOneWidget); }); // The review is a grid of fixed-width halves, which is the shape that // overflows when the system font grows. Measured rather than eyeballed. for (final scale in const [1.0, 1.3, 1.5, 2.0]) { testWidgets('the review lays out at ${scale}x on a 360pt phone', ( tester, ) async { await pump( tester, stopOf(type: 'combined', deliveryQty: 2, pickupQty: 3, cash: 2450), width: 360, textScale: scale, ); await tap(tester, 'Parcel 1 of 2'); await tap(tester, 'Parcel 2 of 2'); await tap(tester, 'Security'); await type(tester, "Person's name", 'Ramesh Kumar Subramaniam'); await tester.enterText(find.byType(TextField).last, '482156'); await tester.pumpAndSettle(); await tap(tester, 'Take photo'); await tap(tester, 'Continue to collect'); await tap(tester, 'Damaged'); await type(tester, 'Total weight in kg', '12.75'); await type( tester, 'e.g. corner of the box is crushed', 'Corner of the second box is crushed on the underside', ); await tap(tester, 'Take photo'); await tap(tester, 'Review'); expect(find.text('HANDED OVER'), findsOneWidget); expect(find.text('COLLECTED'), findsOneWidget); expect(tester.takeException(), isNull); }); } }