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'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; /// ── Stop type may be a mark, never a surface ── /// /// Pending stop cards used to be *filled* with their stop-type accent at 5% and /// bordered with the same colour at 38%, so the route came down the screen as a /// lavender pane, then a pink one, then a blue one. /// /// The line drawn now is between a mark and a surface. A small filled chip /// naming the mode is a mark: it is a tag, it is bounded, and it does not colour /// anything the rider is reading through. A card fill is a surface, and tinting /// it puts a wash behind the name, the address and the buttons — which is where /// Uber's Base rule bites: surfaces are white or one neutral grey, and the /// accent is reserved for "one moment per screen". /// /// So the type colours are allowed to appear, on a chip, and the chip fills use /// the *darkened* variants ([ColorConstants.pickupChip] and friends) so white /// text on them clears AA. What they may not do is fill or border a card. /// /// These tests pin that by colour value, which is the only way it stays pinned — /// a comment saying "keep this neutral" does not survive a redesign. /// /// Note on coverage: `pickupAccent` is an alias of `ColorConstants.primary`, so /// it is indistinguishable from the legitimate brand accent and cannot be /// asserted against. void main() { Map stop( String id, { required String type, double cash = 0, int deliverQty = 0, int pickupQty = 0, }) => { 'orderid': id, 'type': type, 'pickupcustomer': 'Sri Balaji Stores', 'pickupaddress': '19, Thadagam Road, Venkitapuram, Coimbatore', 'pickupcontactno': '9876543210', if (deliverQty > 0) 'deliveryqty': '$deliverQty', if (pickupQty > 0) 'pickupqty': '$pickupQty', 'Quantity': '3', if (cash > 0) 'collectionamt': '$cash', 'expected_pickup_time': DateTime.now().toIso8601String(), }; Future pumpCard( WidgetTester tester, { Set accepted = const {}, double width = 390, }) async { tester.view.physicalSize = Size(width * 3, 2532); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final trip = Trip.fromStops( id: 'mixed', stops: [ stop('P', type: 'pickup', pickupQty: 4), stop('D', type: 'delivery', deliverQty: 2), stop('C', type: 'combined', deliverQty: 1, pickupQty: 2, cash: 340), ], ); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold( body: SingleChildScrollView( child: TripCard( trip: trip, acceptedIds: accepted, onToggleSelect: (_) {}, ), ), ), ), ), ); await tester.pumpAndSettle(); } /// Every colour the card paints: decoration fills, border sides, icon tints /// and text colours (including rich-text spans). List<(String, Color)> paintedColours(WidgetTester tester) { final found = <(String, Color)>[]; void addBorder(String where, BoxBorder? b) { if (b is Border) { for (final side in [b.top, b.right, b.bottom, b.left]) { if (side.style != BorderStyle.none) found.add(('$where.border', side.color)); } } } // Walks `children` directly rather than via `visitChildren`: that visitor // yields the span itself before its children, so recursing from inside it // re-enters on the same node forever. void addSpan(String where, InlineSpan? span) { if (span == null) return; final c = span.style?.color; if (c != null) found.add(('$where.span', c)); if (span is TextSpan) { for (final child in span.children ?? const []) { addSpan(where, child); } } } for (final w in tester.allWidgets) { switch (w) { case Container(:final decoration): if (decoration is BoxDecoration) { if (decoration.color != null) { found.add(('Container.color', decoration.color!)); } addBorder('Container', decoration.border); } case DecoratedBox(:final decoration): if (decoration is BoxDecoration) { if (decoration.color != null) { found.add(('DecoratedBox.color', decoration.color!)); } addBorder('DecoratedBox', decoration.border); } case Icon(:final color) when color != null: found.add(('Icon', color)); case Text(:final style, :final textSpan): if (style?.color != null) found.add(('Text', style!.color!)); addSpan('Text', textSpan); case RichText(:final text): addSpan('RichText', text); default: break; } } return found; } /// The *undarkened* accents. These are the values that were washing the card /// fills and borders; the chip uses the darkened variants instead, so seeing /// one of these anywhere means a tint has crept back. final bannedOutright = { 'deliveryAccent (royal blue)': ColorConstants.deliveryAccent, 'combinedAccent (deep indigo)': ColorConstants.combinedAccent, }; testWidgets('the un-darkened type accents are not painted at all', ( tester, ) async { await pumpCard(tester); final painted = paintedColours(tester); expect(painted, isNotEmpty, reason: 'sanity: the card painted something'); for (final entry in bannedOutright.entries) { final hits = painted.where((p) => p.$2 == entry.value).toList(); expect( hits, isEmpty, reason: '${entry.key} is the light accent — the chip should use the ' 'darkened variant. Found at ${hits.map((h) => h.$1).toSet()}', ); } }); testWidgets('the same holds once some stops are accepted', (tester) async { await pumpCard(tester, accepted: {'D', 'C'}); final painted = paintedColours(tester); for (final entry in bannedOutright.entries) { expect( painted.where((p) => p.$2 == entry.value), isEmpty, reason: '${entry.key} must not appear in the accepted state either', ); } }); // ── Where the type went, and why these two tests changed ── // // The rule this file was written for was "a stop type may be a mark, never a // surface", and it was measured by finding the type *word* on the row and // checking nothing was filled with its colour. // // The route timeline then took the word off the row as well. That was not a // slip: a step is `who` and `which bag`, the two facts a rider matches // against a shelf, and the type went into the detail sheet with the address, // the phone and the parcel counts. So the rule gets **stricter** rather than // looser — a stop type may not tint anything on the route, and it may not // spend a line there either — and that is what is measured now. // // The wording itself is not lost and is not untested: [StopKind.badgeLabel] // and [StopKind.chipLabel] still carry it, the Bookings card still prints it // on a line of its own, and `service_card_test.dart` holds both. testWidgets('a stop type tints nothing on the route', (tester) async { await pumpCard(tester); final containerColours = tester.allWidgets .whereType() .map((c) => c.decoration) .whereType() .expand( (d) => [ d.color, if (d.border is Border) (d.border as Border).top.color, ], ) .whereType() .toList(); final textColours = tester.allWidgets .whereType() .map((t) => t.style?.color) .whereType() .toSet(); for (final chip in [ ColorConstants.pickupChip, ColorConstants.deliveryChip, ColorConstants.combinedChip, ]) { expect( containerColours.contains(chip), isFalse, reason: 'a stop type may not fill or border anything on the route', ); expect( textColours.contains(chip), isFalse, reason: 'nor tint type: the route is one accent — the brand, on the live ' 'node and the journey action — and a per-stop hue competes with it', ); } }); testWidgets('a step states who it is for, and no stop type', (tester) async { await pumpCard(tester); // This is a parcel route, so each stop is its own node and what names it is // the customer plus its position in the run. (The `who` + `which bag` pair // is the kitchen-group shape and is held by `one_bag_per_order_test.dart`.) expect(find.text('Sri Balaji Stores'), findsWidgets); expect(find.text('Stop 1'), findsOneWidget); // Everything the card used to shout on every row. The type is one tap away // in the sheet, on the badge `StopDetailSheet._header` draws. for (final label in const [ 'PICKUP', 'DELIVERY', 'P&D', 'PICKUP & DELIVERY', ]) { expect( find.text(label), findsNothing, reason: '"$label" is on the stop sheet, not on a row a rider reads forty ' 'times a shift', ); } }); testWidgets('the type chip is never squeezed out of shape', (tester) async { // The bug: the chip rendered as "P…". Its `Flexible` sat beside a `Spacer`, // and a Spacer is an `Expanded` — so the two split the free width evenly and // the chip was capped at about 23pt against a 63pt label, i.e. **36% of what // it needed**, no matter how short the wording was. // // Asserted as a *ratio* to the label's own natural width rather than as a // fixed number of points. The headless test font draws every glyph as a full // em square — roughly twice a real font — so absolute widths here mean // nothing, but "how much of what it asked for did it get" survives the // difference. A flat floor would also wrongly fail "P&D", which is three // characters and legitimately narrow. const squeezed = 0.36; // what the broken layout gave const acceptable = 0.6; for (final width in [320.0, 360.0, 390.0, 430.0]) { await pumpCard(tester, width: width); expect( tester.takeException(), isNull, reason: 'the meta row overflowed at ${width.toInt()}pt', ); for (final label in ['PICKUP', 'DELIVERY', 'P&D']) { final finder = find.text(label); if (finder.evaluate().isEmpty) continue; final box = finder.evaluate().first.renderObject! as RenderBox; final natural = (TextPainter( text: TextSpan( text: label, style: tester.widget(finder.first).style, ), textDirection: TextDirection.ltr, )..layout()).width; final got = box.size.width / natural; expect( got, greaterThan(acceptable), reason: 'at ${width.toInt()}pt the "$label" chip got ' '${(got * 100).round()}% of the width its label needs; the ' 'Spacer-stealing bug gave ${(squeezed * 100).round()}%', ); } } }); }