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/route_brief.dart'; import 'package:miler/views/Dashboard/home/trip.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/Dashboard/home/trip_brief_strip.dart'; import 'package:miler/views/Dashboard/home/trip_card.dart'; import 'package:miler/views/Dashboard/home/trip_tabs.dart'; import 'package:miler/views/helpers/widgets/app_widgets.dart'; /// Density is the whole point of these two redesigns, so it is asserted rather /// than claimed. A phone screen is roughly 844pt tall; how much of it the /// permanent furniture eats decides how many stops the rider can see. void main() { Future pump(WidgetTester tester, Widget child) async { tester.view.physicalSize = const Size(1170, 2532); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold(body: SingleChildScrollView(child: child)), ), ), ); await tester.pumpAndSettle(); } Map stop(String id, {double cash = 0}) => { 'orderid': id, 'type': 'pickup', 'pickupcustomer': 'Sri Balaji Stores', 'pickupaddress': '19, Thadagam Road, Venkitapuram, Coimbatore', 'pickupqty': '3', 'Quantity': '3', if (cash > 0) 'collectionamt': '$cash', 'expected_pickup_time': DateTime.now().toIso8601String(), }; // ── The trip brief is collapsed by default, and that is the whole point ── // // The four surfaces this replaced — a shift strip, a "CURRENT SHIFT" card, a // 66dp ring and three stat cards — measured ~351dp at this design size, on a // scrollable region of roughly 650. Over half the screen was spent before the // rider saw a stop. These tests hold the new budget, because the failure mode // is exactly the one that produced the old header: a line at a time, each // individually defensible. group('Trip brief strip', () { Future briefHeight(WidgetTester tester, {double cash = 0}) async { await pump( tester, TripCard(trip: Trip.fromStops(id: 't1', stops: [stop('A', cash: cash)])), ); return tester.getSize(find.byType(TripBriefStrip)).height; } testWidgets('stays compact when collapsed', (tester) async { final h = await briefHeight(tester); expect( h, lessThan(90), reason: 'measured $h — the brief is the permanent furniture above the ' 'route, so every point of it is charged at every scroll position', ); }); testWidgets('cash does not make it taller', (tester) async { // The COD figure rides as a chip on the same row, not a fourth card and // not a second line. final without = await briefHeight(tester); final with_ = await briefHeight(tester, cash: 1250); expect( with_, without, reason: 'adding a COD figure must not grow the collapsed strip', ); }); testWidgets('holds the journey figures back until it is opened', ( tester, ) async { await pump( tester, TripCard(trip: Trip.fromStops(id: 't1', stops: [stop('A', cash: 940)])), ); // The journey tiles are small-caps. ('Trip slot' was in this list until // the row was removed — see the shift/slot test below.) const briefLabels = [ 'DURATION', 'DISTANCE', 'PARCELS', 'PAYMENT', ]; // Collapsed: progress and time, nothing else. for (final label in briefLabels) { expect( find.text(label), findsNothing, reason: '"$label" is briefing material — it must not be on the face', ); } expect( find.text('CURRENT SHIFT'), findsNothing, reason: 'the mislabelled shift caption is gone with the card', ); final collapsed = tester.getSize(find.byType(TripBriefStrip)).height; await tester.tap(find.byType(TripBriefStrip)); await tester.pumpAndSettle(); for (final label in briefLabels) { expect(find.text(label), findsOneWidget, reason: '"$label" on open'); } expect( tester.getSize(find.byType(TripBriefStrip)).height, greaterThan(collapsed), reason: 'opening the brief must actually reveal something', ); expect(tester.takeException(), isNull); }); testWidgets('lays out inside a scroll view, open or shut', (tester) async { // The row this replaced used `CrossAxisAlignment.stretch`, which needs an // `IntrinsicHeight` above it or the cards are asked to be infinitely tall // in an unbounded scroll extent — see `card_stripe_layout_test.dart`. It // cost a build once. The replacement has no stretch row at all, and this // holds that it stays that way. await pump( tester, TripCard(trip: Trip.fromStops(id: 't1', stops: [stop('A', cash: 1310)])), ); expect(tester.takeException(), isNull); await tester.tap(find.byType(TripBriefStrip)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); }); group('Trip tabs', () { testWidgets('fit in one compact row', (tester) async { await pump( tester, TripTabs( trips: [ Trip.fromStops(id: 't1', stops: [stop('A')]), Trip.fromStops(id: 't2', stops: [stop('B')]), ], selectedIndex: 0, onSelect: (_) {}, ), ); final h = tester.getSize(find.byType(TripTabs)).height; // ── This bound was raised deliberately, and it cost something ── // // The strip went from one line (~41pt) to a segmented control carrying a // label, a status line and a running-indicator (~70pt). That is ~30pt of // *permanent* height, and it is worse than it sounds because the bar is // now a pinned sliver: it is on screen at every scroll position, not just // at the top. // // What it buys is the separation of "the trip I am looking at" from "the // trip that is running", which the one-line version could not express at // all, plus per-trip progress in words rather than an ambiguous bar. That // is a real trade, taken on purpose — but the guard stays, because the // failure mode here is a strip that keeps growing a line at a time until // it crowds out the route it is supposed to be navigating. expect( h, lessThan(80), reason: 'the tab strip is pinned furniture — it is on screen at ' 'every scroll position, so its height is charged continuously', ); expect(tester.takeException(), isNull); }); testWidgets('still show three slots before the hub assigns them', ( tester, ) async { await pump( tester, TripTabs( trips: [ Trip.fromStops(id: 't1', stops: [stop('A')]), ], selectedIndex: 0, onSelect: (_) {}, ), ); expect(find.text('Trip 1'), findsOneWidget); expect(find.text('Trip 2'), findsOneWidget); expect(find.text('Trip 3'), findsOneWidget); }); }); // ── A stop row carries no controls at all ── // // Every row used to have its own Accept and Reject, and for a while an // accepted row kept a live Reject beside its own green confirmation — so a // stop could be accepted and then rejected a second later, with the two wire // calls racing. Both buttons are gone from the row entirely now: ticking // chooses, and the floating bar is the only thing that commits. group('decision row', () { Future pumpStop( WidgetTester tester, { Set accepted = const {}, Set rejected = const {}, }) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), acceptedIds: accepted, rejectedIds: rejected, onToggleSelect: (_) {}, onAcceptSelected: (_) async {}, ), ); } testWidgets('an undecided stop offers a tick, not buttons', (tester) async { await pumpStop(tester); for (final label in ['Reject', 'Accept', 'Accept instead']) { expect( find.widgetWithText(MilerButton, label), findsNothing, reason: '"$label" is not a per-stop control any more', ); } // The tick is what is left, and it is how a stop is chosen. expect(find.bySemanticsLabel('Select stop 1'), findsOneWidget); }); testWidgets('an accepted stop leaves the screen entirely', (tester) async { await pumpStop(tester, accepted: {'A'}); // Home lists what still needs deciding here. Once a stop is taken it is // work in hand, and work in hand lives on Bookings — the row used to stay // behind saying "Accepted", which is a row the rider can do nothing with. expect(find.text('Sri Balaji Stores'), findsNothing); expect(find.bySemanticsLabel('Select stop 1'), findsNothing); // The trip says what happened to it instead of leaving a gap. expect( find.text('Every stop on this trip is taken — work them from Bookings'), findsOneWidget, ); }); testWidgets('a skipped stop leaves the screen, like a finished one', ( tester, ) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [ {...stop('A'), 'orderstatus': 'skipped'}, ]), onToggleSelect: (_) {}, onAcceptSelected: (_) async {}, ), ); // A skip is a record of something that happened at a door, not an offer // waiting on a decision — so it is Activity's row now, with the Resume // control on it. Home is only ever "what am I taking?". expect(find.text('Sri Balaji Stores'), findsNothing); expect(find.widgetWithText(MilerButton, 'Resume'), findsNothing); expect( find.text('Every stop on this trip is taken — work them from Bookings'), findsOneWidget, ); }); testWidgets('a rejected stop can be put back to undecided', (tester) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), rejectedIds: const {'A'}, onToggleSelect: (_) {}, onAcceptSelected: (_) async {}, onUnrejectStop: (_) async {}, ), ); expect(find.widgetWithText(MilerButton, 'Undo reject'), findsOneWidget); // Undoing returns it to the queue; it does not accept it outright. expect(find.widgetWithText(MilerButton, 'Accept'), findsNothing); }); testWidgets('a rejected stop is stated by its chip, not a button', ( tester, ) async { await pumpStop(tester, rejected: {'A'}); expect(find.text('Rejected'), findsOneWidget); expect( find.widgetWithText(MilerButton, 'Rejected'), findsNothing, reason: 'a button that cannot be pressed is not a status', ); }); }); // ── One accept in the whole screen ── // // The footer used to carry "Accept all N stops", a second bulk action over a // different set of stops from the floating bar's — and the difference between // the two sets was exactly the stops the rider had just decided against. group('bulk action', () { List> threeStops() => [ stop('A'), stop('B'), stop('C'), ]; testWidgets('the footer has no accept button of its own', (tester) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: threeStops()), onAcceptSelected: (_) async {}, onToggleSelect: (_) {}, ), ); expect(find.textContaining('Accept all'), findsNothing); expect(find.textContaining('Accept remaining'), findsNothing); expect(find.textContaining('Continue trip'), findsNothing); // It points at the one accept there is instead. expect( find.text('Tick the stops you want, then accept them below'), findsOneWidget, ); }); testWidgets('and says where the action is once stops are ticked', ( tester, ) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: threeStops()), selectedIds: const {'A', 'B'}, onAcceptSelected: (_) async {}, onToggleSelect: (_) {}, ), ); expect( find.text('Accept the stops you picked from the bar below'), findsOneWidget, ); }); }); group('Trip brief contents', () { testWidgets('the shift is labelled, and the trip slot is not repeated', ( tester, ) async { // Two bugs, closed one after the other. // // First: the old card captioned `trip.slotLabel` — the TRIP's window — as // "CURRENT SHIFT", while the rider's actual shift sat in a separate strip // directly above it. Two ranges, stacked, one wearing the other's name. // The fix was a labelled row each. // // Then the slot row turned out to be printing a range the rider could // already see: the collapsed face falls back to `slotLabel`, and the trip // TAB above the strip is labelled with it too. So the slot keeps its // places and the brief keeps only the window that is genuinely its own. final trip = Trip.fromStops(id: 't1', stops: [stop('A')]); final brief = RouteBrief.from( orderIds: const [], ordersMap: const {}, shiftStart: '09:00:00', shiftEnd: '18:00:00', ); await pump(tester, TripCard(trip: trip, brief: brief)); await tester.tap(find.byType(TripBriefStrip)); await tester.pumpAndSettle(); expect(find.text('Shift'), findsOneWidget); expect( find.text('Trip slot'), findsNothing, reason: 'the slot is on the face and on the tab; a third copy is noise', ); expect( find.text(brief.shiftWindowLabel), findsOneWidget, reason: "the shift row states the rider's window, not the trip's", ); }); testWidgets('the brief holds no actions, only figures', (tester) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), onViewRoute: () {}, ), ); await tester.tap(find.byType(TripBriefStrip)); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TripBriefStrip), matching: find.text('MAP'), ), findsNothing, reason: 'the map is an action; it belongs on the route, not in a list of ' 'read-only figures', ); }); }); // ── The map opens from where the route begins ── // // It used to be buried at the foot of the trip brief: an action among nine // read-only figures, two taps deep behind a strip the rider has no other // reason to open. It now sits on the START · HUB row, which is both where the // route starts and where his eye already is when he starts it. group('MAP on the hub row', () { testWidgets('is reachable without opening anything', (tester) async { var opened = false; await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), onViewRoute: () => opened = true, ), ); expect(find.text('MAP'), findsOneWidget); expect( tester.getRect(find.text('MAP')).top, lessThan(tester.getRect(find.text('Sri Balaji Stores')).top), reason: 'it sits on the hub row, above the first stop', ); await tester.tap(find.text('MAP')); await tester.pumpAndSettle(); expect(opened, isTrue); }); // ── It has to fit in the row it sits on ── // // Aligning the hub node with its label once boxed the whole row to the // node's 22dp height, which cropped this control — a tap target with its // own padding and a chevron — through the middle. The band is sized for the // tallest thing on the row now, and this is what says so. testWidgets('is drawn whole, not cropped by the row it sits in', ( tester, ) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), onViewRoute: () {}, ), ); final label = tester.getRect(find.text('MAP')); final chevron = tester.getRect(find.byIcon(Icons.chevron_right_rounded)); final icon = tester.getRect(find.byIcon(Icons.map_rounded)); // Every part of the control has real height and shares a centre line // with the rest of it. for (final part in [label, chevron, icon]) { expect(part.height, greaterThan(0)); } expect(chevron.center.dy, closeTo(label.center.dy, 1.5)); expect(icon.center.dy, closeTo(label.center.dy, 1.5)); }); // The alignment the band was introduced for in the first place. testWidgets('and the hub node shares its centre line with START · HUB', ( tester, ) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), onViewRoute: () {}, ), ); final node = tester.getRect(find.byIcon(Icons.warehouse_rounded).first); final label = tester.getRect(find.text('START · HUB')); expect(node.center.dy, closeTo(label.center.dy, 1.5)); }); testWidgets('and stays hidden when there is nothing to open', ( tester, ) async { await pump( tester, TripCard(trip: Trip.fromStops(id: 't1', stops: [stop('A')])), ); expect( find.text('MAP'), findsNothing, reason: 'the action is hidden until a callback is supplied', ); }); testWidgets('only on START · HUB, never on RETURN · HUB', (tester) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), onViewRoute: () {}, ), ); expect( find.text('MAP'), findsOneWidget, reason: 'one map action per trip, on the row the route starts from', ); expect( tester.getRect(find.text('MAP')).top, lessThan(tester.getRect(find.text('RETURN · HUB')).top), ); }); }); group('Trip tab states', () { Future pumpTabs(WidgetTester tester, {required int selected}) async { await pump( tester, TripTabs( trips: [ Trip.fromStops(id: 't1', stops: [stop('A')]), Trip.fromStops(id: 't2', stops: [stop('B')]), ], selectedIndex: selected, onSelect: (_) {}, ), ); } Color colourOf(WidgetTester tester, String label) => tester.widget(find.text(label)).style!.color!; testWidgets('the selected tab takes the brand accent', (tester) async { // The bug: the pill moved but the label stayed slate, so standing on // Trip 2 looked no different from Trip 2 sitting idle. await pumpTabs(tester, selected: 1); expect( colourOf(tester, 'Trip 2'), ColorConstants.primary, reason: 'the tab the rider is on must read as picked', ); expect(colourOf(tester, 'Trip 1'), isNot(ColorConstants.primary)); await pumpTabs(tester, selected: 0); expect(colourOf(tester, 'Trip 1'), ColorConstants.primary); expect( colourOf(tester, 'Trip 3'), isNot(ColorConstants.primary), reason: 'an unassigned slot stays quiet', ); }); testWidgets('the strip stays compact after the state work', (tester) async { await pumpTabs(tester, selected: 0); final h = tester.getSize(find.byType(TripTabs)).height; expect( h, lessThan(66), reason: 'measured $h — the segmented strip is pinned furniture, so ' 'every point of it is charged at every scroll position', ); }); }); group('Trip tab selection marker', () { Future pumpTabs(WidgetTester tester, {required int selected}) async { await pump( tester, TripTabs( trips: [ Trip.fromStops(id: 't1', stops: [stop('A')]), Trip.fromStops(id: 't2', stops: [stop('B')]), ], selectedIndex: selected, onSelect: (_) {}, ), ); } /// Same tree, but leaves the pill mid-flight so it can be sampled. Future pumpTabsNoSettle( WidgetTester tester, { required int selected, }) async { await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold( body: SingleChildScrollView( child: TripTabs( trips: [ Trip.fromStops(id: 't1', stops: [stop('A')]), Trip.fromStops(id: 't2', stops: [stop('B')]), ], selectedIndex: selected, onSelect: (_) {}, ), ), ), ), ), ); await tester.pump(); } /// The travelling selection marker. /// /// Found by key, not by colour. It moved out of the segments and into the /// pill when it was made to travel, and colour cannot identify it any more: /// crossing an unassigned trip it is deliberately grey rather than the brand /// red, so a colour finder loses it exactly during the journey these tests /// exist to measure. Rect underline(WidgetTester tester) { final marks = find.byKey(TripTabs.markerKey); expect( marks, findsOneWidget, reason: 'exactly one tab may be marked at a time', ); return tester.getRect(marks); } // The marker followed a separate "this trip is running" signal for a while, // which left Trip 1 permanently underlined while the rider stood on Trip 2 — // so a tap appeared to do nothing. The marker belongs to selection. testWidgets('the underline sits under the selected tab', (tester) async { await pumpTabs(tester, selected: 0); final first = underline(tester); final trip1 = tester.getRect(find.text('Trip 1')); expect( first.center.dx, closeTo(trip1.center.dx, 2), reason: 'the marker should be under Trip 1', ); }); testWidgets('and travels when a different tab is picked', (tester) async { await pumpTabs(tester, selected: 0); final before = underline(tester).center.dx; await pumpTabs(tester, selected: 1); await tester.pumpAndSettle(); final after = underline(tester).center.dx; final trip2 = tester.getRect(find.text('Trip 2')); expect( after, greaterThan(before), reason: 'the marker moved from $before to $after — it must follow ' 'the selection rightward to Trip 2', ); expect(after, closeTo(trip2.center.dx, 2)); }); testWidgets('Trip 3 → Trip 1 slides across Trip 2 rather than jumping', ( tester, ) async { // The pill used to belong to each segment: one faded out on the right // while a *different* one faded in on the left, so nothing ever occupied // the space between and there was no object to cross Trip 2 with. await pumpTabs(tester, selected: 2); await tester.pumpAndSettle(); final trip2 = tester.getRect(find.text('Trip 2')); final start = underline(tester).center.dx; expect( start, greaterThan(trip2.center.dx), reason: 'sanity: the marker starts to the right of Trip 2', ); // `pumpTabs` pumps and settles; drive the frames by hand instead so the // journey can be sampled. await pumpTabsNoSettle(tester, selected: 0); final samples = []; for (var elapsed = 0; elapsed < 420; elapsed += 20) { await tester.pump(const Duration(milliseconds: 20)); samples.add(underline(tester).center.dx); } final half = trip2.width / 2; expect( samples.any((x) => (x - trip2.center.dx).abs() < half), isTrue, reason: 'the marker never passed over Trip 2; samples: $samples', ); for (var i = 1; i < samples.length; i++) { expect( samples[i], lessThanOrEqualTo(samples[i - 1] + 0.5), reason: 'the marker moved backwards at sample $i: $samples', ); } await tester.pumpAndSettle(); expect( underline(tester).center.dx, closeTo(tester.getRect(find.text('Trip 1')).center.dx, 2), ); }); testWidgets('the pill is the brand tint, not a white card', (tester) async { // It was a white pill with a drop shadow, which made this the one // selected-state in the app that was not the brand colour — the two // controls a rider uses most looked like they came from different apps. await pumpTabs(tester, selected: 0); final fills = tester .widgetList(find.byType(Container)) .map((c) => c.decoration) .whereType() .toList(); final glass = ColorConstants.primary.withValues(alpha: 0.12); expect( fills.map((d) => d.color), contains(glass), reason: 'the selection pill should be the light glass red the bottom ' 'nav bar uses', ); expect( fills.where( (d) => d.color == Colors.white && (d.boxShadow?.isNotEmpty ?? false), ), isEmpty, reason: 'no raised white pill should remain', ); }); }); // ── Home has exactly two horizontal insets ── // // Bookings cards sit at 16 (`PickupCard` carries `margin: horizontal 16.w` // and its list adds no horizontal padding). Home is wider on purpose: it is a // denser screen — shift card, three stat cards, a pinned tab strip and a rail // above the route — and at 16 the whole column read as pressed against the // border. // // Pinned because it is the kind of value that drifts a couple of points at a // time. What matters is that every element on the screen agrees on it: the // cards, the tab strip and the accept bar were at 16, 20 and 18 respectively // at one point, which is what a gutter looks like when nobody is checking. // ── Select-all leads the list it selects ── // // It used to sit under the last hub row, a full screen of scrolling below the // first stop on a six-stop trip — and it is the control the rider reaches for // *before* reading the list, since "take the lot" is the common case. group('select all', () { List> threeStops() => [stop('A'), stop('B'), stop('C')]; Future pumpTrip(WidgetTester tester, {Set picked = const {}}) { return pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: threeStops()), selectedIds: picked, onToggleSelect: (_) {}, onSelectAll: (_) {}, onAcceptSelected: (_) async {}, ), ); } testWidgets('sits above the route, not below it', (tester) async { await pumpTrip(tester); final selectAll = find.text('Select all 3 stops'); expect(selectAll, findsOneWidget); expect( tester.getRect(selectAll).top, lessThan(tester.getRect(find.text('START · HUB')).top), reason: 'it heads the list; the route starts under it', ); }); testWidgets('counts the picked stops as they are ticked', (tester) async { await pumpTrip(tester, picked: const {'A', 'B'}); expect(find.text('2 of 3 selected'), findsOneWidget); expect(find.text('ALL'), findsOneWidget); }); testWidgets('offers a clear once everything is picked', (tester) async { await pumpTrip(tester, picked: const {'A', 'B', 'C'}); expect(find.text('3 of 3 selected'), findsOneWidget); expect(find.text('CLEAR'), findsOneWidget); }); testWidgets('stays away when there is only one stop to pick', (tester) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), onToggleSelect: (_) {}, onSelectAll: (_) {}, onAcceptSelected: (_) async {}, ), ); expect(find.textContaining('Select all'), findsNothing); }); }); // ── Finished work leaves the decision screen ── // // A completed stop used to stay in the list, greyed out with a "Completed" // chip, and there is no bound on how many of those a shift produces. Six // stops in, the rider was scrolling past six rows he could do nothing with to // reach the one he could. They move to the Activity tab — but the *trip* // keeps them, because its ring and its "N of M done" are counted from it. group('completed stops', () { testWidgets('are not listed, but still count towards the trip', ( tester, ) async { final trip = Trip.fromStops(id: 't1', stops: [ {...stop('A'), 'orderstatus': 'picked'}, stop('B'), ]); await pump( tester, TripCard( trip: trip, acceptedIds: const {'A'}, onToggleSelect: (_) {}, onAcceptSelected: (_) async {}, ), ); // Two stops in the trip, one row on screen. expect(trip.stopCount, 2); expect(find.text('Sri Balaji Stores'), findsOneWidget); // And the brief still counts the finished one. expect(find.text('1 of 2 stops done'), findsOneWidget); }); testWidgets('the numbering does not renumber as stops finish', ( tester, ) async { // Stop 1 is done, so the list starts at 2 — it does not slide up and // call the second stop "1", which would rename a stop mid-shift. await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [ {...stop('A'), 'orderstatus': 'picked'}, stop('B'), ]), acceptedIds: const {'A'}, onToggleSelect: (_) {}, onAcceptSelected: (_) async {}, ), ); expect(find.text('2'), findsOneWidget); expect(find.text('1'), findsNothing); }); }); group('page gutter', () { const homeGutter = 28.0; const stopRowInset = 32.0; /// The rounded surface a piece of text sits on: walk up from the text to the /// nearest `Container` that actually paints a card. Rect cardBehind(WidgetTester tester, String anchor) { final ancestors = find .ancestor(of: find.text(anchor), matching: find.byType(Container)) .evaluate(); for (final e in ancestors) { final d = (e.widget as Container).decoration; if (d is BoxDecoration && d.borderRadius != null) { final box = e.renderObject as RenderBox; final pos = box.localToGlobal(Offset.zero); return Rect.fromLTWH(pos.dx, pos.dy, box.size.width, box.size.height); } } fail('no card found behind "$anchor"'); } testWidgets('the summary cards and the stop rows each keep their inset', ( tester, ) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A', cash: 340)]), ), ); final screen = tester.getSize(find.byType(TripCard)).width; // The summary is one surface now rather than four, so there is one card // to measure instead of a list of captions — but it still has to land on // the same gutter the footer bar uses. Found by its own decoration rather // than by a caption, because every string on the face is data that moves // with the trip. final surface = find.descendant( of: find.byType(TripBriefStrip), matching: find.byWidgetPredicate( (w) => w is Container && w.decoration is BoxDecoration && (w.decoration as BoxDecoration).borderRadius != null, ), ); final right = screen - tester.getRect(surface.first).right; expect( right, closeTo(homeGutter, 0.5), reason: "the brief ends $right from the edge, but Home's gutter is " '$homeGutter — the summary must sit on it', ); // The stop rows are inset further, on purpose: the rail takes the first // 30 points on their left, so matching the gutter on the right left them // visibly lopsided inside their own row. They are indented list items // under the summary, not more cards in the same column. final stopCard = cardBehind(tester, 'Sri Balaji Stores'); final stopRight = screen - stopCard.right; expect( stopRight, closeTo(stopRowInset, 0.5), reason: 'the stop card ends $stopRight from the edge; it should be ' 'inset to $stopRowInset rather than share the summary gutter', ); expect( stopRight, greaterThan(homeGutter), reason: 'the point of the inset is that it clears the border more', ); }); testWidgets('the rail is inset as far as the card edge', (tester) async { await pump( tester, TripCard( trip: Trip.fromStops(id: 't1', stops: [stop('A')]), ), ); // The route block indents as a whole: whatever the card's right edge // clears, the rail's left edge clears too. Insetting one side only left // the rail closer to the border than anything else on screen, so the row // still read as pushed against the edge — just from the other side. final node = tester.getRect(find.byType(TripCard)); final railLeft = tester.getRect(find.text('1')).center.dx - 11; // half the 22pt node, back to its left edge expect( railLeft - node.left, closeTo(stopRowInset, 1.5), reason: 'the rail starts ${railLeft - node.left} in; the card ends ' '$stopRowInset in — an indented block indents on both sides', ); }); }); }