feat(todo): a location's search goes through the instance (ADR 0033, D32)

The picker now searches with the server's `search`, and says search
isn't available where the instance has no Photon rather than that it
failed. A debug build may still ask for komoot's public one, for an
instance without. A point on the map stays unnamed through the instance,
which searches by name alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-05 23:04:18 +01:00
commit 5d9c640e5a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 170 additions and 8 deletions

View file

@ -8,7 +8,8 @@ import 'package:halkyon_todo_flutter/src/feature/location/model/task_location.da
import 'package:halkyon_todo_flutter/src/feature/location/widget/location_picker.dart';
import 'package:material_ui/material_ui.dart';
class _Places(final List<TaskLocation> found) implements PlaceSearchGateway {
class _Places(final List<TaskLocation> found, {final bool isOffered = true})
implements PlaceSearchGateway {
final asked = <String>[];
LatLon? near;
@ -25,6 +26,9 @@ class _Places(final List<TaskLocation> found) implements PlaceSearchGateway {
@override
Future<TaskLocation?> at(LatLon point, {String? language}) async => null;
@override
Future<bool> offered() async => isOffered;
}
/// The location picker (ADR 0033, D32), without tiles.
@ -132,6 +136,21 @@ void main() {
expect(find.byKey(locationMapKey), findsOneWidget);
});
testWidgets('an instance without a search says so, the map stays', (
tester,
) async {
await open(tester, places: _Places(const [], isOffered: false));
await tester.pump();
expect(find.byKey(locationSearchKey), findsNothing);
expect(
find.text(
"Search isn't available here. Move the map and tap to place a pin.",
),
findsOneWidget,
);
expect(find.byKey(locationMapKey), findsOneWidget);
});
testWidgets('recent places show before a search', (tester) async {
await open(tester, places: _Places(const []), recents: const [aeschach]);
expect(find.text('Recent'), findsOneWidget);

View file

@ -1,8 +1,72 @@
import 'package:halkyon_todo_client/halkyon_todo_client.dart';
import 'package:halkyon_todo_flutter/src/feature/location/data/place_search_gateway.dart';
import 'package:test/test.dart';
/// Replies as Photon's (ADR 0033, D32).
void main() {
test("the instance's places keep their address and object", () async {
final gateway = InstancePlaceSearch(
asked: () async => true,
find: (query, near, language) async {
expect((query, near?.lat, language), ('Lindau EDEKA', 47.5, 'de'));
return [
Place(
name: 'EDEKA',
detail: 'Ludwig-Kick-Straße 1, Lindau',
kind: 'house',
osmType: 'N',
osmId: 7,
lat: 47.55,
lon: 9.7,
),
];
},
);
final [place] = await gateway.search(
'Lindau EDEKA',
near: (lat: 47.5, lon: 9.7),
language: 'de',
);
expect(place.address, 'Ludwig-Kick-Straße 1, Lindau');
expect((place.lat, place.lon), (47.55, 9.7));
expect((place.osmType, place.osmId), ('N', 7));
});
test('a place without a detail has no address', () async {
final gateway = InstancePlaceSearch(
asked: () async => true,
find: (_, _, _) async => [
Place(name: 'Lindau', kind: 'city', lat: 47.55, lon: 9.7),
],
);
final [place] = await gateway.search('Lindau');
expect(place.address, '');
});
test("an instance that can't be asked offers no search", () async {
final gateway = InstancePlaceSearch(
asked: () async => throw Exception('offline'),
find: (_, _, _) async => const [],
);
expect(await gateway.offered(), isFalse);
});
test('a refusal reaches the picker, which says the search failed', () {
final gateway = InstancePlaceSearch(
asked: () async => true,
find: (_, _, _) async => throw SearchRefused(reason: 'unreachable'),
);
expect(gateway.search('Lindau'), throwsA(isA<SearchRefused>()));
});
test('a point on the map has no name from the instance', () async {
final gateway = InstancePlaceSearch(
asked: () async => true,
find: (_, _, _) async => const [],
);
expect(await gateway.at((lat: 47.5, lon: 9.7)), isNull);
});
Map<String, Object?> feature(
Map<String, Object?> properties, {
List<num> at = const [9.70, 47.55],