import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:nuvolari/core/region/geo.dart'; import 'package:nuvolari/core/region/region_config.dart'; import 'package:nuvolari/core/region/region_repository.dart'; import 'package:nuvolari/features/places/map_target.dart'; import 'package:nuvolari/features/places/places_repository.dart'; import 'package:nuvolari/features/places/saved_place.dart'; import 'package:nuvolari/features/places/saved_places.dart'; const torino = GeoPoint(7.686, 45.070); const cuneo = GeoPoint(7.549, 44.393); SavedPlace place(String id, String name, GeoPoint point) => SavedPlace(id: id, name: name, point: point, createdAt: DateTime.utc(2026)); ({ProviderContainer container, InMemoryPreferredTargetStore store}) harness( RegionConfig region, { List? places, String? preferred, }) { final store = InMemoryPreferredTargetStore(preferred); final container = ProviderContainer( overrides: [ regionConfigProvider.overrideWith((ref) async => region), placesStoreProvider.overrideWithValue(InMemoryPlacesStore(places)), preferredTargetStoreProvider.overrideWithValue(store), ], ); addTearDown(container.dispose); return (container: container, store: store); } void main() { late RegionConfig region; setUpAll(() { region = RegionConfig.parse( File('assets/regions/piemonte.json').readAsStringSync(), ); }); group('MapTargetController', () { test('selecting a place remembers it as what to reopen on', () async { final casa = place('a', 'Casa', torino); final h = harness(region, places: [casa]); await h.container.read(savedPlacesProvider.future); await h.container .read(mapTargetProvider.notifier) .select(PlaceTarget(casa)); expect(h.container.read(mapTargetProvider), PlaceTarget(casa)); expect(await h.store.load(), 'place:a'); }); test('choosing the whole region clears the stored preference', () async { final h = harness(region, preferred: 'follow'); await h.container.read(savedPlacesProvider.future); await h.container .read(mapTargetProvider.notifier) .select(const FreeTarget()); expect(await h.store.load(), isNull); }); // Searching is looking, not deciding. Before PointTarget existed a search // went through select(FreeTarget), which wrote null over the preference and // silently cost the user the place their app opened on. test('previewing a searched point leaves the preference alone', () async { final casa = place('a', 'Casa', torino); final h = harness(region, places: [casa], preferred: 'place:a'); await h.container.read(savedPlacesProvider.future); h.container .read(mapTargetProvider.notifier) .previewPoint(point: cuneo, label: 'Cuneo'); expect( h.container.read(mapTargetProvider), const PointTarget(point: cuneo, label: 'Cuneo'), ); expect( await h.store.load(), 'place:a', reason: 'a search must not change what the app reopens on', ); }); test('a previewed point is never persisted, even if asked', () async { final h = harness(region); await h.container.read(savedPlacesProvider.future); expect( () => h.container .read(mapTargetProvider.notifier) .select(const PointTarget(point: cuneo, label: 'Cuneo')), throwsA(isA()), ); }); // What the map marks is the whole point of the target for anything that is // not the user themselves: a town has no precise position, so its centre is // the honest answer. test('says what the map should mark', () { final casa = place('a', 'Casa', torino); expect(PlaceTarget(casa).markedPoint, torino); expect( const PointTarget(point: cuneo, label: 'Cuneo').markedPoint, cuneo, ); expect(const FreeTarget().markedPoint, isNull); expect( const FollowUser().markedPoint, isNull, reason: 'the blue dot marks the user, not a pin', ); }); test('points differ by coordinates and by label', () { const at = PointTarget(point: cuneo, label: 'Cuneo'); expect(at, const PointTarget(point: cuneo, label: 'Cuneo')); expect(at, isNot(const PointTarget(point: torino, label: 'Cuneo'))); expect(at, isNot(const PointTarget(point: cuneo, label: 'Cuneo Centro'))); }); test('restores the preferred place once places have loaded', () async { final casa = place('a', 'Casa', torino); final h = harness(region, places: [casa], preferred: 'place:a'); await h.container.read(savedPlacesProvider.future); // Reading it is what builds it, and the restore starts from there: it // reads the store, so it settles a microtask later, not immediately. expect(h.container.read(mapTargetProvider), const FreeTarget()); await Future.delayed(Duration.zero); expect(h.container.read(mapTargetProvider), PlaceTarget(casa)); }); test('forgets a preferred place that no longer exists', () async { final h = harness(region, places: [], preferred: 'place:gone'); await h.container.read(savedPlacesProvider.future); h.container.read(mapTargetProvider); await Future.delayed(Duration.zero); expect(h.container.read(mapTargetProvider), const FreeTarget()); expect(await h.store.load(), isNull); }); test( 'panning while following stops the chase but keeps the choice', () async { final h = harness(region); await h.container.read(savedPlacesProvider.future); await h.container .read(mapTargetProvider.notifier) .select(const FollowUser()); h.container.read(mapTargetProvider.notifier).releaseFollow(); expect(h.container.read(mapTargetProvider), const FreeTarget()); expect( await h.store.load(), 'follow', reason: 'looking elsewhere now is not changing your mind for next time', ); }, ); }); }