162 lines
5.8 KiB
Dart
162 lines
5.8 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:motorcycle_dash/models/maintenance_models.dart';
|
|
|
|
void main() {
|
|
group('Motorcycle Maintenance Due Tracking (MOB-16)', () {
|
|
test('MaintenanceData parses from specificationJson safely', () {
|
|
final initialSpec = json.encode({
|
|
'year': 2024,
|
|
'manufacturer': 'Yamaha',
|
|
'model': 'YZF-R7',
|
|
'baseOdometer': 1200.0,
|
|
'serviceRecords': [
|
|
{
|
|
'id': 'srv_1',
|
|
'date': '2026-08-15T10:00:00Z',
|
|
'odometer': 600.0,
|
|
'serviceType': 'Break-in Oil & Filter',
|
|
'notes': 'Yamalube 10W-40, OEM filter',
|
|
'cost': 45.00,
|
|
},
|
|
],
|
|
});
|
|
|
|
final data = MaintenanceData.fromSpecificationJson(initialSpec);
|
|
expect(data.baseOdometer, 1200.0);
|
|
expect(data.serviceRecords.length, 1);
|
|
expect(data.serviceRecords.first.serviceType, 'Break-in Oil & Filter');
|
|
expect(data.serviceRecords.first.cost, 45.00);
|
|
});
|
|
|
|
test('MaintenanceData handles null or empty specificationJson gracefully', () {
|
|
final dataNull = MaintenanceData.fromSpecificationJson(null);
|
|
expect(dataNull.baseOdometer, 0.0);
|
|
expect(dataNull.serviceRecords, isEmpty);
|
|
|
|
final dataEmpty = MaintenanceData.fromSpecificationJson('');
|
|
expect(dataEmpty.baseOdometer, 0.0);
|
|
expect(dataEmpty.serviceRecords, isEmpty);
|
|
|
|
final dataCorrupt = MaintenanceData.fromSpecificationJson('{corrupted json}');
|
|
expect(dataCorrupt.baseOdometer, 0.0);
|
|
expect(dataCorrupt.serviceRecords, isEmpty);
|
|
});
|
|
|
|
test('calculatePills accurately counts down miles and determines urgency', () {
|
|
final data = MaintenanceData(
|
|
baseOdometer: 1000.0,
|
|
serviceRecords: [
|
|
ServiceRecord(
|
|
id: 'srv_1',
|
|
date: DateTime(2026, 8, 1),
|
|
odometer: 1000.0,
|
|
serviceType: 'Engine Oil Change',
|
|
),
|
|
ServiceRecord(
|
|
id: 'srv_2',
|
|
date: DateTime(2026, 8, 10),
|
|
odometer: 1400.0,
|
|
serviceType: 'Chain Clean & Lube',
|
|
),
|
|
],
|
|
);
|
|
|
|
// Ride 500 miles
|
|
final pills = data.calculatePills(500.0);
|
|
// Total odometer = 1000 + 500 = 1500 mi.
|
|
// Oil last serviced at 1000 mi -> miles since service = 500 mi.
|
|
// Oil interval is 3000 mi -> miles remaining = 2500 mi (Urgency: ok).
|
|
final oilPill = pills.firstWhere((p) => p.key == 'oil');
|
|
expect(oilPill.milesRemaining, 2500.0);
|
|
expect(oilPill.urgency, MaintenanceUrgency.ok);
|
|
expect(oilPill.label, '2500 mi remaining');
|
|
|
|
// Chain last serviced at 1400 mi -> miles since service = 100 mi.
|
|
// Chain interval is 600 mi -> miles remaining = 500 mi (Urgency: ok).
|
|
final chainPill = pills.firstWhere((p) => p.key == 'chain');
|
|
expect(chainPill.milesRemaining, 500.0);
|
|
expect(chainPill.urgency, MaintenanceUrgency.ok);
|
|
|
|
// Tire wear: no service logged yet -> defaults to baseOdometer (1000 mi).
|
|
// Miles since service = 500 mi. Interval = 4000 mi -> remaining = 3500 mi.
|
|
final tirePill = pills.firstWhere((p) => p.key == 'tire');
|
|
expect(tirePill.milesRemaining, 3500.0);
|
|
expect(tirePill.urgency, MaintenanceUrgency.ok);
|
|
});
|
|
|
|
test('calculatePills flags dueSoon and overdue accurately', () {
|
|
final data = MaintenanceData(
|
|
baseOdometer: 2800.0,
|
|
serviceRecords: [
|
|
ServiceRecord(
|
|
id: 'srv_1',
|
|
date: DateTime(2026, 1, 1),
|
|
odometer: 0.0,
|
|
serviceType: 'Initial Oil',
|
|
),
|
|
ServiceRecord(
|
|
id: 'srv_2',
|
|
date: DateTime(2026, 1, 1),
|
|
odometer: 2250.0,
|
|
serviceType: 'Chain Service',
|
|
),
|
|
],
|
|
);
|
|
|
|
// Ride 50 miles -> total odometer = 2850 mi.
|
|
// Oil: last serviced 0 mi -> miles since service = 2850 mi.
|
|
// Interval = 3000 mi -> remaining = 150 mi (dueSoonThreshold is 350 mi) -> dueSoon!
|
|
final pills = data.calculatePills(50.0);
|
|
final oilPill = pills.firstWhere((p) => p.key == 'oil');
|
|
expect(oilPill.milesRemaining, 150.0);
|
|
expect(oilPill.urgency, MaintenanceUrgency.dueSoon);
|
|
expect(oilPill.label, 'Due Soon (150 mi)');
|
|
|
|
// Chain: last serviced at 2250 mi -> miles since service = 600 mi.
|
|
// Interval = 600 mi -> remaining = 0 mi -> overdue!
|
|
final chainPill = pills.firstWhere((p) => p.key == 'chain');
|
|
expect(chainPill.milesRemaining, 0.0);
|
|
expect(chainPill.urgency, MaintenanceUrgency.overdue);
|
|
expect(chainPill.label, contains('OVERDUE'));
|
|
});
|
|
|
|
test('updateSpecJsonWithMaintenance preserves existing spec data without mutations', () {
|
|
final original = json.encode({
|
|
'year': 2024,
|
|
'manufacturer': 'Ducati',
|
|
'model': 'Panigale V2',
|
|
'horsepower': 155.0,
|
|
'displacementCc': 955.0,
|
|
});
|
|
|
|
final updated = MaintenanceData.updateSpecJsonWithMaintenance(
|
|
originalSpecJson: original,
|
|
baseOdometer: 3400.0,
|
|
serviceRecords: [
|
|
ServiceRecord(
|
|
id: 'srv_1',
|
|
date: DateTime(2026, 9, 1),
|
|
odometer: 3000.0,
|
|
serviceType: 'Desmo Service',
|
|
cost: 850.0,
|
|
),
|
|
],
|
|
);
|
|
|
|
final decoded = json.decode(updated) as Map<String, dynamic>;
|
|
expect(decoded['year'], 2024);
|
|
expect(decoded['manufacturer'], 'Ducati');
|
|
expect(decoded['model'], 'Panigale V2');
|
|
expect(decoded['horsepower'], 155.0);
|
|
expect(decoded['displacementCc'], 955.0);
|
|
expect(decoded['baseOdometer'], 3400.0);
|
|
|
|
final records = decoded['serviceRecords'] as List;
|
|
expect(records.length, 1);
|
|
expect(records[0]['serviceType'], 'Desmo Service');
|
|
expect(records[0]['cost'], 850.0);
|
|
});
|
|
});
|
|
}
|