import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/home/route_timeline.dart'; import 'package:miler/views/Dashboard/home/trip.dart'; import 'package:miler/views/Dashboard/home/trip_card.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; /// ───────────────────────────────────────────────────────────────────────── /// CONTENT LEADS, CONTAINERS RECEDE /// /// Home's grouping unit is a **pickup place**, and for three iterations it was /// drawn as a box. First a card per order — fifteen boxes on a route. Then a /// card per place, which was the right correction to the wrong object and /// still the wrong object: a route already says that two things belong /// together, with a spine, a node and an indent, so the rectangle said it a /// second time in a louder voice. The orders then needed a *tinted* rectangle /// inside that one to separate themselves from their own container, which is /// the point at which the furniture is arguing with itself. /// /// These pin the third answer: no surface at all. The group is the spine plus /// whitespace plus type, the orders sit on Home's own ground, and the three /// things a rider does to a kitchen have three separate targets. /// ───────────────────────────────────────────────────────────────────────── void main() { tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel)); Map meal( String id, String who, { int step = 1, String? printed, }) => { 'orderid': id, if (printed != null) 'baglabel': printed, 'type': 'delivery', 'step': step, 'pickupcustomer': who, 'kitchenid': 'K1', 'kitchenname': 'Vidhya Kitchen', 'sourcename': 'Vidhya Kitchen', 'pickupcontactno': '9787698259', 'pickupaddress': '4 Cross, RS Puram, Coimbatore', 'dropaddress': '12 SNS Colony, Peelamedu, Coimbatore 641004', 'Quantity': '1', }; Future pumpHome( WidgetTester tester, { Set accepted = const {}, Set selected = const {}, void Function(String)? onToggleSelect, double width = 390, double textScale = 1.0, /// What each counter printed on an order, by id. Empty by default: most /// counters print nothing, and the app invents nothing for them. Map printedLabels = const {}, }) async { ServiceProfile.setActive(ServiceProfile.milkMan); tester.view.physicalSize = Size(width * 3, 2600 * 3); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.reset); final trip = Trip.fromStops( id: 'run', stops: [ meal('1', 'Joe Mathew', step: 1, printed: printedLabels['1']), meal('2', 'Arun Prakash', step: 2, printed: printedLabels['2']), meal('3', 'Ravi Kumar', step: 3, printed: printedLabels['3']), ], ); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( builder: (context, child) => MediaQuery( data: MediaQuery.of( context, ).copyWith(textScaler: TextScaler.linear(textScale)), child: child!, ), home: Scaffold( backgroundColor: ColorConstants.daylightSurface, body: SingleChildScrollView( child: TripCard( trip: trip, acceptedIds: accepted, selectedIds: selected, onToggleSelect: onToggleSelect ?? (_) {}, onSelectAll: (_) {}, onStopTap: (_) {}, onCallStop: (_) {}, ), ), ), ), ), ); await tester.pumpAndSettle(); } /// Makes sure the kitchen's orders are on screen. /// /// The run's first place opens itself — a rider arriving at work should see /// the counter he is riding to, not a closed list — so this only taps when /// it is actually shut. Tapping unconditionally would *collapse* it, which /// is how a row assertion ends up looking like a broken row. Future openKitchen(WidgetTester tester) async { if (find.text('Joe Mathew').evaluate().isEmpty) { await tester.tap(find.text('Vidhya Kitchen')); await tester.pumpAndSettle(); } } group('the kitchen is not a box', () { testWidgets('no surface, border, shadow or radius groups a kitchen', ( tester, ) async { await pumpHome(tester); // Every decoration inside the group, at any depth. A grouping rectangle // is exactly what this screen may not draw — see the header note. final decorations = tester .widgetList( find.descendant( of: find.byType(PickupTimelineGroup), matching: find.byType(Container), ), ) .map((c) => c.decoration) .whereType(); // Circles are exempt: the sequence node and the tick are round marks on // the spine, which is the language doing the grouping rather than a // rectangle competing with it. final rectangles = decorations.where((d) => d.shape != BoxShape.circle); for (final d in rectangles) { expect( d.boxShadow, anyOf(isNull, isEmpty), reason: 'a shadow lifts a group off the page; the spine groups it', ); // ── A rule between rows is not an outline around them ── // // This required `border` to be null outright, which caught the thing // it was written for — a rectangle drawn round a kitchen — and also // catches a one-sided hairline separating one order from the next. // Those are opposites: a box groups by enclosing, a rule separates by // dividing, and the second is the spine's own grammar rather than a // competitor to it. What stays banned is an enclosure. final border = d.border; if (border is Border) { expect( border.left.style == BorderStyle.none && border.right.style == BorderStyle.none && border.top.style == BorderStyle.none, isTrue, reason: 'an outline around a group is the card by another name', ); } else { expect( border, isNull, reason: 'an outline around a group is the card by another name', ); } } // ── What a fill may be here, since the route moved onto a panel ── // // The original rule was "no filled rectangle at all": the orders sat on // a white page, so any panel behind them was a card by another name. // The route now stands on a white [MilerPanel], which re-created the // exact flatness that rule was written against — an open kitchen was // white-on-white. Two fills are now legitimate, and only these two: // // • exactly one canvas-toned well behind the open orders — the // ladder's single permitted inset, reading as the card opening // down to the page; // • small white tags (the bag labels) standing on that well. // // What stays banned is what the rule always meant: a white panel wide // enough to be a row's or the group's own card. final fillBoxes = find .descendant( of: find.byType(PickupTimelineGroup), matching: find.byType(DecoratedBox), ) .evaluate() .where((e) { final d = (e.widget as DecoratedBox).decoration; return d is BoxDecoration && d.shape != BoxShape.circle && (d.color == ColorConstants.pureSurface || d.color == ColorConstants.daylightSurface); }) .toList(); final wells = fillBoxes .where( (e) => ((e.widget as DecoratedBox).decoration as BoxDecoration) .color == ColorConstants.daylightSurface, ) .toList(); expect( wells.length, lessThanOrEqualTo(1), reason: 'one well behind the open orders, never a stack of them', ); for (final e in fillBoxes) { final d = (e.widget as DecoratedBox).decoration as BoxDecoration; if (d.color == ColorConstants.pureSurface) { final w = e.renderObject!.paintBounds.width; expect( w, lessThan(120), reason: 'a white fill inside the group must be a small tag — at ' '${w.toStringAsFixed(1)}pt wide it is a row panel, which is ' 'the card by another name', ); } } }); testWidgets('an order row carries no tile of its own', (tester) async { await pumpHome(tester, accepted: const {'1'}, selected: const {'1'}); // Selection and acceptance are carried by the node — shape and fill — // and by the name's weight. Painting whole rows made a list of five // steps read as five loose objects on open ground. final steps = find.byType(PickupTimelineStep); expect(steps, findsWidgets); final tinted = tester .widgetList( find.descendant( of: steps, matching: find.byType(AnimatedContainer), ), ) .map((c) => c.decoration) .whereType() // The node is a filled disc when selected — that is the channel // carrying the state, and it is deliberately not a rectangle. .where((d) => d.shape != BoxShape.circle) .where((d) => d.color != null && d.color != Colors.transparent); expect(tinted, isEmpty, reason: 'no wash across a row'); }); }); group('three targets, three jobs', () { testWidgets('the kitchen header only expands — it never selects', ( tester, ) async { final selected = []; await pumpHome(tester, onToggleSelect: selected.add); // The header is the identity block. Tapping it opens and closes the // place, and that is the whole of its contract: a gesture that also // ticked an order would mean the same object did two things depending // on data the rider cannot see. await tester.tap(find.text('Vidhya Kitchen')); await tester.pumpAndSettle(); expect(selected, isEmpty, reason: 'disclosure is not selection'); }); testWidgets('an order row selects, and does not open anything', ( tester, ) async { final selected = []; final opened = []; await pumpHome(tester, onToggleSelect: selected.add); await openKitchen(tester); await tester.tap(find.text('Joe Mathew')); await tester.pumpAndSettle(); expect(selected, ['1']); expect(opened, isEmpty); }); testWidgets('the corner block opens details, and does not select', ( tester, ) async { final selected = []; // The tag is the counter's printed label now — the derived `Bag 1` is // gone — so the block is tapped by the label the kitchen really stuck on. await pumpHome( tester, onToggleSelect: selected.add, printedLabels: const {'1': 'DG-1042'}, ); await openKitchen(tester); // Siblings, never nested: two recognisers over the same pixels resolve // to neither, which is how the chevron was once dead for a whole build. await tester.tap(find.text('DG-1042')); await tester.pumpAndSettle(); expect( selected, isEmpty, reason: 'the corner is the way in, not a second tick', ); }); }); group('the metadata is metadata', () { testWidgets('Pending is its own amber, and never the warning one', ( tester, ) async { await pumpHome(tester); // ── What this test used to assert, and what survives of it ── // // It demanded neutral grey, on the argument that amber is this app's // exception hue and every kitchen is pending on a fresh morning — so // spending it here opens the whole screen in warning. // // Half of that argument is still load-bearing and is still checked: the // word must not be [ColorConstants.warning]. The other half was wrong — // "waiting on you" is worth a colour of its own, and grey made the rider // read undecided work by the *absence* of a mark. Pending has its own // gold now; the thing that must never happen is it colliding with the // hue reserved for a skip or a short payment. // `Pending 3/3` — the word carries the count now, the same way Accepted // states its share of a counter. See the chip in `route_timeline.dart`. final word = tester.widget(find.textContaining('Pending')); expect(word.data, 'Pending 3/3'); expect(word.style!.color, ColorConstants.pendingAmber); expect(word.style!.color, isNot(ColorConstants.warning)); expect(word.style!.color, isNot(ColorConstants.primary)); }); testWidgets('Accepted is green and Collected is not', (tester) async { // The two rungs a rider most needs to tell apart on one counter. They // were both green once — accepted took the brand red only on the group // being ridden to, and collected took [ColorConstants.acceptGreen] — so // the pair could render as one hue and two words. await pumpHome(tester, accepted: const {'1', '2', '3'}); final accepted = tester.widget(find.textContaining('Accepted')); expect(accepted.style!.color, ColorConstants.acceptGreen); expect( ColorConstants.pickedBlue, isNot(ColorConstants.acceptGreen), reason: 'in-hand and taken-on must not share a colour', ); }); testWidgets('a partly accepted kitchen says so as a fraction', ( tester, ) async { await pumpHome(tester, accepted: const {'1'}); // Two of five accepted must never render the same word as five of five. expect(find.text('Accepted 1/3'), findsOneWidget); expect(find.text('Accepted'), findsNothing); }); testWidgets('the state wears no capsule', (tester) async { await pumpHome(tester); // ── Only the boxes that hug the word ── // // This walked *every* `DecoratedBox` above the text and demanded none of // them be filled, which held only while Home had no filled surfaces at // all. The route now stands on a white `MilerPanel` — layer 1, the page // having a ground is the point — and that panel is an ancestor of every // word on the screen, so the test began failing on a surface it was // never about. // // A capsule is a box drawn *around a word*: it is the width of the word // plus its padding. A page surface is not. Filtering by width keeps the // original claim — the state is set, not stamped — and makes it immune // to whatever the page is standing on. final word = tester.getRect(find.textContaining('Pending')); final chip = find.ancestor( of: find.textContaining('Pending'), matching: find.byType(DecoratedBox), ); for (final e in chip.evaluate()) { final box = (e.widget as DecoratedBox).decoration; if (box is! BoxDecoration) continue; final r = tester.getRect(find.byWidget(e.widget)); if (r.width > word.width + 48) continue; // a surface, not a capsule expect( box.color, anyOf(isNull, Colors.transparent), reason: 'operational metadata is set, not stamped', ); } }); }); group('the order row', () { testWidgets('shows the area, never a truncated full address', ( tester, ) async { await pumpHome(tester); await openKitchen(tester); // `Peelamedu`, not `12 SNS Colony, Peelamedu, Coimb…`. The full address // is a detail-sheet fact; what the rider decides on here is the area. expect(find.textContaining('641004'), findsNothing); expect(find.textContaining('12 SNS Colony'), findsNothing); expect(find.text('Peelamedu'), findsWidgets); }); testWidgets('a counter that prints nothing shows no tag', (tester) async { // ── What this file used to assert here ── // // That every step carried `Bag 1 … Bag 3`, right-aligned on one axis so // the phone could be read against a shelf. The numbering was the app's // own — derived from each row's position whenever the payload printed // nothing, which on this fixture is every row — and it sat two columns // from a rail already numbering the same steps. // // Only what a counter actually printed survives. The fixture prints // nothing, so the rows carry the person and the chevron and no tag at // all. await pumpHome(tester); await openKitchen(tester); for (final label in const ['Bag 1', 'Bag 2', 'Bag 3']) { expect(find.text(label), findsNothing); } // The people are still there, one row each. expect(find.text('Joe Mathew'), findsOneWidget); }); testWidgets('a printed label is shown exactly as it was sent', ( tester, ) async { // A code the kitchen stuck on the order is a fact about an object in the // rider's hands, so it travels verbatim — never prefixed, never renamed. await pumpHome(tester, printedLabels: const {'1': 'DG-1042'}); await openKitchen(tester); expect(find.text('DG-1042'), findsOneWidget); expect(find.textContaining('Bag'), findsNothing); }); }); group('the offer states its own number', () { testWidgets('resting reads "Select all 3"', (tester) async { await pumpHome(tester); await openKitchen(tester); // The count is what makes the offer an offer. It used to be bare, on the // argument that the card above said `3 orders` — and the card is gone. expect(find.text('Select all 3'), findsOneWidget); }); testWidgets('partial and full both state the fraction', (tester) async { await pumpHome(tester, selected: const {'1'}); await openKitchen(tester); expect(find.text('1 of 3 selected'), findsOneWidget); await pumpHome(tester, selected: const {'1', '2', '3'}); await openKitchen(tester); expect(find.text('All 3 selected · Clear'), findsOneWidget); }); }); }