Files
doormile_milderapp/test/trip_test.dart
2026-08-11 13:16:33 +05:30

746 lines
26 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/views/Dashboard/home/trip.dart';
import 'package:miler/views/Dashboard/pickups/route_metrics.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
import 'fixtures/demo_stops.dart';
Map<String, dynamic> stop({
required String id,
int? step,
String type = 'pickup',
int qty = 1,
double? cash,
double lat = 11.03,
double lng = 76.96,
DateTime? due,
}) =>
{
'orderid': id,
if (step != null) 'step': step,
'type': type,
'Quantity': '$qty',
if (cash != null) 'collectionamt': '$cash',
'pickuplat': '$lat',
'pickuplon': '$lng',
if (due != null) 'expected_pickup_time': due.toIso8601String(),
};
void main() {
group('RouteMetricsHelper — distance units', () {
test('shows metres below 1 km, rounded to 10', () {
expect(RouteMetricsHelper.formatDistance(450), '450 m');
expect(RouteMetricsHelper.formatDistance(447), '450 m');
expect(RouteMetricsHelper.formatDistance(83), '80 m');
expect(RouteMetricsHelper.formatDistance(999), '1000 m');
});
test('shows kilometres with one decimal at and above 1 km', () {
expect(RouteMetricsHelper.formatDistance(1000), '1.0 km');
expect(RouteMetricsHelper.formatDistance(3200), '3.2 km');
});
test('drops the decimal past 100 km', () {
expect(RouteMetricsHelper.formatDistance(104400), '104 km');
});
test('never fakes a zero', () {
expect(RouteMetricsHelper.formatDistance(null), '—');
expect(RouteMetricsHelper.formatDistance(0), '—');
expect(RouteMetricsHelper.formatDistanceAway(null), 'Distance unknown');
});
test('rounds a tiny distance up to the 10 m floor', () {
expect(RouteMetricsHelper.formatDistance(3), '10 m');
});
});
group('RouteMetricsHelper — speed and ETA', () {
test('falls back to the urban average when speed is unusable', () {
expect(RouteMetricsHelper.usableSpeedMps(null),
RouteMetricsHelper.kDefaultSpeedMps);
// Stopped at a light must not stretch the ETA to an hour.
expect(RouteMetricsHelper.usableSpeedMps(0.4),
RouteMetricsHelper.kDefaultSpeedMps);
// A GPS spike is a bad fix, not a fast rider.
expect(RouteMetricsHelper.usableSpeedMps(60),
RouteMetricsHelper.kDefaultSpeedMps);
});
test('uses a real riding speed when it is plausible', () {
expect(RouteMetricsHelper.usableSpeedMps(8.0), 8.0);
});
test('computes an arrival clock time', () {
final now = DateTime(2026, 7, 27, 16, 0);
// 1110 m at 5.55 m/s = 200 s ≈ 3 min.
final eta = RouteMetricsHelper.eta(1110, now: now)!;
expect(eta.difference(now).inSeconds, 200);
expect(RouteMetricsHelper.formatEta(1110, now: now), '4:03 PM');
});
test('formats travel time readably', () {
expect(RouteMetricsHelper.formatDuration(const Duration(minutes: 4)),
'4 min');
expect(
RouteMetricsHelper.formatDuration(
const Duration(hours: 1, minutes: 5)),
'1h 05m');
});
});
group('Trip — grouping into slots', () {
test('buckets stops into clock-aligned slot windows', () {
final trips = Trip.groupIntoTrips([
stop(id: 'a', due: DateTime(2026, 7, 27, 10, 15)),
stop(id: 'b', due: DateTime(2026, 7, 27, 12, 40)),
stop(id: 'c', due: DateTime(2026, 7, 27, 14, 5)),
]);
// 10:15 and 12:40 both fall in the 09:00–12:00 / 12:00–15:00 blocks.
expect(trips.length, 2);
expect(trips.first.slotStart, DateTime(2026, 7, 27, 9));
expect(trips.first.slotEnd, DateTime(2026, 7, 27, 12));
expect(trips.first.stopCount, 1);
expect(trips.last.stopCount, 2);
});
test('an explicit backend trip id wins over time bucketing', () {
final trips = Trip.groupIntoTrips([
{...stop(id: 'a', due: DateTime(2026, 7, 27, 10, 0)), 'tripid': 'T1'},
{...stop(id: 'b', due: DateTime(2026, 7, 27, 20, 0)), 'tripid': 'T1'},
]);
expect(trips.length, 1);
expect(trips.single.stopCount, 2);
});
test('stops with no time still land in a trip rather than vanishing', () {
final trips = Trip.groupIntoTrips([stop(id: 'a'), stop(id: 'b')]);
expect(trips.length, 1);
expect(trips.single.stopCount, 2);
expect(trips.single.slotLabel, "Today's route");
});
test('trips come back earliest slot first', () {
final trips = Trip.groupIntoTrips([
stop(id: 'late', due: DateTime(2026, 7, 27, 16, 0)),
stop(id: 'early', due: DateTime(2026, 7, 27, 10, 0)),
]);
expect(trips.first.stops.first['orderid'], 'early');
});
test('formats the slot window as a clock range', () {
final trips = Trip.groupIntoTrips(
[stop(id: 'a', due: DateTime(2026, 7, 27, 11, 30))],
windowHours: 3,
);
expect(trips.single.slotLabel, '9:00 AM – 12:00 PM');
});
});
group('Trip — ordering', () {
test('respects the admin step sequence, never distance', () {
final ordered = Trip.sortStops([
stop(id: 'c', step: 3),
stop(id: 'a', step: 1),
stop(id: 'b', step: 2),
]);
expect([for (final s in ordered) s['orderid']], ['a', 'b', 'c']);
});
test('falls back to booked time when step is missing', () {
final ordered = Trip.sortStops([
stop(id: 'later', due: DateTime(2026, 7, 27, 12, 0)),
stop(id: 'sooner', due: DateTime(2026, 7, 27, 10, 0)),
]);
expect(ordered.first['orderid'], 'sooner');
});
test('is stable when nothing distinguishes two stops', () {
final ordered = Trip.sortStops([stop(id: 'x'), stop(id: 'y')]);
expect([for (final s in ordered) s['orderid']], ['x', 'y']);
});
});
group('Trip — duration estimates', () {
test('service time varies by stop kind', () {
final pickup = Trip.serviceTimeFor(stop(id: 'p', type: 'pickup'));
final delivery = Trip.serviceTimeFor(stop(id: 'd', type: 'delivery'));
final combined = Trip.serviceTimeFor(stop(id: 'c', type: 'combined'));
// Handing over is quickest; doing both at one door is slowest.
expect(delivery < pickup, isTrue);
expect(pickup < combined, isTrue);
});
test('cash collection adds real minutes', () {
final free = Trip.serviceTimeFor(stop(id: 'a', type: 'pickup'));
final cod = Trip.serviceTimeFor(stop(id: 'b', type: 'pickup', cash: 250));
expect(cod - free, const Duration(seconds: 90));
});
test('extra parcels add handling time', () {
final one = Trip.serviceTimeFor(stop(id: 'a', type: 'pickup', qty: 1));
final five = Trip.serviceTimeFor(stop(id: 'b', type: 'pickup', qty: 5));
expect(five > one, isTrue);
});
test('total duration is ride time plus every doorstep minute', () {
final trip = Trip.fromStops(
id: 't',
stops: [
stop(id: 'a', step: 1, lat: 11.03, lng: 76.96),
stop(id: 'b', step: 2, lat: 11.05, lng: 76.98),
],
hubLat: 11.01,
hubLng: 76.95,
);
expect(trip.totalDuration, trip.travelDuration + trip.serviceDuration);
expect(trip.serviceDuration > Duration.zero, isTrue);
expect(trip.travelDuration > Duration.zero, isTrue);
});
test('round trip includes the return leg to the hub', () {
final stops = [
stop(id: 'a', step: 1, lat: 11.05, lng: 76.98),
];
final withHub = Trip.fromStops(
id: 'h', stops: stops, hubLat: 11.01, hubLng: 76.95);
final withoutHub = Trip.fromStops(id: 'n', stops: stops);
// hub → stop → hub must be about twice the one-way leg, and strictly
// longer than a chain with no hub at all.
expect(withHub.routeMeters, greaterThan(withoutHub.routeMeters));
});
test('average stop duration divides service time across stops', () {
final trip = Trip.fromStops(
id: 't',
stops: [
stop(id: 'a', step: 1, type: 'pickup'),
stop(id: 'b', step: 2, type: 'pickup'),
],
);
expect(trip.averageStopDuration.inSeconds,
trip.serviceDuration.inSeconds ~/ 2);
});
test('rounds estimates up to 5 minutes so they read as estimates', () {
expect(roundTripDuration(const Duration(minutes: 37)),
const Duration(minutes: 40));
expect(roundTripDuration(const Duration(minutes: 1)),
const Duration(minutes: 5));
expect(formatTripDuration(const Duration(minutes: 100)), '1h 40m');
});
});
group('Trip — totals', () {
test('splits deliver and collect parcels and sums cash', () {
final trip = Trip.fromStops(
id: 't',
stops: [
stop(id: 'a', step: 1, type: 'delivery', qty: 2),
stop(id: 'b', step: 2, type: 'pickup', qty: 3, cash: 250),
],
);
expect(trip.deliverParcels, 2);
expect(trip.collectParcels, 3);
expect(trip.cashToCollect, 250);
expect(trip.orderIds, ['a', 'b']);
});
test('a combined stop counts on both sides', () {
final combined = {
...stop(id: 'c', step: 1),
'type': 'combined',
'deliveryqty': '1',
'pickupqty': '2',
};
final trip = Trip.fromStops(id: 't', stops: [combined]);
expect(stopKindOf(combined), StopKind.combined);
expect(trip.deliverParcels, 1);
expect(trip.collectParcels, 2);
expect(trip.totalParcels, 3);
});
});
group('StopKind — three modes', () {
test('infers combined from a payload carrying both quantities', () {
final s = {'deliveryqty': '1', 'pickupqty': '2'};
expect(stopKindOf(s), StopKind.combined);
});
test('combined runs the delivery leg first', () {
expect(StopKind.combined.legs, [StopLeg.delivery, StopLeg.pickup]);
expect(StopKind.pickup.legs, [StopLeg.pickup]);
expect(StopKind.delivery.legs, [StopLeg.delivery]);
});
test('isDelivery stays narrow so combined keeps its payment path', () {
// A combined stop still has a pickup leg that can carry COD, so it must
// NOT be treated as a prepaid delivery-only stop.
expect(StopKind.combined.isDelivery, isFalse);
expect(StopKind.combined.hasDelivery, isTrue);
expect(StopKind.combined.hasPickup, isTrue);
});
test('labels and accents differ per mode', () {
expect(StopKind.combined.badgeLabel, 'PICKUP & DELIVERY');
expect(StopKind.combined.startCtaLabel, 'Start Combined Stop');
expect(StopKind.pickup.accent == StopKind.delivery.accent, isFalse);
expect(StopKind.combined.accent == StopKind.pickup.accent, isFalse);
});
test('survives a wire round trip', () {
for (final k in StopKind.values) {
expect(stopKindFromWire(k.wireName), k);
}
expect(stopKindFromWire('nonsense'), StopKind.pickup);
});
});
group('StopState — per-stop lifecycle', () {
final s1 = stop(id: 'a', step: 1);
final s2 = stop(id: 'b', step: 2);
test('local accept/reject wins over a stale server status', () {
// The optimistic write must not be bounced back to pending by a slow
// refetch that still says "new".
expect(
stopStateOf(s1, acceptedIds: {'a'}, rejectedIds: const {}),
StopState.accepted,
);
expect(
stopStateOf(s2, acceptedIds: const {}, rejectedIds: {'b'}),
StopState.rejected,
);
});
test('reads the backend status when there is no local decision', () {
expect(
stopStateOf({...s1, 'orderstatus': 'picked'},
acceptedIds: const {}, rejectedIds: const {}),
StopState.done,
);
expect(
stopStateOf({...s1, 'orderstatus': 'ARRIVED'},
acceptedIds: const {}, rejectedIds: const {}),
StopState.active,
);
expect(
stopStateOf({...s1, 'orderstatus': 'skipped'},
acceptedIds: const {}, rejectedIds: const {}),
StopState.skipped,
);
expect(
stopStateOf(s1, acceptedIds: const {}, rejectedIds: const {}),
StopState.pending,
);
});
test('classifies states for the UI', () {
expect(StopState.done.isResolved, isTrue);
// Rejected is resolved: the rider has nothing left to do with it, so it
// must not strand the trip below 100%.
expect(StopState.rejected.isResolved, isTrue);
expect(StopState.accepted.isResolved, isFalse);
expect(StopState.accepted.isCommitted, isTrue);
expect(StopState.pending.needsDecision, isTrue);
});
});
group('Trip — completion and partial accepts', () {
final stops = [
stop(id: 'a', step: 1),
stop(id: 'b', step: 2),
stop(id: 'c', step: 3),
stop(id: 'd', step: 4),
];
final trip = Trip.fromStops(id: 't', stops: stops);
test('a fresh trip is 0% and all stops need a decision', () {
expect(
trip.completionPercent(acceptedIds: const {}, rejectedIds: const {}),
0,
);
expect(
trip.pendingOrderIds(acceptedIds: const {}, rejectedIds: const {}),
['a', 'b', 'c', 'd'],
);
});
test('accepting stops does NOT advance completion — work does', () {
// Accepting is a commitment, not progress. If accepting moved the ring
// the rider would see 100% before riding a metre.
expect(
trip.completionPercent(
acceptedIds: {'a', 'b', 'c', 'd'}, rejectedIds: const {}),
0,
);
});
test('the rider can take 1, 2 and 4 and decline 3', () {
final accepted = {'a', 'b', 'd'};
const rejected = {'c'};
expect(
trip.pendingOrderIds(acceptedIds: accepted, rejectedIds: rejected),
isEmpty,
);
expect(
trip.hasPendingStops(acceptedIds: accepted, rejectedIds: rejected),
isFalse,
);
// The rejected stop counts as resolved → 1 of 4 = 25%.
expect(
trip.completionPercent(acceptedIds: accepted, rejectedIds: rejected),
25,
);
});
test('accept-all only targets stops still awaiting a decision', () {
expect(
trip.pendingOrderIds(acceptedIds: {'a'}, rejectedIds: {'b'}),
['c', 'd'],
);
});
test('completed work fills the ring', () {
final worked = Trip.fromStops(id: 't', stops: [
{...stops[0], 'orderstatus': 'picked'},
{...stops[1], 'orderstatus': 'picked'},
stops[2],
stops[3],
]);
expect(
worked.completionPercent(
acceptedIds: const {}, rejectedIds: const {}),
50,
);
expect(
worked.isFinished(acceptedIds: const {}, rejectedIds: const {}),
isFalse,
);
});
test('a trip finishes when every stop is done or rejected', () {
final worked = Trip.fromStops(id: 't', stops: [
{...stops[0], 'orderstatus': 'picked'},
{...stops[1], 'orderstatus': 'picked'},
{...stops[2], 'orderstatus': 'picked'},
stops[3],
]);
expect(
worked.completionPercent(acceptedIds: const {}, rejectedIds: {'d'}),
100,
);
expect(
worked.isFinished(acceptedIds: const {}, rejectedIds: {'d'}),
isTrue,
);
});
test('an empty trip is never reported as finished', () {
final empty = Trip.fromStops(id: 'e', stops: const []);
expect(empty.isFinished(acceptedIds: const {}, rejectedIds: const {}),
isFalse);
expect(
empty.completionPercent(
acceptedIds: const {}, rejectedIds: const {}),
0);
});
test('tab labels are 1-based', () {
expect(Trip.tabLabel(0), 'Trip 1');
expect(Trip.tabLabel(2), 'Trip 3');
});
});
group('StopState — decision precedence', () {
final s = stop(id: 'a', step: 1);
test('a local un-reject beats a stale server "rejected"', () {
// The rider tapped "Accept instead"; the next poll still says rejected.
// The local decision is strictly newer and must win, or the undo would
// silently bounce back.
expect(
stopStateOf({...s, 'orderstatus': 'rejected'},
acceptedIds: {'a'}, rejectedIds: const {}),
StopState.accepted,
);
});
test('a local reject beats a stale server "accepted"', () {
expect(
stopStateOf({...s, 'orderstatus': 'accepted'},
acceptedIds: const {}, rejectedIds: {'a'}),
StopState.rejected,
);
});
test('collected work cannot be un-decided locally', () {
// You cannot un-collect a parcel. The terminal server state wins over
// any local tap.
expect(
stopStateOf({...s, 'orderstatus': 'picked'},
acceptedIds: const {}, rejectedIds: {'a'}),
StopState.done,
);
expect(
stopStateOf({...s, 'orderstatus': 'cancelled'},
acceptedIds: {'a'}, rejectedIds: const {}),
StopState.done,
);
});
test('a stop being worked right now outranks a local decision', () {
expect(
stopStateOf({...s, 'orderstatus': 'arrived'},
acceptedIds: const {}, rejectedIds: {'a'}),
StopState.active,
);
});
test('falls back to the server view with no local decision', () {
expect(
stopStateOf({...s, 'orderstatus': 'rejected'},
acceptedIds: const {}, rejectedIds: const {}),
StopState.rejected,
);
expect(
stopStateOf({...s, 'orderstatus': 'accepted'},
acceptedIds: const {}, rejectedIds: const {}),
StopState.accepted,
);
});
});
group('Demo trips fixture', () {
test('groups into three trips covering every stop type', () {
final trips = Trip.groupIntoTrips(getDemoTripStops());
expect(trips.length, 3);
final kinds = {
for (final t in trips)
for (final s in t.stops) stopKindOf(s),
};
expect(kinds, containsAll(StopKind.values));
});
test('trip 1 covers every state the card can render', () {
final trips = Trip.groupIntoTrips(getDemoTripStops());
final states = trips.first
.states(acceptedIds: const {}, rejectedIds: const {});
// done + active exercise the locked rows; skipped the amber state;
// pending the accept/reject buttons.
expect(states, contains(StopState.done));
expect(states, contains(StopState.active));
expect(states, contains(StopState.skipped));
expect(states, contains(StopState.pending));
// Part-worked, so the ring is neither empty nor full.
final percent = trips.first
.completionPercent(acceptedIds: const {}, rejectedIds: const {});
expect(percent, greaterThan(0));
expect(percent, lessThan(100));
});
test('every stop type appears in more than one trip', () {
final trips = Trip.groupIntoTrips(getDemoTripStops());
for (final kind in StopKind.values) {
final tripsWithKind = trips
.where((t) => t.stops.any((s) => stopKindOf(s) == kind))
.length;
expect(tripsWithKind, greaterThan(1), reason: 'only $tripsWithKind trip(s) have $kind');
}
});
test('deliveries and combined stops are well represented', () {
final stops = getDemoTripStops();
int countOf(StopKind k) =>
stops.where((s) => stopKindOf(s) == k).length;
// The card's delivery and combined branches were the thinnest on data,
// so the fixture deliberately over-weights them.
expect(countOf(StopKind.delivery), greaterThanOrEqualTo(6));
expect(countOf(StopKind.combined), greaterThanOrEqualTo(6));
expect(countOf(StopKind.pickup), greaterThanOrEqualTo(3));
});
test('a finished DELIVERY exists, not just a finished pickup', () {
final done = getDemoTripStops().where((s) =>
stopStateOf(s, acceptedIds: const {}, rejectedIds: const {}) ==
StopState.done);
expect(done.any((s) => stopKindOf(s) == StopKind.delivery), isTrue);
expect(done.any((s) => stopKindOf(s) == StopKind.pickup), isTrue);
});
test('a combined stop carries cash on its pickup leg', () {
final combinedWithCash = getDemoTripStops().where((s) =>
stopKindOf(s) == StopKind.combined && stopCollectionAmount(s) > 0);
expect(combinedWithCash, isNotEmpty);
});
test('multi-parcel deliveries exist for the service-time estimate', () {
final multi = getDemoTripStops().where((s) =>
stopKindOf(s) == StopKind.delivery && deliveryParcelCount(s) > 2);
expect(multi, isNotEmpty);
});
test('trips 2 and 3 are entirely undecided', () {
final trips = Trip.groupIntoTrips(getDemoTripStops());
for (final t in trips.skip(1)) {
expect(
t.pendingOrderIds(acceptedIds: const {}, rejectedIds: const {})
.length,
t.stopCount,
);
}
});
test('demo stops carry the fields the card renders', () {
for (final s in getDemoTripStops()) {
expect((s['orderid'] as String).isNotEmpty, isTrue);
expect((s['pickupcustomer'] as String).isNotEmpty, isTrue);
expect((s['pickupaddress'] as String).isNotEmpty, isTrue);
expect(double.tryParse(s['pickuplat'] as String), isNotNull);
expect(DateTime.tryParse(s['expected_pickup_time'] as String),
isNotNull);
}
});
test('a combined demo stop splits deliver and collect counts', () {
final combined = getDemoTripStops()
.firstWhere((s) => stopKindOf(s) == StopKind.combined);
expect(deliveryParcelCount(combined), greaterThan(0));
expect(pickupParcelCount(combined), greaterThan(0));
});
});
group('Trip tabs — always three slots', () {
test('a one-trip day still fills three tab slots', () {
final trips = Trip.groupIntoTrips([
stop(id: 'a', due: DateTime(2026, 7, 27, 10, 0)),
]);
expect(trips.length, 1);
// Trip 2 and Trip 3 exist as tabs; indices past the end are legitimate
// "not assigned yet" slots, not errors.
const maxInline = 3;
final tabCount = trips.length > maxInline ? trips.length : maxInline;
expect(tabCount, 3);
expect(2.clamp(0, tabCount - 1), 2);
});
test('an unusual day folds into three trips without losing a stop', () {
// Bucketing is time-based, so a stop booked well outside the others — or
// simply a long day — used to produce a fourth, fifth or eighth tab.
// "Trip 8" is not a thing the hub assigns.
final stops = [
for (var h = 0; h < 24; h += 3)
stop(id: 'h$h', due: DateTime(2026, 7, 27, h, 30)),
];
final trips = Trip.groupIntoTrips(stops);
expect(trips.length, Trip.maxTripsPerDay,
reason: 'a rider is assigned three slots, not eight');
// The part that matters. Truncating the list would be the one-line fix and
// it would silently hide real work: these are accepted bookings, and a
// stop the rider cannot see is a stop he cannot deliver.
final carried = trips.expand((t) => t.stops).length;
expect(carried, stops.length,
reason: 'every stop must survive the merge — $carried of '
'${stops.length} came through');
final ids = trips
.expand((t) => t.stops)
.map((s) => s['orderid'])
.toSet();
expect(ids.length, stops.length, reason: 'and none may be duplicated');
// The tail lands in the last trip, whose window widens to cover what it
// now holds, so the slot label stays honest.
expect(trips.last.stops.length, greaterThan(1));
expect(trips.last.slotStart!.isBefore(trips.last.slotEnd!), isTrue);
});
test('a normal day is left exactly as it is', () {
final trips = Trip.groupIntoTrips([
stop(id: 'a', due: DateTime(2026, 7, 27, 9, 30)),
stop(id: 'b', due: DateTime(2026, 7, 27, 13, 30)),
]);
expect(trips.length, 2, reason: 'the cap only bites past three');
});
});
group('Address compression', () {
test('drops the tail every stop on the trip shares', () {
const a = '5/2, North Avenue, Vadavalli, Coimbatore, Tamil Nadu, 641041';
const b = '78, Cross Cut Road, Gandhipuram, Coimbatore, Tamil Nadu, 641012';
// Only the pincode differs, so "Coimbatore, Tamil Nadu" is redundant.
final tail = commonAddressTail(const [a, b]);
expect(tail, isEmpty); // pincodes differ → nothing shared at the very end
});
test('strips a fully shared tail', () {
const a = '5/2, North Avenue, Vadavalli, Coimbatore, Tamil Nadu';
const b = '78, Cross Cut Road, Gandhipuram, Coimbatore, Tamil Nadu';
final tail = commonAddressTail(const [a, b]);
expect(tail, ['Coimbatore', 'Tamil Nadu']);
expect(stripAddressTail(a, tail), '5/2, North Avenue, Vadavalli');
expect(stripAddressTail(b, tail), '78, Cross Cut Road, Gandhipuram');
});
test('is a no-op when nothing is shared', () {
const a = '5/2, North Avenue, Vadavalli';
const b = '78, Cross Cut Road, Chennai';
expect(commonAddressTail(const [a, b]), isEmpty);
expect(stripAddressTail(a, const []), a);
});
test('is a no-op on a one-stop trip', () {
expect(commonAddressTail(const ['5/2, North Avenue, Coimbatore']),
isEmpty);
});
test('never strips an address down to nothing', () {
// Two identical addresses share everything; one component must survive.
const a = 'Coimbatore';
final tail = commonAddressTail(const [a, a]);
expect(stripAddressTail(a, tail).isNotEmpty, isTrue);
});
test('ignores case when matching the tail', () {
const a = '5/2, North Avenue, COIMBATORE';
const b = '78, Cross Cut Road, coimbatore';
final tail = commonAddressTail(const [a, b]);
expect(tail.length, 1);
expect(stripAddressTail(a, tail), '5/2, North Avenue');
});
test('shortens the real demo trip addresses', () {
final trips = Trip.groupIntoTrips(getDemoTripStops());
for (final t in trips) {
final tail = t.sharedAddressTail;
// Every demo address ends in "Coimbatore".
expect(tail, ['Coimbatore']);
for (final stop in t.stops) {
final short = t.shortAddress(stop, tail);
expect(short.contains('Coimbatore'), isFalse);
expect(short.isNotEmpty, isTrue);
}
}
});
});
}