first commit
This commit is contained in:
177
test/stop_row_alignment_test.dart
Normal file
177
test/stop_row_alignment_test.dart
Normal file
@@ -0,0 +1,177 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user