feat(todo): a pin in the map's middle, and what is around it (ADR 0033, D32)
The location picker fills the screen with its map, the search floating over it. A pin stays in the middle, lifting while the map moves, as Molly's; where it lands the instance names the address and lists the places around, as Threema's. That is `nearby` on the search endpoint, for those signed in, the point neither kept nor logged, counted among the searches: two lookups, as a plain one finds house numbers alone. Fixes the map's credit, which overflowed a phone and was pale on white. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5d9c640e5a
commit
ce07fab193
2 changed files with 1254 additions and 113 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_languages.dart';
|
||||
|
|
@ -6,10 +8,15 @@ import 'package:halkyon_todo_flutter/src/feature/location/data/location_reminder
|
|||
import 'package:halkyon_todo_flutter/src/feature/location/data/place_search_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/model/task_location.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/widget/location_picker.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/widget/map_tiles.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
class _Places(final List<TaskLocation> found, {final bool isOffered = true})
|
||||
implements PlaceSearchGateway {
|
||||
class _Places(
|
||||
final List<TaskLocation> found, {
|
||||
final bool isOffered = true,
|
||||
final Future<PlacesAround> Function(LatLon point) nearby = _nothing,
|
||||
}) implements PlaceSearchGateway {
|
||||
final looked = <LatLon>[];
|
||||
final asked = <String>[];
|
||||
LatLon? near;
|
||||
|
||||
|
|
@ -25,12 +32,17 @@ class _Places(final List<TaskLocation> found, {final bool isOffered = true})
|
|||
}
|
||||
|
||||
@override
|
||||
Future<TaskLocation?> at(LatLon point, {String? language}) async => null;
|
||||
Future<PlacesAround> around(LatLon point, {String? language}) {
|
||||
looked.add(point);
|
||||
return nearby(point);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> offered() async => isOffered;
|
||||
}
|
||||
|
||||
Future<PlacesAround> _nothing(LatLon point) async => const PlacesAround();
|
||||
|
||||
/// The location picker (ADR 0033, D32), without tiles.
|
||||
void main() {
|
||||
const aeschach = TaskLocation(
|
||||
|
|
@ -51,6 +63,8 @@ void main() {
|
|||
PlaceSearchGateway? places,
|
||||
List<TaskLocation> recents = const [],
|
||||
String? query,
|
||||
LatLon? at,
|
||||
MapTiles? tiles,
|
||||
}) async {
|
||||
final picked = <TaskLocation?>[];
|
||||
await tester.pumpWidget(
|
||||
|
|
@ -66,11 +80,11 @@ void main() {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => LocationPicker(
|
||||
places: places,
|
||||
mapTiles: null,
|
||||
mapTiles: tiles,
|
||||
access: LocationReminderAccess(
|
||||
const NoLocationReminders(),
|
||||
),
|
||||
position: () async => null,
|
||||
position: () async => at,
|
||||
recents: recents,
|
||||
query: query,
|
||||
),
|
||||
|
|
@ -170,4 +184,185 @@ void main() {
|
|||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.text('On arriving'), findsNothing);
|
||||
});
|
||||
|
||||
group("the pin in the map's middle", () {
|
||||
const here = (lat: 47.55, lon: 9.70);
|
||||
const street = TaskLocation(
|
||||
name: 'Danziger Weg 4',
|
||||
lat: 47.55,
|
||||
lon: 9.70,
|
||||
address: 'Lindau',
|
||||
);
|
||||
const bakery = TaskLocation(
|
||||
name: 'Schwarz',
|
||||
lat: 47.551,
|
||||
lon: 9.701,
|
||||
address: 'Kemptener Straße, Lindau',
|
||||
);
|
||||
Future<PlacesAround> found(LatLon point) async =>
|
||||
const PlacesAround(spot: street, places: [bakery]);
|
||||
|
||||
/// Lets a move of the map come to rest, and what it asked answer.
|
||||
Future<void> rest(WidgetTester tester) async {
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
Future<void> dragMap(WidgetTester tester) async {
|
||||
await tester.timedDrag(
|
||||
find.byKey(locationMapKey),
|
||||
const Offset(-120, 40),
|
||||
const Duration(milliseconds: 300),
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry lifted(WidgetTester tester) => tester
|
||||
.widget<AnimatedPadding>(
|
||||
find.descendant(
|
||||
of: find.byKey(locationPinKey),
|
||||
matching: find.byType(AnimatedPadding),
|
||||
),
|
||||
)
|
||||
.padding;
|
||||
|
||||
testWidgets('the spot is named by what is near, and adds', (tester) async {
|
||||
final picked = await open(
|
||||
tester,
|
||||
places: _Places(const [], nearby: found),
|
||||
at: here,
|
||||
);
|
||||
await rest(tester);
|
||||
expect(find.byKey(locationPinKey), findsOneWidget);
|
||||
expect(find.text('Danziger Weg 4'), findsOneWidget);
|
||||
await tester.ensureVisible(find.byKey(locationAddKey));
|
||||
await tester.tap(find.byKey(locationAddKey));
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(picked.single?.name, 'Danziger Weg 4');
|
||||
expect(picked.single?.address, 'Lindau');
|
||||
});
|
||||
|
||||
testWidgets('the places around it can be chosen instead', (tester) async {
|
||||
final picked = await open(
|
||||
tester,
|
||||
places: _Places(const [], nearby: found),
|
||||
at: here,
|
||||
);
|
||||
await rest(tester);
|
||||
expect(find.text('Or choose a nearby place'), findsOneWidget);
|
||||
await tester.tap(find.text(bakery.address));
|
||||
await rest(tester);
|
||||
await tester.tap(find.byKey(locationAddKey));
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(picked.single?.name, bakery.name);
|
||||
expect(
|
||||
(picked.single?.lat, picked.single?.lon),
|
||||
(bakery.lat, bakery.lon),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('moving the map lifts the pin, and lands on a new spot', (
|
||||
tester,
|
||||
) async {
|
||||
final places = _Places(const [], nearby: found);
|
||||
await open(tester, places: places, at: here);
|
||||
await rest(tester);
|
||||
final before = places.looked.length;
|
||||
await dragMap(tester);
|
||||
expect(lifted(tester), isNot(EdgeInsets.zero));
|
||||
await rest(tester);
|
||||
expect(lifted(tester), EdgeInsets.zero);
|
||||
expect(places.looked.length, before + 1);
|
||||
expect(places.looked.last, isNot(here));
|
||||
});
|
||||
|
||||
testWidgets('far out it stands for no spot, and asks nothing', (
|
||||
tester,
|
||||
) async {
|
||||
final places = _Places(const [], nearby: found);
|
||||
await open(tester, places: places);
|
||||
await dragMap(tester);
|
||||
await rest(tester);
|
||||
expect(places.looked, isEmpty);
|
||||
expect(find.byKey(locationAddKey), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a name typed stays when the spot moves', (tester) async {
|
||||
final picked = await open(
|
||||
tester,
|
||||
places: _Places(const [], nearby: found),
|
||||
at: here,
|
||||
);
|
||||
await rest(tester);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Danziger Weg 4'),
|
||||
'Home',
|
||||
);
|
||||
await dragMap(tester);
|
||||
await rest(tester);
|
||||
await tester.tap(find.byKey(locationAddKey));
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(picked.single?.name, 'Home');
|
||||
});
|
||||
|
||||
testWidgets('a lookup that fails leaves the pin, unnamed', (tester) async {
|
||||
await open(
|
||||
tester,
|
||||
places: _Places(
|
||||
const [],
|
||||
nearby: (_) async => throw Exception('offline'),
|
||||
),
|
||||
at: here,
|
||||
);
|
||||
await rest(tester);
|
||||
expect(find.text('Pinned place'), findsOneWidget);
|
||||
expect(find.byKey(locationAddKey), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the answer for a spot left behind is dropped', (tester) async {
|
||||
final first = Completer<PlacesAround>();
|
||||
var asked = 0;
|
||||
await open(
|
||||
tester,
|
||||
places: _Places(
|
||||
const [],
|
||||
nearby: (_) => asked++ == 0
|
||||
? first.future
|
||||
: Future.value(const PlacesAround(spot: street)),
|
||||
),
|
||||
at: here,
|
||||
);
|
||||
await rest(tester);
|
||||
await dragMap(tester);
|
||||
await rest(tester);
|
||||
first.complete(const PlacesAround(spot: bakery));
|
||||
await rest(tester);
|
||||
expect(find.text('Danziger Weg 4'), findsOneWidget);
|
||||
expect(find.text('Schwarz'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the credit fits a narrow phone, and can be read', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(320, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await open(
|
||||
tester,
|
||||
places: _Places(const []),
|
||||
at: here,
|
||||
tiles: () => const SizedBox(),
|
||||
);
|
||||
await rest(tester);
|
||||
expect(tester.takeException(), isNull);
|
||||
final credit = tester.widget<Text>(find.byKey(locationAttributionKey));
|
||||
expect(credit.data, mapAttribution);
|
||||
final theme = Theme.of(
|
||||
tester.element(find.byKey(locationAttributionKey)),
|
||||
);
|
||||
expect(credit.style?.color, theme.colorScheme.onSurface);
|
||||
expect(find.textContaining('flutter_map'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ 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';
|
||||
|
||||
Future<Nearby> _nearby(PlacePoint at, String? language) async =>
|
||||
Nearby(places: const []);
|
||||
|
||||
/// Replies as Photon's (ADR 0033, D32).
|
||||
void main() {
|
||||
test("the instance's places keep their address and object", () async {
|
||||
|
|
@ -21,6 +24,7 @@ void main() {
|
|||
),
|
||||
];
|
||||
},
|
||||
nearby: _nearby,
|
||||
);
|
||||
final [place] = await gateway.search(
|
||||
'Lindau EDEKA',
|
||||
|
|
@ -38,6 +42,7 @@ void main() {
|
|||
find: (_, _, _) async => [
|
||||
Place(name: 'Lindau', kind: 'city', lat: 47.55, lon: 9.7),
|
||||
],
|
||||
nearby: _nearby,
|
||||
);
|
||||
final [place] = await gateway.search('Lindau');
|
||||
expect(place.address, '');
|
||||
|
|
@ -47,6 +52,7 @@ void main() {
|
|||
final gateway = InstancePlaceSearch(
|
||||
asked: () async => throw Exception('offline'),
|
||||
find: (_, _, _) async => const [],
|
||||
nearby: _nearby,
|
||||
);
|
||||
expect(await gateway.offered(), isFalse);
|
||||
});
|
||||
|
|
@ -55,16 +61,49 @@ void main() {
|
|||
final gateway = InstancePlaceSearch(
|
||||
asked: () async => true,
|
||||
find: (_, _, _) async => throw SearchRefused(reason: 'unreachable'),
|
||||
nearby: _nearby,
|
||||
);
|
||||
expect(gateway.search('Lindau'), throwsA(isA<SearchRefused>()));
|
||||
});
|
||||
|
||||
test('a point on the map has no name from the instance', () async {
|
||||
test('what is around a point keeps the address and the places', () async {
|
||||
final gateway = InstancePlaceSearch(
|
||||
asked: () async => true,
|
||||
find: (_, _, _) async => const [],
|
||||
nearby: (at, language) async {
|
||||
expect((at.lat, at.lon, language), (47.5, 9.7, 'de'));
|
||||
return Nearby(
|
||||
spot: Place(
|
||||
name: 'Danziger Weg 4',
|
||||
detail: 'Lindau',
|
||||
kind: 'house',
|
||||
lat: 47.5,
|
||||
lon: 9.7,
|
||||
),
|
||||
places: [
|
||||
Place(name: 'Schwarz', kind: 'house', lat: 47.51, lon: 9.71),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(await gateway.at((lat: 47.5, lon: 9.7)), isNull);
|
||||
final around = await gateway.around(
|
||||
(lat: 47.5, lon: 9.7),
|
||||
language: 'de',
|
||||
);
|
||||
expect(around.spot?.name, 'Danziger Weg 4');
|
||||
expect(around.spot?.address, 'Lindau');
|
||||
expect(around.places.single.name, 'Schwarz');
|
||||
});
|
||||
|
||||
test('nothing around a point leaves the pin unnamed', () async {
|
||||
final gateway = InstancePlaceSearch(
|
||||
asked: () async => true,
|
||||
find: (_, _, _) async => const [],
|
||||
nearby: _nearby,
|
||||
);
|
||||
final around = await gateway.around((lat: 47.5, lon: 9.7));
|
||||
expect(around.spot, isNull);
|
||||
expect(around.places, isEmpty);
|
||||
});
|
||||
|
||||
Map<String, Object?> feature(
|
||||
|
|
|
|||
Loading…
Reference in a new issue