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