import 'package:consumer_app/l10n/app_localizations.dart';
import 'package:consumer_app/core/config/app_config.dart';
import 'package:consumer_app/models/content_models.dart';
import 'package:consumer_app/shared/map/map_surface.dart';
import 'package:consumer_app/shared/navigation/external_navigation_adapter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
void main() {
testWidgets('manual address search hides the personal GPS marker', (
tester,
) async {
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: GeoConfig.fallback(),
searchLatitude: 50.981,
searchLongitude: 11.32,
userLatitude: 50.9795,
userLongitude: 11.3235,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
// One object marker plus the address/search marker. The GPS marker is
// intentionally omitted while an address is the active origin.
expect(layer.markers, hasLength(2));
});
testWidgets('missing tile source exposes the Google Maps fallback action', (
tester,
) async {
NavigationTarget? openedTarget;
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: GeoConfig.fallback(),
onItemTap: (_) {},
onOpenGoogleMaps: (target) async {
openedTarget = target;
return true;
},
),
),
);
await tester.pumpAndSettle();
expect(
find.text(
'Keine interne Kartenquelle aktiv. Du kannst Google Maps öffnen.',
),
findsOneWidget,
);
expect(find.text('Google Maps öffnen'), findsOneWidget);
await tester.tap(find.text('Google Maps öffnen'));
expect(openedTarget, isNotNull);
expect(openedTarget!.latitude, GeoConfig.fallback().defaultLatitude);
expect(openedTarget!.longitude, GeoConfig.fallback().defaultLongitude);
});
testWidgets('search center does not cover the content marker', (
tester,
) async {
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: GeoConfig.fallback(),
searchLatitude: 50.9795,
searchLongitude: 11.3235,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(1));
});
testWidgets('GPS is shown when no manual address is active', (tester) async {
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: GeoConfig.fallback(),
userLatitude: 50.9795,
userLongitude: 11.3235,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(2));
});
testWidgets('GPS uses the bundled marker when WordPress has no marker', (
tester,
) async {
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: GeoConfig.fallback(),
userLatitude: 50.9795,
userLongitude: 11.3235,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final assetImages = tester
.widgetList<Image>(find.byType(Image))
.where(
(image) =>
image.image is AssetImage &&
(image.image as AssetImage).assetName ==
'assets/ico/user_location_marker.png',
);
expect(assetImages, isNotEmpty);
});
testWidgets('nearby markers keep their geographic coordinates', (
tester,
) async {
final first = _item();
final second = DiscoveryItem.fromJson({
'id': 454,
'resource': 'places',
'title': 'Goetheplatz',
'geo': {'available': true, 'lat': 50.9797, 'lng': 11.3237},
});
await tester.pumpWidget(
_app(
MapSurface(
items: [second, first],
meta: null,
geoConfig: GeoConfig.fallback(),
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(2));
final points = layer.markers.map((marker) => marker.point).toSet();
expect(
points,
containsAll(<LatLng>[
LatLng(first.geo.latitude!, first.geo.longitude!),
LatLng(second.geo.latitude!, second.geo.longitude!),
]),
);
});
testWidgets('regional zoom keeps marker at its API coordinate', (
tester,
) async {
final item = _item();
await tester.pumpWidget(
_app(
MapSurface(
items: [item],
meta: null,
geoConfig: GeoConfig.fallback(),
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
map.mapController!.move(const LatLng(50.98, 11.32), 5);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(1));
expect(
layer.markers.single.point,
LatLng(item.geo.latitude!, item.geo.longitude!),
);
});
testWidgets('compact detail maps never paint a search loupe over the item', (
tester,
) async {
final item = _item();
await tester.pumpWidget(
_app(
MapSurface(
items: [item],
meta: null,
geoConfig: GeoConfig.fallback(),
initialSelectedItem: item,
compact: true,
// Deliberately use a different search origin. The detail map must
// suppress the search-center marker even when it would not collide
// with the content marker.
searchLatitude: 50.981,
searchLongitude: 11.32,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(1));
expect(
layer.markers.single.point,
LatLng(item.geo.latitude!, item.geo.longitude!),
);
});
testWidgets('detail maps explicitly suppress the discovery search marker', (
tester,
) async {
final item = _item();
await tester.pumpWidget(
_app(
MapSurface(
items: [item],
meta: null,
geoConfig: GeoConfig.fallback(),
initialSelectedItem: item,
searchLatitude: 50.981,
searchLongitude: 11.32,
showSearchCenterMarker: false,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(1));
expect(
layer.markers.single.point,
LatLng(item.geo.latitude!, item.geo.longitude!),
);
});
testWidgets('single-item detail maps suppress stale search coordinates', (
tester,
) async {
final item = _item();
await tester.pumpWidget(
_app(
MapSurface(
items: [item],
meta: null,
geoConfig: GeoConfig.fallback(),
initialSelectedItem: item,
// Deliberately leave compact=true out. Detail semantics must still
// win over stale coordinates forwarded by a parent route.
searchLatitude: 50.981,
searchLongitude: 11.32,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(1));
expect(layer.markers.single.point, LatLng(50.9795, 11.3235));
});
testWidgets(
'detail maps stay centered on the object with an active GPS pin',
(tester) async {
final item = _item();
await tester.pumpWidget(
_app(
MapSurface(
items: [item],
meta: null,
geoConfig: GeoConfig.fallback(),
initialSelectedItem: item,
userLatitude: 50.99,
userLongitude: 11.4,
compact: true,
showSearchCenterMarker: false,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
expect(
map.mapController!.camera.center,
LatLng(item.geo.latitude!, item.geo.longitude!),
);
final layer = tester.widget<MarkerLayer>(find.byType(MarkerLayer));
expect(layer.markers, hasLength(2));
expect(
layer.markers.map((marker) => marker.point),
contains(LatLng(item.geo.latitude!, item.geo.longitude!)),
);
},
);
testWidgets('applies a late API geo center without user interaction', (
tester,
) async {
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: GeoConfig.fallback(),
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final configured = GeoConfig.fromJson({
'default_latitude': 51.05,
'default_longitude': 12.05,
'default_map_zoom': 11,
});
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: configured,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
expect(map.mapController!.camera.center, const LatLng(51.05, 12.05));
});
testWidgets('renders the API tile layer after late configuration', (
tester,
) async {
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: GeoConfig.fallback(),
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
expect(find.byType(TileLayer), findsNothing);
final configured = GeoConfig.fromJson({
'map_configured': true,
'tile_url_template': 'https://tiles.example/{z}/{x}/{y}.png',
'attribution': 'Example map data',
'user_agent_contact': 'tests@example.test',
'default_latitude': 51.05,
'default_longitude': 12.05,
});
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: configured,
onItemTap: (_) {},
),
),
);
await tester.pump();
expect(find.byType(TileLayer), findsOneWidget);
expect(
find.text(
'Keine Kartenquelle konfiguriert. Die Karte bleibt ohne Kacheln nutzbar.',
),
findsNothing,
);
});
testWidgets('recreates the tile layer when the API source changes', (
tester,
) async {
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: GeoConfig.fallback(),
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: GeoConfig.fromJson({
'map_configured': true,
'tile_url_template': 'https://tiles.example/road/{z}/{x}/{y}.png',
'attribution': 'Example map data',
'user_agent_contact': 'tests@example.test',
}),
onItemTap: (_) {},
),
),
);
await tester.pump();
final roadLayer = tester.widget<TileLayer>(find.byType(TileLayer));
await tester.pumpWidget(
_app(
MapSurface(
items: const [],
meta: null,
geoConfig: GeoConfig.fromJson({
'map_configured': true,
'tile_url_template': 'https://tiles.example/dark/{z}/{x}/{y}.png',
'attribution': 'Example map data',
'user_agent_contact': 'tests@example.test',
}),
onItemTap: (_) {},
),
),
);
await tester.pump();
final darkLayer = tester.widget<TileLayer>(find.byType(TileLayer));
expect(roadLayer.key, isNot(equals(darkLayer.key)));
});
testWidgets('tile updates preserve a manually moved viewport', (
tester,
) async {
final firstConfig = GeoConfig.fromJson({
'map_configured': true,
'tile_url_template': 'https://tiles.example/road/{z}/{x}/{y}.png',
'attribution': 'Example map data',
'user_agent_contact': 'tests@example.test',
});
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: firstConfig,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
const movedCenter = LatLng(51.15, 11.7);
map.mapController!.move(movedCenter, 8);
await tester.pumpAndSettle();
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: GeoConfig.fromJson({
'map_configured': true,
'tile_url_template': 'https://tiles.example/dark/{z}/{x}/{y}.png',
'attribution': 'Example map data',
'user_agent_contact': 'tests@example.test',
}),
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
expect(map.mapController!.camera.center, movedCenter);
expect(map.mapController!.camera.zoom, 8);
});
testWidgets('GPS refreshes do not reframe an active address search', (
tester,
) async {
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: GeoConfig.fallback(),
searchLatitude: 50.981,
searchLongitude: 11.32,
userLatitude: 50.9795,
userLongitude: 11.3235,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
final map = tester.widget<FlutterMap>(find.byType(FlutterMap));
const movedCenter = LatLng(51.15, 11.7);
map.mapController!.move(movedCenter, 8);
await tester.pumpAndSettle();
await tester.pumpWidget(
_app(
MapSurface(
items: [_item()],
meta: null,
geoConfig: GeoConfig.fallback(),
searchLatitude: 50.981,
searchLongitude: 11.32,
userLatitude: 51.5,
userLongitude: 11.9,
onItemTap: (_) {},
),
),
);
await tester.pumpAndSettle();
expect(map.mapController!.camera.center, movedCenter);
expect(map.mapController!.camera.zoom, 8);
});
}
Widget _app(Widget child) {
return MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('de')],
locale: const Locale('de'),
home: Scaffold(body: child),
);
}
DiscoveryItem _item() {
return DiscoveryItem.fromJson({
'id': 453,
'resource': 'places',
'title': 'Felsenkeller',
'geo': {'available': true, 'lat': 50.9795, 'lng': 11.3235},
});
}
