import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:fuel_tax_tracker/models/fuel_entry.dart'; import 'package:fuel_tax_tracker/models/vehicle.dart'; import 'package:fuel_tax_tracker/screens/report_screen.dart'; import 'package:fuel_tax_tracker/services/app_state.dart'; /// Covers the per-vehicle selection cards on the Reports tab: with a date /// range in place (now defaulted automatically — see /// defaultReportDateRange/ReportScreen.initState), every vehicle with a /// receipted entry in range starts selected (colored like the "Share" /// button), tapping one to deselect it /// drops it from the total (and, by extension, the generated report) and /// reverts its coloring to a plain unselected card, and deselecting every /// vehicle shows a distinct "nothing selected" message rather than "no /// entries in range". There's deliberately no checkbox — selection state is /// conveyed by color alone. void main() { final vehicleA = Vehicle(id: 'a', vin: 'VINAAAAAAAAAAAAAA', nickname: 'Truck', updatedAt: DateTime.utc(2026, 1, 1)); final vehicleB = Vehicle(id: 'b', vin: 'VINBBBBBBBBBBBBBB', nickname: 'Van', updatedAt: DateTime.utc(2026, 1, 1)); // ReportScreen's date range now defaults to the current fuel-tax-refund // period (see defaultReportDateRange) as soon as it opens — dating these // entries to "now" means they always fall inside that default range, // without these tests needing to drive the date pickers themselves. final now = DateTime.now(); FuelEntry entryFor(String vehicleId, String id, double gallons, double cost) => FuelEntry( id: id, vehicleId: vehicleId, date: now, gallons: gallons, pricePerGallon: cost / gallons, totalCost: cost, updatedAt: now.toUtc(), receiptDriveFileId: 'remote-$id', ); Future pumpReport(WidgetTester tester, {bool showEstimatedFuelRefund = false}) async { final appState = AppState() ..isLoading = false ..vehicles = [vehicleA, vehicleB] ..fuelEntries = [ entryFor('a', 'e1', 10, 35), entryFor('b', 'e2', 20, 70), ] ..showEstimatedFuelRefund = showEstimatedFuelRefund; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: ReportScreen()), ), ); return appState; } Card cardFor(WidgetTester tester, String vehicleLabel) => tester.widget( find.ancestor(of: find.text(vehicleLabel), matching: find.byType(Card)), ); testWidgets('both vehicles start colored as selected once a range with entries for both is picked', (tester) async { await pumpReport(tester); final colorScheme = Theme.of(tester.element(find.text('Truck'))).colorScheme; expect(cardFor(tester, 'Truck').color, colorScheme.primary); expect(cardFor(tester, 'Van').color, colorScheme.primary); expect(find.text('30.000 gal · \$105.00'), findsOneWidget, reason: 'combined total'); }); testWidgets( 'shows an estimated fuel refund below each vehicle\'s total cost and next to the ' 'combined total, when the setting is on', (tester) async { await pumpReport(tester, showEstimatedFuelRefund: true); // Truck: 10 gal * $0.125/gal = $1.25. Van: 20 gal * $0.125/gal = $2.50. expect(find.text('Est. \$1.25'), findsOneWidget); expect(find.text('Est. \$2.50'), findsOneWidget); // Combined: 30 gal * $0.125/gal = $3.75, folded into the Total row. expect(find.text('30.000 gal · \$105.00 · Est. \$3.75'), findsOneWidget); }); testWidgets('shows no estimated fuel refund anywhere when the setting is off (default)', (tester) async { await pumpReport(tester); expect(find.textContaining('Est. \$'), findsNothing); }); testWidgets('tapping a selected vehicle deselects it (reverts its coloring) and drops it from the total', (tester) async { await pumpReport(tester); final colorScheme = Theme.of(tester.element(find.text('Truck'))).colorScheme; await tester.tap(find.text('Truck')); await tester.pumpAndSettle(); expect(find.text('20.000 gal · \$70.00'), findsOneWidget, reason: "only Van's total remains"); expect(cardFor(tester, 'Truck').color, isNot(colorScheme.primary), reason: 'deselected'); expect(cardFor(tester, 'Van').color, colorScheme.primary, reason: 'still selected'); }); testWidgets('tapping a deselected vehicle again reselects it (restores its coloring)', (tester) async { await pumpReport(tester); final colorScheme = Theme.of(tester.element(find.text('Truck'))).colorScheme; await tester.tap(find.text('Truck')); await tester.pumpAndSettle(); await tester.tap(find.text('Truck')); await tester.pumpAndSettle(); expect(cardFor(tester, 'Truck').color, colorScheme.primary); expect(find.text('30.000 gal · \$105.00'), findsOneWidget, reason: 'both counted again'); }); testWidgets('tapping Preview opens the report preview screen', (tester) async { await pumpReport(tester); await tester.scrollUntilVisible(find.text('Preview'), 300, scrollable: find.byType(Scrollable)); await tester.tap(find.text('Preview')); // Not pumpAndSettle: PdfPreview's own internal rendering isn't under // test here (no platform channel mocked for it) — just that Preview // actually navigates. A couple of bounded pumps are enough for that // navigation (a synchronous PDF build, no plugin calls) to land. await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); expect(find.text('Report Preview'), findsOneWidget); }); testWidgets('deselecting every vehicle shows a distinct "nothing selected" message', (tester) async { await pumpReport(tester); await tester.tap(find.text('Truck')); await tester.pumpAndSettle(); await tester.tap(find.text('Van')); await tester.pumpAndSettle(); expect( find.text('No vehicles selected — tap at least one above to build a report.'), findsOneWidget, ); expect(find.text('Preview'), findsNothing); expect(find.text('Share'), findsNothing); expect(find.text('Print'), findsNothing); // The cards themselves stay visible so the user can tap one again. expect(find.text('Truck'), findsOneWidget); expect(find.text('Van'), findsOneWidget); }); }