MO-Fuel-Tax-Back/test/receipts_screen_test.dart

260 lines
8.5 KiB
Dart

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/receipt_detail_screen.dart';
import 'package:fuel_tax_tracker/screens/receipts_screen.dart';
import 'package:fuel_tax_tracker/services/app_state.dart';
void main() {
final vehicle = Vehicle(
id: 'v1',
vin: '1FMPU18L1TLB51349',
nickname: 'Red Truck',
updatedAt: DateTime.utc(2026, 1, 1),
);
testWidgets('shows an empty state with no fuel entries', (tester) async {
final appState = AppState()
..isLoading = false
..vehicles = [vehicle];
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: appState,
child: const MaterialApp(home: ReceiptsScreen()),
),
);
expect(find.textContaining('No fuel entries'), findsOneWidget);
});
testWidgets('shows the hero total and a receipt row with date/amount/vehicle chip',
(tester) async {
final entry = FuelEntry(
id: 'e1',
vehicleId: 'v1',
date: DateTime(2026, 5, 14),
gallons: 10.523,
pricePerGallon: 3.5,
totalCost: 32.48,
updatedAt: DateTime.utc(2026, 5, 14),
receiptDriveFileId: 'remote-1',
);
final appState = AppState()
..isLoading = false
..vehicles = [vehicle]
..fuelEntries = [entry];
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: appState,
child: const MaterialApp(home: ReceiptsScreen()),
),
);
// Hero total sums totalCost across all entries — just the one here.
expect(find.text('\$32.48'), findsNWidgets(2)); // hero total + row amount
expect(find.text('Total Fuel Cost'), findsOneWidget, reason: 'no year prefix');
expect(find.text('Total Gallons'), findsOneWidget);
expect(find.text('11'), findsOneWidget, reason: 'hero total gallons, rounded to whole gallons');
expect(find.text('May 14, 2026'), findsOneWidget);
expect(find.text('10.523 gal'), findsOneWidget, reason: 'row subtitle');
expect(find.text('Red Truck'), findsOneWidget);
});
testWidgets('tapping a receipted row opens the receipt detail page', (tester) async {
final entry = FuelEntry(
id: 'e1',
vehicleId: 'v1',
date: DateTime(2026, 5, 14),
gallons: 10,
pricePerGallon: 3.5,
totalCost: 35,
updatedAt: DateTime.utc(2026, 5, 14),
receiptDriveFileId: 'remote-1',
);
final appState = AppState()
..isLoading = false
..vehicles = [vehicle]
..fuelEntries = [entry];
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: appState,
child: const MaterialApp(home: ReceiptsScreen()),
),
);
await tester.tap(find.text('May 14, 2026'));
await tester.pumpAndSettle();
expect(find.byType(ReceiptDetailScreen), findsOneWidget);
});
testWidgets('tapping a row with no receipt photo still opens the detail page', (tester) async {
final entry = FuelEntry(
id: 'e1',
vehicleId: 'v1',
date: DateTime(2026, 5, 14),
gallons: 10,
pricePerGallon: 3.5,
totalCost: 35,
updatedAt: DateTime.utc(2026, 5, 14),
);
final appState = AppState()
..isLoading = false
..vehicles = [vehicle]
..fuelEntries = [entry];
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: appState,
child: const MaterialApp(home: ReceiptsScreen()),
),
);
await tester.tap(find.text('May 14, 2026'));
await tester.pumpAndSettle();
expect(find.byType(ReceiptDetailScreen), findsOneWidget,
reason: 'the data is still viewable/editable even without a photo');
});
testWidgets('formats large gallon totals with thousands separators', (tester) async {
final entry = FuelEntry(
id: 'e1',
vehicleId: 'v1',
date: DateTime(2026, 8, 5),
gallons: 1234.5678,
pricePerGallon: 3.5,
totalCost: 4320.99,
updatedAt: DateTime.utc(2026, 8, 5),
receiptDriveFileId: 'remote-1',
);
final appState = AppState()
..isLoading = false
..vehicles = [vehicle]
..fuelEntries = [entry];
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: appState,
child: const MaterialApp(home: ReceiptsScreen()),
),
);
expect(find.text('1,235'), findsOneWidget, reason: 'hero total gallons, rounded to whole gallons');
expect(find.text('1,234.568 gal'), findsOneWidget, reason: 'row subtitle keeps full precision');
});
testWidgets('date range selector filters totals and the list, and recolors the active label',
(tester) async {
// Aug 5 2026: within Current Month, Last 3 Months, and Year to Date.
final recentEntry = FuelEntry(
id: 'recent',
vehicleId: 'v1',
date: DateTime(2026, 8, 5),
gallons: 10,
pricePerGallon: 3,
totalCost: 30,
updatedAt: DateTime.utc(2026, 8, 5),
receiptDriveFileId: 'r1',
);
// Jul 1 2026: within Last 3 Months and Year to Date, not Current Month.
final midEntry = FuelEntry(
id: 'mid',
vehicleId: 'v1',
date: DateTime(2026, 7, 1),
gallons: 20,
pricePerGallon: 3,
totalCost: 60,
updatedAt: DateTime.utc(2026, 7, 1),
receiptDriveFileId: 'r2',
);
// Mar 1 2026: within Year to Date only.
final oldEntry = FuelEntry(
id: 'old',
vehicleId: 'v1',
date: DateTime(2026, 3, 1),
gallons: 40,
pricePerGallon: 3,
totalCost: 120,
updatedAt: DateTime.utc(2026, 3, 1),
receiptDriveFileId: 'r3',
);
final appState = AppState()
..isLoading = false
..vehicles = [vehicle]
..fuelEntries = [recentEntry, midEntry, oldEntry];
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: appState,
child: const MaterialApp(home: ReceiptsScreen()),
),
);
bool isBold(String label) => tester
.widgetList<Text>(find.text(label))
.any((t) => t.style?.fontWeight == FontWeight.w800);
// Defaults to Year to Date: all three visible.
expect(find.text('Aug 5, 2026'), findsOneWidget);
expect(find.text('Jul 1, 2026'), findsOneWidget);
expect(find.text('Mar 1, 2026'), findsOneWidget);
expect(isBold('Year to Date'), isTrue, reason: 'selected by default');
await tester.tap(find.text('Last 3 Months'));
await tester.pumpAndSettle();
expect(find.text('Aug 5, 2026'), findsOneWidget);
expect(find.text('Jul 1, 2026'), findsOneWidget);
expect(find.text('Mar 1, 2026'), findsNothing, reason: 'older than 3 months back');
expect(isBold('Last 3 Months'), isTrue);
expect(isBold('Year to Date'), isFalse, reason: 'no longer selected');
await tester.tap(find.text('Current Month'));
await tester.pumpAndSettle();
expect(find.text('Aug 5, 2026'), findsOneWidget);
expect(find.text('Jul 1, 2026'), findsNothing, reason: 'not in the current month');
expect(find.text('Mar 1, 2026'), findsNothing);
});
testWidgets(
'tapping + with no vehicles opens vehicle creation and prompts to create one for the receipt',
(tester) async {
final appState = AppState()..isLoading = false;
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: appState,
child: const MaterialApp(home: ReceiptsScreen()),
),
);
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
expect(find.widgetWithText(AppBar, 'Add Vehicle'), findsOneWidget);
expect(find.text('Create a vehicle to attach receipt'), findsOneWidget);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(find.text('Create a vehicle to attach receipt'), findsNothing);
expect(find.widgetWithText(AppBar, 'Add Vehicle'), findsOneWidget, reason: 'still on the form');
});
// Not covered here: that saving the new vehicle continues into receipt
// capture (rather than back to an empty Receipts tab). Exercising that
// needs AddEditVehicleScreen's real _save() to run, which touches a real
// database — and combining a real sqflite_common_ffi database with
// testWidgets (as opposed to a plain test()) reliably deadlocks in this
// project's test environment (confirmed directly: the identical
// `DatabaseService().init()` call returns immediately under test(), but
// times out under testWidgets()). Verified manually on-device instead.
}