157 lines
5.7 KiB
Dart
157 lines
5.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|
|
|
import 'package:fuel_tax_tracker/models/fuel_entry.dart';
|
|
import 'package:fuel_tax_tracker/models/vehicle.dart';
|
|
import 'package:fuel_tax_tracker/screens/edit_fuel_entry_screen.dart';
|
|
import 'package:fuel_tax_tracker/screens/receipt_detail_screen.dart';
|
|
import 'package:fuel_tax_tracker/screens/vehicle_detail_screen.dart';
|
|
import 'package:fuel_tax_tracker/services/app_state.dart';
|
|
import 'package:fuel_tax_tracker/services/database_service.dart';
|
|
|
|
/// Covers editing a fuel entry's logged data (date/gallons/price/total) —
|
|
/// not its receipt photo, which [EditFuelEntryScreen] deliberately shows
|
|
/// read-only. The database-level overwrite semantics ([DatabaseService]
|
|
/// group) are what [AppState.updateFuelEntry] relies on: it just calls
|
|
/// [DatabaseService.saveFuelEntry] again with the same id, same as any
|
|
/// other upsert in this app (see [DatabaseService.saveVehicle]).
|
|
void main() {
|
|
final vehicle = Vehicle(id: 'v1', vin: '1FMPU18L1TLB51349', updatedAt: DateTime.utc(2026, 1, 1));
|
|
final entry = FuelEntry(
|
|
id: 'e1',
|
|
vehicleId: 'v1',
|
|
date: DateTime(2026, 3, 5, 14, 30),
|
|
gallons: 10.5,
|
|
pricePerGallon: 3.499,
|
|
totalCost: 36.74,
|
|
updatedAt: DateTime.utc(2026, 3, 5),
|
|
);
|
|
|
|
group('DatabaseService.saveFuelEntry overwrite semantics', () {
|
|
late Directory tempDir;
|
|
late DatabaseService database;
|
|
|
|
setUpAll(() {
|
|
sqfliteFfiInit();
|
|
databaseFactory = databaseFactoryFfi;
|
|
});
|
|
|
|
setUp(() async {
|
|
tempDir = await Directory.systemTemp.createTemp('edit_fuel_entry_test_');
|
|
PathProviderPlatform.instance = _FakePathProviderPlatform(tempDir.path);
|
|
database = DatabaseService();
|
|
await database.init();
|
|
});
|
|
|
|
tearDown(() async {
|
|
await database.rawDb.close();
|
|
await tempDir.delete(recursive: true);
|
|
});
|
|
|
|
test('saving again with the same id overwrites gallons/price/total/date, marking it dirty',
|
|
() async {
|
|
await database.saveVehicle(vehicle);
|
|
await database.saveFuelEntry(entry);
|
|
// Clear the dirty flag the way a completed sync would, so the
|
|
// overwrite below can prove it gets re-marked dirty.
|
|
await database.rawDb.update('fuel_entries', {'dirty': 0}, where: 'id = ?', whereArgs: ['e1']);
|
|
|
|
await database.saveFuelEntry(FuelEntry(
|
|
id: 'e1',
|
|
vehicleId: 'v1',
|
|
date: DateTime(2026, 3, 6, 9, 0),
|
|
gallons: 11.2,
|
|
pricePerGallon: 3.599,
|
|
totalCost: 40.31,
|
|
updatedAt: DateTime.utc(2026, 3, 6),
|
|
));
|
|
|
|
final rows = await database.getFuelEntries();
|
|
expect(rows, hasLength(1), reason: 'overwritten, not duplicated');
|
|
final saved = rows.single;
|
|
expect(saved.gallons, 11.2);
|
|
expect(saved.pricePerGallon, 3.599);
|
|
expect(saved.totalCost, 40.31);
|
|
expect(saved.date, DateTime(2026, 3, 6, 9, 0));
|
|
|
|
final rawRow = (await database.rawDb.query('fuel_entries', where: 'id = ?', whereArgs: ['e1'])).single;
|
|
expect(rawRow['dirty'], 1);
|
|
});
|
|
});
|
|
|
|
group('EditFuelEntryScreen', () {
|
|
Future<void> pumpVehicleDetail(WidgetTester tester, AppState appState) async {
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider.value(
|
|
value: appState,
|
|
child: MaterialApp(home: VehicleDetailScreen(vehicleId: vehicle.id)),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets(
|
|
'tapping an entry row opens its receipt detail page, and its edit icon opens '
|
|
'EditFuelEntryScreen pre-filled', (tester) async {
|
|
final appState = AppState()
|
|
..isLoading = false
|
|
..vehicles = [vehicle]
|
|
..fuelEntries = [entry];
|
|
|
|
await pumpVehicleDetail(tester, appState);
|
|
|
|
await tester.tap(find.text('10.500 gal • \$36.74'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(ReceiptDetailScreen), findsOneWidget);
|
|
|
|
await tester.tap(find.byTooltip('Edit fuel entry'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(EditFuelEntryScreen), findsOneWidget);
|
|
expect(find.widgetWithText(TextFormField, '10.500'), findsOneWidget);
|
|
expect(find.widgetWithText(TextFormField, '3.499'), findsOneWidget);
|
|
expect(find.widgetWithText(TextFormField, '36.74'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('clearing a required field shows a validation error instead of saving',
|
|
(tester) async {
|
|
final appState = AppState()
|
|
..isLoading = false
|
|
..vehicles = [vehicle]
|
|
..fuelEntries = [entry];
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider.value(
|
|
value: appState,
|
|
child: MaterialApp(home: EditFuelEntryScreen(entry: entry)),
|
|
),
|
|
);
|
|
|
|
await tester.enterText(find.widgetWithText(TextFormField, '10.500'), '');
|
|
await tester.drag(find.byType(ListView), const Offset(0, -300));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Save Changes'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Required'), findsOneWidget);
|
|
expect(find.byType(EditFuelEntryScreen), findsOneWidget, reason: 'did not navigate away');
|
|
});
|
|
});
|
|
}
|
|
|
|
class _FakePathProviderPlatform extends PathProviderPlatform with MockPlatformInterfaceMixin {
|
|
final String tempPath;
|
|
_FakePathProviderPlatform(this.tempPath);
|
|
|
|
@override
|
|
Future<String?> getApplicationDocumentsPath() async => tempPath;
|
|
|
|
@override
|
|
Future<String?> getTemporaryPath() async => tempPath;
|
|
}
|