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'); } }); testWidgets('type colour reaches the chip and nothing else', (tester) async { await pumpCard(tester); // Present: the three darkened chip fills, one per stop kind. final containerFills = tester.allWidgets .whereType() .map((c) => c.decoration) .whereType() .map((d) => d.color) .whereType() .toList(); for (final chip in [ ColorConstants.pickupChip, ColorConstants.deliveryChip, ColorConstants.combinedChip, ]) { expect(containerFills, contains(chip), reason: 'the type chip should be filled with its darkened accent'); } // Absent: any of those three used as a *border*. A chip is a mark; a border // belongs to a surface, and that is the line being held. final borderColours = tester.allWidgets .whereType() .map((c) => c.decoration) .whereType() .map((d) => d.border) .whereType() .expand((b) => [b.top.color, b.right.color, b.bottom.color, b.left.color]) .toList(); for (final chip in [ ColorConstants.pickupChip, ColorConstants.deliveryChip, ColorConstants.combinedChip, ]) { expect(borderColours, isNot(contains(chip)), reason: 'stop type must not outline a surface'); } }); testWidgets('stop cards are borderless, on one fill whatever the type', (tester) async { await pumpCard(tester); // The stop bodies: a rounded fill with no border at all now. final bodies = tester.allWidgets .whereType() .map((c) => c.decoration) .whereType() .where((d) => d.color == ColorConstants.cardSurface) .toList(); expect(bodies.length, 3, reason: 'one fill per stop, all three identical'); for (final d in bodies) { expect(d.border, isNull, reason: 'the card carries no border'); expect(d.boxShadow, anyOf(isNull, isEmpty), reason: 'the fill does the separating, not an elevation'); } }); testWidgets('type is stated in words, not colour', (tester) async { await pumpCard(tester); // The labels still say which kind of stop each one is — that information was // never removed, only moved out of the fill and into the text. // // The combined chip reads "P&D", not "PICKUP & DELIVERY". On a stop row the // chip shares a line with a duration, a call button and a tick box, all of // which have a fixed claim on the width, so 17 characters ellipsised down to // "P…" — a chip that has to truncate is not a chip. The full wording still // appears on the Bookings card, which has a whole line for it. expect(find.text('PICKUP'), findsWidgets); expect(find.text('DELIVERY'), findsWidgets); expect(find.text('P&D'), findsWidgets); expect(find.text('PICKUP & DELIVERY'), findsNothing, reason: 'the long label does not fit a stop-row chip'); }); 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()}%'); } } }); }