feat(wip): implement trip persistence through a local repository. Include loaded trips in the start page UI

This commit is contained in:
2025-12-30 00:51:40 +01:00
parent 81ed2fd8c3
commit 014b48591e
28 changed files with 2395 additions and 387 deletions

View File

@@ -0,0 +1,124 @@
import 'package:anyway/domain/entities/landmark.dart';
import 'package:anyway/domain/entities/landmark_type.dart';
import 'package:anyway/domain/entities/preferences.dart';
import 'package:anyway/domain/entities/trip.dart';
import 'package:anyway/domain/repositories/trip_repository.dart';
import 'package:anyway/presentation/providers/core_providers.dart';
import 'package:anyway/presentation/providers/trip_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
class _FakeTripRepository implements TripRepository {
_FakeTripRepository(this._trips);
final List<Trip> _trips;
@override
Future<void> deleteSavedTrip(String uuid) async {
_trips.removeWhere((trip) => trip.uuid == uuid);
}
@override
Future<Trip?> getSavedTrip(String uuid) async {
try {
return _trips.firstWhere((trip) => trip.uuid == uuid);
} on StateError {
return null;
}
}
@override
Future<List<Trip>> getSavedTrips() async {
return List.unmodifiable(_trips);
}
@override
Future<void> saveTrip(Trip trip) async {
_trips.removeWhere((t) => t.uuid == trip.uuid);
_trips.insert(0, trip);
}
// The following methods are not used in these tests.
@override
Future<Trip> getTrip({Preferences? preferences, String? tripUUID, List<Landmark>? landmarks}) {
throw UnimplementedError();
}
@override
Future<List<Landmark>> searchLandmarks(Preferences preferences) {
throw UnimplementedError();
}
}
Trip _buildTrip(String uuid) {
return Trip(
uuid: uuid,
totalTimeMinutes: 60,
landmarks: [
Landmark(
uuid: 'lm-$uuid',
name: 'Landmark $uuid',
location: const [0.0, 0.0],
type: const LandmarkType(type: LandmarkTypeEnum.sightseeing),
),
],
);
}
void main() {
test('currentTripsProvider loads saved trips on build', () async {
final repo = _FakeTripRepository([_buildTrip('trip-1')]);
final container = ProviderContainer(overrides: [
tripRepositoryProvider.overrideWithValue(repo),
]);
addTearDown(container.dispose);
final trips = await container.read(currentTripsProvider.future);
expect(trips, hasLength(1));
expect(trips.first.uuid, 'trip-1');
});
test('saveTrip persists trips locally and updates provider state', () async {
final repo = _FakeTripRepository([_buildTrip('existing')]);
final container = ProviderContainer(overrides: [
tripRepositoryProvider.overrideWithValue(repo),
]);
addTearDown(container.dispose);
await container.read(currentTripsProvider.future);
final notifier = container.read(currentTripsProvider.notifier);
await notifier.saveTrip(_buildTrip('fresh'));
final tripsState = container.read(currentTripsProvider);
expect(tripsState.value, isNotNull);
expect(tripsState.value!.first.uuid, 'fresh');
final repoTrips = await repo.getSavedTrips();
expect(repoTrips, hasLength(2));
expect(repoTrips.first.uuid, 'fresh');
});
test('deleteTrip removes trips from both provider state and repository', () async {
final repo = _FakeTripRepository([
_buildTrip('trip-1'),
_buildTrip('trip-2'),
]);
final container = ProviderContainer(overrides: [
tripRepositoryProvider.overrideWithValue(repo),
]);
addTearDown(container.dispose);
await container.read(currentTripsProvider.future);
final notifier = container.read(currentTripsProvider.notifier);
await notifier.deleteTrip('trip-1');
final tripsState = container.read(currentTripsProvider);
expect(tripsState.value, isNotNull);
expect(tripsState.value, hasLength(1));
expect(tripsState.value!.single.uuid, 'trip-2');
final repoTrips = await repo.getSavedTrips();
expect(repoTrips, hasLength(1));
expect(repoTrips.single.uuid, 'trip-2');
});
}

View File

@@ -0,0 +1,69 @@
import 'package:anyway/data/datasources/trip_local_datasource.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late TripLocalDataSource dataSource;
Map<String, dynamic> buildTripJson(String uuid) {
return {
'uuid': uuid,
'total_time_minute': 90,
'landmarks': [
{
'uuid': 'lm-$uuid',
'name': 'Landmark $uuid',
'location': [0.0, 0.0],
'type': {'type': 'sightseeing'},
},
],
};
}
setUp(() {
SharedPreferences.setMockInitialValues({});
dataSource = TripLocalDataSourceImpl();
});
test('loadTrips returns empty list when storage is empty', () async {
final trips = await dataSource.loadTrips();
expect(trips, isEmpty);
});
test('upsertTrip stores and retrieves a trip', () async {
await dataSource.upsertTrip(buildTripJson('trip-1'));
final trips = await dataSource.loadTrips();
expect(trips, hasLength(1));
expect(trips.first['uuid'], 'trip-1');
final fetched = await dataSource.getTrip('trip-1');
expect(fetched, isNotNull);
expect(fetched!['uuid'], 'trip-1');
});
test('deleteTrip removes the persisted entry', () async {
await dataSource.upsertTrip(buildTripJson('trip-1'));
await dataSource.upsertTrip(buildTripJson('trip-2'));
await dataSource.deleteTrip('trip-1');
final remaining = await dataSource.loadTrips();
expect(remaining, hasLength(1));
expect(remaining.single['uuid'], 'trip-2');
});
test('upsertTrip overwrites an existing trip and keeps latest first', () async {
await dataSource.upsertTrip(buildTripJson('trip-1'));
await dataSource.upsertTrip(buildTripJson('trip-2'));
final updatedTrip1 = buildTripJson('trip-1')..['total_time_minute'] = 120;
await dataSource.upsertTrip(updatedTrip1);
final trips = await dataSource.loadTrips();
expect(trips, hasLength(2));
expect(trips.first['uuid'], 'trip-1');
expect(trips.first['total_time_minute'], 120);
expect(trips[1]['uuid'], 'trip-2');
});
}