178 lines
6.6 KiB
Dart
178 lines
6.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:miler/views/Dashboard/home/trip.dart';
|
|
import 'package:miler/views/Dashboard/home/trip_card.dart';
|
|
|
|
/// The call button and the tick box on a Home stop row must sit on the same two
|
|
/// columns for every stop in the route.
|
|
///
|
|
/// They did not. Both were *conditional* children of the meta row —
|
|
/// `if (phone.isNotEmpty)` and `if (pending && onToggleSelect != null)` — so
|
|
/// whenever one was missing the other slid across to take its place. A stop with
|
|
/// no phone number pushed its tick box a tap-target to the right; an
|
|
/// already-accepted stop pushed its call button the same way. Down a route of
|
|
/// mixed states the two controls zig-zagged, and a rider reaching for "the tick"
|
|
/// had to locate it again on every row.
|
|
///
|
|
/// This is the kind of thing that is obvious in a screenshot and invisible in
|
|
/// code review, so it is measured: the x-positions are compared across a trip
|
|
/// deliberately built to hit every combination.
|
|
void main() {
|
|
Map<String, dynamic> stop(
|
|
String id, {
|
|
required String type,
|
|
String? phone,
|
|
}) =>
|
|
{
|
|
'orderid': id,
|
|
'type': type,
|
|
'pickupcustomer': 'Sri Balaji Stores',
|
|
'pickupaddress': '19, Thadagam Road, Coimbatore',
|
|
if (phone != null) 'pickupcontactno': phone,
|
|
'Quantity': '2',
|
|
'expected_pickup_time': DateTime.now().toIso8601String(),
|
|
};
|
|
|
|
/// A route covering every trailing-slot combination.
|
|
///
|
|
/// The states are the three Home still lists — pending, rejected, skipped.
|
|
/// Accepted stops used to fill the "no tick box" cases here, but they leave
|
|
/// Home for Bookings now, so a decided-but-still-listed state does the job:
|
|
///
|
|
/// 1 pending + phone → both controls
|
|
/// 2 pending, no phone → tick only
|
|
/// 3 rejected + phone → call only (no tick: it is already decided)
|
|
/// 4 skipped, no phone → neither
|
|
Future<void> pumpRoute(WidgetTester tester) async {
|
|
tester.view.physicalSize = const Size(1170, 2532);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final trip = Trip.fromStops(id: 'mixed', stops: [
|
|
stop('A', type: 'pickup', phone: '9876543210'),
|
|
stop('B', type: 'delivery'),
|
|
{...stop('C', type: 'combined', phone: '9876500000')},
|
|
{...stop('D', type: 'pickup'), 'orderstatus': 'skipped'},
|
|
]);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: TripCard(
|
|
trip: trip,
|
|
rejectedIds: const {'C'},
|
|
onToggleSelect: (_) {},
|
|
onCallStop: (_) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('every call button shares one column', (tester) async {
|
|
await pumpRoute(tester);
|
|
|
|
final calls = tester
|
|
.widgetList<Widget>(find.byIcon(Icons.call_rounded))
|
|
.toList();
|
|
expect(calls.length, 2, reason: 'two of the four stops carry a phone');
|
|
|
|
final rects = find
|
|
.byIcon(Icons.call_rounded)
|
|
.evaluate()
|
|
.map((e) => (e.renderObject! as RenderBox).localToGlobal(Offset.zero).dx)
|
|
.toList();
|
|
|
|
expect(rects.first, closeTo(rects.last, 0.01),
|
|
reason: 'the call buttons are at x=$rects — they must share a column '
|
|
'even though one stop is pending and the other rejected');
|
|
});
|
|
|
|
testWidgets('the call column does not move when the tick box is absent',
|
|
(tester) async {
|
|
await pumpRoute(tester);
|
|
|
|
// Stop A is pending and has a phone, so it shows BOTH controls.
|
|
// Stop C is rejected and has a phone, so it shows the call button ALONE.
|
|
// Before the fix, C's call button sat one tap-target further right.
|
|
final callXs = find
|
|
.byIcon(Icons.call_rounded)
|
|
.evaluate()
|
|
.map((e) => (e.renderObject! as RenderBox).localToGlobal(Offset.zero).dx)
|
|
.toSet();
|
|
|
|
expect(callXs.length, 1,
|
|
reason: 'a missing tick box must not pull the call button across');
|
|
});
|
|
|
|
testWidgets('the card and the rail each get their side gap', (tester) async {
|
|
await pumpRoute(tester);
|
|
|
|
// The rail node is the numbered circle; the card is the fill beside it.
|
|
final railLeft = tester.getRect(find.text('1')).left;
|
|
expect(railLeft, greaterThan(12),
|
|
reason: 'the rail should not run into the page gutter');
|
|
|
|
// Nothing is clipped or overflowing at the other edge either.
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('the two controls are the same size', (tester) async {
|
|
await pumpRoute(tester);
|
|
|
|
final call = tester.getSize(find.byIcon(Icons.call_rounded).first);
|
|
expect(call.width, call.height, reason: 'the call button is square');
|
|
|
|
// Both controls render inside equal-width reserved slots.
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('every duration sits on one right-hand column', (tester) async {
|
|
await pumpRoute(tester);
|
|
|
|
// "~5 min" is materially narrower than "~10 min". Left-aligned, that put the
|
|
// duration in a different place on every card and dragged the call button
|
|
// and tick box along with it. Each now sits in a fixed-width slot,
|
|
// right-aligned, so all four share a trailing edge.
|
|
final rights = find
|
|
.textContaining('~')
|
|
.evaluate()
|
|
.map((e) {
|
|
final box = e.renderObject! as RenderBox;
|
|
return box.localToGlobal(Offset.zero).dx + box.size.width;
|
|
})
|
|
.toSet();
|
|
|
|
expect(rights.length, 1,
|
|
reason: 'durations end at $rights — they must share one column');
|
|
});
|
|
|
|
testWidgets('the three trailing elements keep a constant spacing',
|
|
(tester) async {
|
|
await pumpRoute(tester);
|
|
|
|
double leftOf(Finder f) =>
|
|
(f.evaluate().first.renderObject! as RenderBox)
|
|
.localToGlobal(Offset.zero)
|
|
.dx;
|
|
|
|
// Duration, then call, then tick — in that order, left to right, and the
|
|
// gaps between them are identical on every card because all three are
|
|
// fixed-width slots rather than content-sized children.
|
|
final duration = leftOf(find.textContaining('~'));
|
|
final call = leftOf(find.byIcon(Icons.call_rounded));
|
|
expect(duration, lessThan(call),
|
|
reason: 'the duration reads before the controls it belongs to');
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|