244 lines
8.9 KiB
Dart
244 lines
8.9 KiB
Dart
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/views/Dashboard/pickups/stop_verify.dart';
|
||
|
||
/// 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<XFile?> getImageFromSource({
|
||
required ImageSource source,
|
||
ImagePickerOptions options = const ImagePickerOptions(),
|
||
}) async => XFile(path);
|
||
}
|
||
|
||
void main() {
|
||
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 <int>[
|
||
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<void> pump(
|
||
WidgetTester tester,
|
||
Map<String, dynamic> 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<void> 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<void> 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<String, dynamic> stopOf({
|
||
required String type,
|
||
int deliveryQty = 0,
|
||
int pickupQty = 0,
|
||
double cash = 0,
|
||
}) => <String, dynamic>{
|
||
'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);
|
||
});
|
||
}
|
||
}
|