Files
doormile_milderapp/test/stop_row_alignment_test.dart
2026-08-11 13:16:33 +05:30

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);
});
}