first commit
This commit is contained in:
297
test/stop_card_neutral_test.dart
Normal file
297
test/stop_card_neutral_test.dart
Normal file
@@ -0,0 +1,297 @@
|
||||
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<String, dynamic> 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<void> pumpCard(WidgetTester tester,
|
||||
{Set<String> 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 <InlineSpan>[]) {
|
||||
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 = <String, Color>{
|
||||
'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<Container>()
|
||||
.map((c) => c.decoration)
|
||||
.whereType<BoxDecoration>()
|
||||
.map((d) => d.color)
|
||||
.whereType<Color>()
|
||||
.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<Container>()
|
||||
.map((c) => c.decoration)
|
||||
.whereType<BoxDecoration>()
|
||||
.map((d) => d.border)
|
||||
.whereType<Border>()
|
||||
.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<Container>()
|
||||
.map((c) => c.decoration)
|
||||
.whereType<BoxDecoration>()
|
||||
.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<Text>(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()}%');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user