@Tags(['shots']) library; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'support/golden_fonts.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/activity/activity_page.dart'; import 'package:miler/views/Dashboard/activity/delivery_details_page.dart'; import 'package:miler/views/helpers/constants/Font_constant.dart'; import 'package:miler/data/accepted_store.dart'; /// Visual harness for the Activity tab. /// /// Unlike Home and Deliveries, [ActivityPage] *is* pumpable — it reads the /// completed/skipped stores out of SharedPreferences, so a mocked store is a /// whole rendered page rather than a component in a frame. Run with /// `--update-goldens`. void main() { setUpAll(() async { await loadGoldenFonts(); }); tearDown(() { Get.reset(); ServiceProfile.setActive(ServiceProfile.parcel); }); String day(DateTime t) => '${t.year}-${t.month.toString().padLeft(2, '0')}-' '${t.day.toString().padLeft(2, '0')}'; // ── The clock has to be pinned, or the goldens rot every minute ── // // These records render absolute times ("5:21 PM"), so fixtures built off a // bare `DateTime.now()` produce a different image on every run — the shots // passed when generated and failed in the next full-suite pass. Today's // *date* is kept so the page still groups them under TODAY / YESTERDAY; // only the time of day is fixed. final _today = DateTime.now(); final now = DateTime(_today.year, _today.month, _today.day, 17, 30); Map finished( String id, String customer, String source, { required String type, double cash = 0, int minsAgo = 30, int legMins = 14, int daysAgo = 0, }) { final at = now .subtract(Duration(days: daysAgo)) .subtract(Duration(minutes: minsAgo)); return { 'orderid': id, 'bookingno': id, 'type': type, 'pickupcustomer': customer, 'name': customer, 'sourcename': source, 'kitchenname': source, 'pickupaddress': '4 Cross, RS Puram, Coimbatore 641002', 'dropaddress': '12, SNS Colony, Peelamedu, Coimbatore 641004', 'deliveryaddress': '12, SNS Colony, Peelamedu, Coimbatore 641004', 'orderstatus': type == 'delivery' ? 'delivered' : 'picked', 'completedday': day(now.subtract(Duration(days: daysAgo))), 'completedat': at.toIso8601String(), 'startedat': at.subtract(Duration(minutes: legMins)).toIso8601String(), 'arrivedat': at.subtract(const Duration(minutes: 3)).toIso8601String(), 'kms': '4.6', if (cash > 0) 'collectionamt': '$cash', 'proof': {if (cash > 0) 'amountcollected': cash}, }; } Future shoot( WidgetTester tester, String name, { required List> completed, List> skipped = const [], ServiceProfile profile = ServiceProfile.milkMan, double width = 390, double height = 844, double textScale = 1.0, }) async { ServiceProfile.setActive(profile); SharedPreferences.setMockInitialValues({'userId': 1, 'userid': 1}); // ── Both stores go through `debugSeedStore` ── // // The skipped set used to be planted as a bare `skipped_bookings` key in // the mock prefs. Both stores are scoped per rider/tenant/line now, so a // bare key is a key nothing reads — the fixture seeded a parked stop and // the page rendered a clean day, which made this harness quietly unable to // photograph the one case it most needs to: a trip with something owed. await debugSeedStore('completed_bookings', jsonEncode(completed)); await debugSeedStore('skipped_bookings', jsonEncode(skipped)); tester.view.physicalSize = Size(width * 3, height * 3); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.reset); await tester.pumpWidget( ScreenUtilInit( designSize: Size(width, height), builder: (_, _) => MaterialApp( debugShowCheckedModeBanner: false, home: MediaQuery( data: MediaQueryData(textScaler: TextScaler.linear(textScale)), child: const ActivityPage(), ), ), ), ); await tester.pumpAndSettle(const Duration(seconds: 2)); await expectLater( find.byType(MaterialApp), matchesGoldenFile('shots/activity_$name.png'), ); } testWidgets('a round s worth of deliveries', (tester) async { await shoot( tester, 'round', completed: [ finished( 'DM2401', 'Joe Mathew', 'Vidhya Kitchen', type: 'delivery', minsAgo: 22, ), finished( 'DM2402', 'Arun Prakash', 'Vidhya Kitchen', type: 'delivery', minsAgo: 51, ), finished( 'DM2403', 'Priya Nair', 'Joe Kitchen', type: 'delivery', minsAgo: 96, cash: 2450, ), finished( 'DM2404', 'Ravi Kumar', 'Joe Kitchen', type: 'delivery', minsAgo: 140, daysAgo: 1, ), ], ); }); testWidgets('nothing done yet', (tester) async { // Structural, not a golden: the empty state draws a looping animation, // and its frame at settle-timeout differs between an isolated run and // the parallel full suite — the one golden in this file that flaked on // scheduling rather than on design. The look is pinned well enough by // MilerEmptyState's own use everywhere; what this run holds is that an // empty store renders the empty state and nothing throws. ServiceProfile.setActive(ServiceProfile.milkMan); SharedPreferences.setMockInitialValues({'userId': 1, 'userid': 1}); await debugSeedStore('completed_bookings', jsonEncode(const [])); tester.view.physicalSize = const Size(390 * 3, 844 * 3); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.reset); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => const MaterialApp(home: ActivityPage()), ), ); await tester.pump(const Duration(seconds: 2)); expect(tester.takeException(), isNull); // The copy is gone — the artwork carries it now. What the shot pins is // that the state renders at all, with its one control. expect(find.textContaining('Go to'), findsWidgets); }); testWidgets('one delivery record', (tester) async { ServiceProfile.setActive(ServiceProfile.milkMan); SharedPreferences.setMockInitialValues({'userId': 1, 'userid': 1}); tester.view.physicalSize = const Size(390 * 3, 1500 * 3); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.reset); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 1500), builder: (_, _) => MaterialApp( debugShowCheckedModeBanner: false, home: DeliveryDetailsPage( stop: finished( 'DM-BK-16DA518D-6381', 'Joe Mathew', 'Vidhya Kitchen', type: 'delivery', minsAgo: 22, cash: 2450, ), ), ), ), ); await tester.pumpAndSettle(const Duration(seconds: 2)); await expectLater( find.byType(MaterialApp), matchesGoldenFile('shots/activity_record.png'), ); }); testWidgets('three trips, one of them with a problem', (tester) async { // ── The two-second test ── // // At arm's length a rider must be able to say "Trip 2 had a problem", and // after opening it, "Arun is the one I have to go back to". This is the // shot that answers whether the page does that. Map at( String id, String who, int hour, { double cash = 0, }) { final t = DateTime(now.year, now.month, now.day, hour, 15); return { ...finished(id, who, 'Vidhya Kitchen', type: 'delivery', cash: cash), 'completedat': t.toIso8601String(), 'expected_pickup_time': t.toIso8601String(), }; } final skipAt = DateTime(now.year, now.month, now.day, 14, 40); await shoot( tester, 'trips', height: 1500, completed: [ at('DM2401', 'Joe Mathew', 9), at('DM2402', 'Sarah', 10), at('DM2403', 'Priya Nair', 14, cash: 2450), at('DM2404', 'Meera', 19), ], skipped: [ { ...finished('DM2405', 'Arun', 'Vidhya Kitchen', type: 'delivery'), 'orderstatus': 'skipped', 'skipreason': 'Customer unavailable', 'skippedat': skipAt.toIso8601String(), 'skippedday': '${skipAt.year}-${skipAt.month.toString().padLeft(2, '0')}-' '${skipAt.day.toString().padLeft(2, '0')}', 'expected_pickup_time': skipAt.toIso8601String(), }, ], ); }); testWidgets('a 320pt phone', (tester) async { await shoot( tester, 'small', width: 320, completed: [ finished( 'DM2401', 'Joe Mathew', 'Vidhya Kitchen', type: 'delivery', minsAgo: 22, ), finished( 'DM2403', 'Priya Nair', 'Joe Kitchen', type: 'delivery', minsAgo: 96, cash: 2450, ), ], ); }); }