246 lines
9.6 KiB
Dart
246 lines
9.6 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:motorcycle_dash/database/database.dart';
|
|
import 'package:motorcycle_dash/models/maintenance_models.dart';
|
|
import 'package:motorcycle_dash/features/garage/garage_providers.dart';
|
|
import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.dart';
|
|
import 'package:motorcycle_dash/services/cloud/firebase_account_sync_service.dart';
|
|
|
|
void main() {
|
|
group('MaintenanceData & Countdown Pill Logic', () {
|
|
test('calculatePills returns empty when no odometer set and no records exist', () {
|
|
const data = MaintenanceData(baseOdometer: 0.0, serviceRecords: []);
|
|
final pills = data.calculatePills(0.0);
|
|
expect(pills, isEmpty, reason: 'Must not synthesize fake countdowns for untracked motorcycles');
|
|
});
|
|
|
|
test('calculatePills calculates correct countdown when base odometer is set', () {
|
|
const data = MaintenanceData(baseOdometer: 1000.0, serviceRecords: []);
|
|
final pills = data.calculatePills(500.0); // current odo = 1500
|
|
|
|
expect(pills.length, 4);
|
|
|
|
final oilPill = pills.firstWhere((p) => p.key == 'oil');
|
|
expect(oilPill.intervalMiles, 3000);
|
|
expect(oilPill.milesRemaining, 2500.0);
|
|
expect(oilPill.urgency, MaintenanceUrgency.ok);
|
|
|
|
final chainPill = pills.firstWhere((p) => p.key == 'chain');
|
|
expect(chainPill.intervalMiles, 600);
|
|
// current odo 1500, last serviced 1000 -> 500 miles ridden -> 600 - 500 = 100 remaining
|
|
expect(chainPill.milesRemaining, 100.0);
|
|
});
|
|
|
|
test('calculatePills marks overdue and due soon appropriately based on last service', () {
|
|
final record = ServiceRecord(
|
|
id: 'srv_1',
|
|
date: DateTime.now().subtract(const Duration(days: 30)),
|
|
odometer: 10000.0,
|
|
serviceType: 'Engine Oil & Filter',
|
|
cost: 45.0,
|
|
);
|
|
|
|
final data = MaintenanceData(
|
|
baseOdometer: 9000.0,
|
|
serviceRecords: [record],
|
|
);
|
|
|
|
// Current odo = 9000 + 3800 = 12800. Oil last serviced at 10000. Miles since service = 2800.
|
|
// Oil interval = 3000. Remaining = 200 mi (<= 350 dueSoonThreshold)
|
|
final pillsDueSoon = data.calculatePills(3800.0);
|
|
final oilDueSoon = pillsDueSoon.firstWhere((p) => p.key == 'oil');
|
|
expect(oilDueSoon.urgency, MaintenanceUrgency.dueSoon);
|
|
expect(oilDueSoon.milesRemaining, 200.0);
|
|
|
|
// Current odo = 9000 + 4200 = 13200. Miles since service = 3200. Remaining = -200 (overdue)
|
|
final pillsOverdue = data.calculatePills(4200.0);
|
|
final oilOverdue = pillsOverdue.firstWhere((p) => p.key == 'oil');
|
|
expect(oilOverdue.urgency, MaintenanceUrgency.overdue);
|
|
expect(oilOverdue.milesRemaining, -200.0);
|
|
});
|
|
|
|
test('updateSpecJsonWithMaintenance preserves existing specs while modifying maintenance', () {
|
|
final initialSpec = json.encode({
|
|
'displacementCc': 599.0,
|
|
'horsepower': 120.0,
|
|
});
|
|
|
|
final updatedJson = MaintenanceData.updateSpecJsonWithMaintenance(
|
|
originalSpecJson: initialSpec,
|
|
baseOdometer: 14500.0,
|
|
serviceRecords: [
|
|
ServiceRecord(
|
|
id: 'srv_100',
|
|
date: DateTime(2026, 5, 1),
|
|
odometer: 14500.0,
|
|
serviceType: 'Chain Clean & Lube',
|
|
notes: 'Motul paste',
|
|
),
|
|
],
|
|
customIntervals: {'chain': 500},
|
|
);
|
|
|
|
final parsed = json.decode(updatedJson);
|
|
expect(parsed['displacementCc'], 599.0);
|
|
expect(parsed['horsepower'], 120.0);
|
|
expect(parsed['baseOdometer'], 14500.0);
|
|
expect(parsed['serviceRecords'], hasLength(1));
|
|
expect(parsed['customIntervals']['chain'], 500);
|
|
|
|
final data = MaintenanceData.fromSpecificationJson(updatedJson);
|
|
expect(data.baseOdometer, 14500.0);
|
|
expect(data.serviceRecords.first.serviceType, 'Chain Clean & Lube');
|
|
expect(data.customIntervals['chain'], 500);
|
|
});
|
|
});
|
|
|
|
group('GarageController Maintenance CRUD Operations', () {
|
|
late AppDatabase db;
|
|
late GarageController controller;
|
|
|
|
setUp(() {
|
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
|
controller = GarageController(db, HybridMotorcycleRepository());
|
|
});
|
|
|
|
tearDown(() async {
|
|
await db.close();
|
|
});
|
|
|
|
test('add, update, and delete service records in database', () async {
|
|
// 1. Insert a test motorcycle
|
|
final bikeId = await db.into(db.garageMotorcycles).insert(
|
|
GarageMotorcyclesCompanion.insert(
|
|
year: 2001,
|
|
makeId: 'yamaha',
|
|
makeName: 'Yamaha',
|
|
modelId: 'yzf-r6',
|
|
modelName: 'YZF-R6',
|
|
dateAdded: DateTime.now(),
|
|
lastUpdated: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
// 2. Set base odometer
|
|
await controller.updateBaseOdometer(bikeId, 21500.0);
|
|
|
|
var bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle();
|
|
var data = MaintenanceData.fromSpecificationJson(bike.specificationJson);
|
|
expect(data.baseOdometer, 21500.0);
|
|
expect(data.serviceRecords, isEmpty);
|
|
|
|
// 3. Add a service record
|
|
final record1 = ServiceRecord(
|
|
id: 'srv_test_1',
|
|
date: DateTime(2026, 4, 10),
|
|
odometer: 21500.0,
|
|
serviceType: 'Engine Oil & Filter',
|
|
notes: 'Castrol Power1',
|
|
cost: 39.99,
|
|
);
|
|
await controller.addServiceRecord(bikeId, record1);
|
|
|
|
bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle();
|
|
data = MaintenanceData.fromSpecificationJson(bike.specificationJson);
|
|
expect(data.serviceRecords, hasLength(1));
|
|
expect(data.serviceRecords.first.serviceType, 'Engine Oil & Filter');
|
|
expect(data.serviceRecords.first.cost, 39.99);
|
|
|
|
// 4. Update the service record
|
|
final updatedRecord = ServiceRecord(
|
|
id: 'srv_test_1',
|
|
date: DateTime(2026, 4, 10),
|
|
odometer: 21550.0,
|
|
serviceType: 'Engine Oil & Filter (Full Synthetic)',
|
|
notes: 'Castrol Power1 4T + K&N Filter',
|
|
cost: 54.99,
|
|
);
|
|
await controller.updateServiceRecord(bikeId, updatedRecord);
|
|
|
|
bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle();
|
|
data = MaintenanceData.fromSpecificationJson(bike.specificationJson);
|
|
expect(data.serviceRecords, hasLength(1));
|
|
expect(data.serviceRecords.first.odometer, 21550.0);
|
|
expect(data.serviceRecords.first.serviceType, 'Engine Oil & Filter (Full Synthetic)');
|
|
expect(data.serviceRecords.first.cost, 54.99);
|
|
|
|
// 5. Update custom interval
|
|
await controller.updateCustomInterval(bikeId, 'oil', 2500);
|
|
bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle();
|
|
data = MaintenanceData.fromSpecificationJson(bike.specificationJson);
|
|
expect(data.customIntervals['oil'], 2500);
|
|
|
|
// 6. Delete service record
|
|
await controller.deleteServiceRecord(bikeId, 'srv_test_1');
|
|
bike = await (db.select(db.garageMotorcycles)..where((t) => t.id.equals(bikeId))).getSingle();
|
|
data = MaintenanceData.fromSpecificationJson(bike.specificationJson);
|
|
expect(data.serviceRecords, isEmpty);
|
|
// base odometer and custom intervals should still be intact
|
|
expect(data.baseOdometer, 21500.0);
|
|
expect(data.customIntervals['oil'], 2500);
|
|
});
|
|
});
|
|
|
|
group('Cross-Device Maintenance Sync Merging', () {
|
|
test('mergeSpecificationJson preserves service records from both devices and attaches cloudId', () {
|
|
final localJson = MaintenanceData.updateSpecJsonWithMaintenance(
|
|
originalSpecJson: json.encode({'displacementCc': 599.0, 'vin': 'JYARJ06E'}),
|
|
baseOdometer: 15200.0,
|
|
serviceRecords: [
|
|
ServiceRecord(
|
|
id: 'srv_phone_oil',
|
|
date: DateTime(2026, 3, 1),
|
|
odometer: 15000.0,
|
|
serviceType: 'Oil & Filter',
|
|
),
|
|
],
|
|
customIntervals: {'oil': 3000},
|
|
);
|
|
|
|
final remoteJson = MaintenanceData.updateSpecJsonWithMaintenance(
|
|
originalSpecJson: json.encode({'displacementCc': 599.0, 'color': 'Blue'}),
|
|
baseOdometer: 15800.0,
|
|
serviceRecords: [
|
|
ServiceRecord(
|
|
id: 'srv_web_tires',
|
|
date: DateTime(2026, 4, 1),
|
|
odometer: 15500.0,
|
|
serviceType: 'Front/Rear Tires',
|
|
),
|
|
],
|
|
customIntervals: {'tires': 6000},
|
|
);
|
|
|
|
final mergedJson = FirebaseAccountSyncService.mergeSpecificationJson(
|
|
localSpecJson: localJson,
|
|
remoteSpecJson: remoteJson,
|
|
cloudId: 'cloud_bike_abc123',
|
|
);
|
|
|
|
final mergedData = MaintenanceData.fromSpecificationJson(mergedJson);
|
|
final rawDecoded = json.decode(mergedJson) as Map<String, dynamic>;
|
|
|
|
// Verifies cloudId attached
|
|
expect(rawDecoded['cloudId'], equals('cloud_bike_abc123'));
|
|
|
|
// Verifies both local and remote service records survived
|
|
expect(mergedData.serviceRecords, hasLength(2));
|
|
final recordIds = mergedData.serviceRecords.map((r) => r.id).toList();
|
|
expect(recordIds, contains('srv_phone_oil'));
|
|
expect(recordIds, contains('srv_web_tires'));
|
|
|
|
// Verifies highest base odometer was selected
|
|
expect(mergedData.baseOdometer, equals(15800.0));
|
|
|
|
// Verifies custom intervals were merged
|
|
expect(mergedData.customIntervals['oil'], equals(3000));
|
|
expect(mergedData.customIntervals['tires'], equals(6000));
|
|
|
|
// Verifies specifications preserved
|
|
expect(rawDecoded['displacementCc'], equals(599.0));
|
|
expect(rawDecoded['vin'], equals('JYARJ06E'));
|
|
});
|
|
});
|
|
}
|