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/route_brief.dart'; void main() { // Two stops roughly 2.5 km and 3.5 km from a Coimbatore origin. final stops = >{ 'A': { 'Quantity': '3', 'weight': '4.5', 'collectionamt': '250', 'pickuplat': '11.0243', 'pickuplon': '76.9558', 'kms': '2.1', }, 'B': { 'Quantity': '8', 'weight': '2', 'collectionamt': '0', 'pickuplat': '11.0312', 'pickuplon': '76.9678', 'kms': '1.5', }, }; group('RouteBrief load & capacity', () { test('sums parcels, weight and cash across the route', () { final brief = RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '09:00:00', shiftEnd: '18:00:00', ); expect(brief.stops, 2); expect(brief.parcels, 11); expect(brief.weightKg, 6.5); expect(brief.collectAmount, 250); }); test('counts accepted stops that already left the pending list', () { final brief = RouteBrief.from( orderIds: const ['A'], ordersMap: stops, shiftStart: '', shiftEnd: '', extraStops: [stops['B']!], ); // Same totals as if both were still pending — an accepted stop is still // a stop the rider has to ride to. expect(brief.stops, 2); expect(brief.parcels, 11); expect(brief.weightKg, 6.5); }); test('strips units baked into numeric strings', () { final brief = RouteBrief.from( orderIds: const ['A'], ordersMap: { 'A': {'Quantity': '2', 'weight': '3.25 kg', 'collectionamt': '₹180'}, }, shiftStart: '', shiftEnd: '', ); expect(brief.weightKg, 3.25); expect(brief.collectAmount, 180); }); }); group('RouteBrief round-trip distance', () { test('closes the loop back to the origin', () { final withOrigin = RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '', shiftEnd: '', riderLat: 11.0180, riderLng: 76.9580, ); final withoutOrigin = RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '', shiftEnd: '', ); // Origin → A → B → origin must be longer than the bare A → B leg. expect(withOrigin.routeKm, greaterThan(withoutOrigin.routeKm)); expect(withOrigin.routeKm, greaterThan(4)); expect(withOrigin.routeKm, lessThan(15)); }); test('falls back to the per-stop kms field when coordinates are missing', () { final brief = RouteBrief.from( orderIds: const ['A'], ordersMap: { 'A': {'Quantity': '1', 'kms': '4.2'}, }, shiftStart: '', shiftEnd: '', ); expect(brief.routeKm, 4.2); }); }); group('RouteBrief shift window', () { test('flags a missing shift', () { final brief = RouteBrief.from( orderIds: const [], ordersMap: const {}, shiftStart: '', shiftEnd: '', ); expect(brief.shiftSet, isFalse); expect(brief.timeLeft, isNull); expect(brief.shiftWindowLabel, 'Shift not set'); }); test('flags a half-set shift as not set', () { final brief = RouteBrief.from( orderIds: const [], ordersMap: const {}, shiftStart: '09:00:00', shiftEnd: '', ); expect(brief.shiftSet, isFalse); }); test('computes time left and a per-stop budget', () { final now = DateTime(2026, 7, 27, 15, 0); final brief = RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '09:00:00', shiftEnd: '18:00:00', now: now, ); expect(brief.shiftSet, isTrue); expect(brief.timeLeft, const Duration(hours: 3)); expect(brief.perStopBudget, const Duration(minutes: 90)); expect(brief.isPaceAtRisk, isFalse); expect(brief.shiftWindowLabel, '9:00 AM – 6:00 PM'); }); test('clamps a finished shift to zero instead of going negative', () { final brief = RouteBrief.from( orderIds: const [], ordersMap: const {}, shiftStart: '09:00:00', shiftEnd: '18:00:00', now: DateTime(2026, 7, 27, 20, 0), ); expect(brief.timeLeft, Duration.zero); expect(brief.perStopBudget, isNull); }); test('rolls an overnight shift end into the next day', () { final brief = RouteBrief.from( orderIds: const [], ordersMap: const {}, shiftStart: '22:00:00', shiftEnd: '06:00:00', now: DateTime(2026, 7, 27, 23, 0), ); expect(brief.timeLeft, const Duration(hours: 7)); }); test('raises the pace flag when stops cannot fit in the time left', () { final brief = RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '09:00:00', shiftEnd: '18:00:00', now: DateTime(2026, 7, 27, 17, 51), // 9 minutes for 2 stops ); expect(brief.isPaceAtRisk, isTrue); }); test('parses 12-hour shift strings', () { final brief = RouteBrief.from( orderIds: const [], ordersMap: const {}, shiftStart: '9:30 AM', shiftEnd: '6:00 PM', now: DateTime(2026, 7, 27, 12, 0), ); expect(brief.shiftSet, isTrue); expect(brief.timeLeft, const Duration(hours: 6)); }); }); /// A missing shift is somebody else's admin: the rider cannot fix it, did not /// cause it, and is not blocked by it — the trips still load and he can still /// ride them. So the banner says nothing at all rather than parking a /// permanent amber "No shift set for today" box at the top of his first /// screen. These tests keep it that way, because a box like that comes back /// easily and always with good intentions. group('ShiftBanner', () { Future pump(WidgetTester tester, RouteBrief brief) async { await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold(body: ShiftBanner(brief: brief)), ), ), ); await tester.pumpAndSettle(); } testWidgets('renders nothing at all when no shift is set', (tester) async { await pump( tester, RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '', shiftEnd: '', ), ); expect(tester.takeException(), isNull); expect(find.text('No shift set for today'), findsNothing); expect(find.text('Retry'), findsNothing); expect(find.textContaining('hub manager'), findsNothing); // Not merely empty of those strings — the widget takes no height, so the // route below it starts at the top of the page. expect(tester.getSize(find.byType(ShiftBanner)).height, 0); }); testWidgets('still shows the window once the hub sets one', (tester) async { await pump( tester, RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '09:00:00', shiftEnd: '18:00:00', now: DateTime(2026, 7, 27, 15, 0), ), ); expect(tester.takeException(), isNull); expect(find.text('3h left'), findsOneWidget); expect(tester.getSize(find.byType(ShiftBanner)).height, greaterThan(0)); }); }); group('RouteBrief time windows', () { final now = DateTime(2026, 7, 27, 15, 0); test('counts stops past their slot and stops closing soon', () { final brief = RouteBrief.from( orderIds: const ['late', 'soon', 'later'], ordersMap: { 'late': { 'expected_pickup_time': now.subtract(const Duration(minutes: 20)).toIso8601String(), }, 'soon': { 'expected_pickup_time': now.add(const Duration(minutes: 10)).toIso8601String(), }, 'later': { 'expected_pickup_time': now.add(const Duration(hours: 3)).toIso8601String(), }, }, shiftStart: '09:00:00', shiftEnd: '18:00:00', now: now, ); expect(brief.overdueStops, 1); expect(brief.dueSoonStops, 1); }); test('ignores stops with no slot time', () { final brief = RouteBrief.from( orderIds: const ['A', 'B'], ordersMap: stops, shiftStart: '09:00:00', shiftEnd: '18:00:00', now: now, ); expect(brief.overdueStops, 0); expect(brief.dueSoonStops, 0); }); }); group('formatDuration', () { test('reads like a clock', () { expect(formatDuration(const Duration(minutes: 45)), '45m'); expect(formatDuration(const Duration(hours: 2, minutes: 15)), '2h 15m'); expect(formatDuration(const Duration(hours: 3)), '3h'); expect(formatDuration(const Duration(seconds: 20)), 'under a minute'); }); }); }