first commit

This commit is contained in:
2026-08-11 13:16:33 +05:30
parent 18531bf46b
commit c8350563d9
388 changed files with 69656 additions and 0 deletions

745
test/trip_test.dart Normal file
View File

@@ -0,0 +1,745 @@
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);
}
}
});
});
}