blob: 36b4d11e07d05ae20e34fcd207db91354bad4eeb [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:js_util' show getProperty;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_maps/google_maps.dart' as gmaps;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
import 'package:integration_test/integration_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateNiceMocks(<MockSpec<dynamic>>[MockSpec<GoogleMapController>()])
import 'google_maps_plugin_test.mocks.dart';
/// Test GoogleMapsPlugin
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('GoogleMapsPlugin', () {
late MockGoogleMapController controller;
late GoogleMapsPlugin plugin;
late Completer<int> reportedMapIdCompleter;
int numberOnPlatformViewCreatedCalls = 0;
void onPlatformViewCreated(int id) {
reportedMapIdCompleter.complete(id);
numberOnPlatformViewCreatedCalls++;
}
setUp(() {
controller = MockGoogleMapController();
plugin = GoogleMapsPlugin();
reportedMapIdCompleter = Completer<int>();
});
group('init/dispose', () {
group('before buildWidget', () {
testWidgets('init throws assertion', (WidgetTester tester) async {
expect(() => plugin.init(0), throwsAssertionError);
});
});
group('after buildWidget', () {
setUp(() {
plugin.debugSetMapById(<int, GoogleMapController>{0: controller});
});
testWidgets('cannot call methods after dispose',
(WidgetTester tester) async {
plugin.dispose(mapId: 0);
verify(controller.dispose());
expect(
() => plugin.init(0),
throwsAssertionError,
reason: 'Method calls should fail after dispose.',
);
});
});
});
group('buildView', () {
const int testMapId = 33930;
const CameraPosition initialCameraPosition =
CameraPosition(target: LatLng(0, 0));
testWidgets(
'returns an HtmlElementView and caches the controller for later',
(WidgetTester tester) async {
final Map<int, GoogleMapController> cache =
<int, GoogleMapController>{};
plugin.debugSetMapById(cache);
final Widget widget = plugin.buildViewWithConfiguration(
testMapId,
onPlatformViewCreated,
widgetConfiguration: const MapWidgetConfiguration(
initialCameraPosition: initialCameraPosition,
textDirection: TextDirection.ltr,
),
);
expect(widget, isA<HtmlElementView>());
expect(
(widget as HtmlElementView).viewType,
contains('$testMapId'),
reason:
'view type should contain the mapId passed when creating the map.',
);
expect(cache, contains(testMapId));
expect(
cache[testMapId],
isNotNull,
reason: 'cached controller cannot be null.',
);
expect(
cache[testMapId]!.isInitialized,
isTrue,
reason: 'buildView calls init on the controller',
);
});
testWidgets('returns cached instance if it already exists',
(WidgetTester tester) async {
const HtmlElementView expected =
HtmlElementView(viewType: 'only-for-testing');
when(controller.widget).thenReturn(expected);
plugin.debugSetMapById(<int, GoogleMapController>{
testMapId: controller,
});
final Widget widget = plugin.buildViewWithConfiguration(
testMapId,
onPlatformViewCreated,
widgetConfiguration: const MapWidgetConfiguration(
initialCameraPosition: initialCameraPosition,
textDirection: TextDirection.ltr,
),
);
expect(widget, equals(expected));
});
testWidgets(
'asynchronously reports onPlatformViewCreated the first time it happens',
(WidgetTester tester) async {
final Map<int, GoogleMapController> cache =
<int, GoogleMapController>{};
plugin.debugSetMapById(cache);
plugin.buildViewWithConfiguration(
testMapId,
onPlatformViewCreated,
widgetConfiguration: const MapWidgetConfiguration(
initialCameraPosition: initialCameraPosition,
textDirection: TextDirection.ltr,
),
);
// Simulate Google Maps JS SDK being "ready"
cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId));
expect(
cache[testMapId]!.isInitialized,
isTrue,
reason: 'buildView calls init on the controller',
);
expect(
await reportedMapIdCompleter.future,
testMapId,
reason: 'Should call onPlatformViewCreated with the mapId',
);
// Fire repeated event again...
cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId));
expect(
numberOnPlatformViewCreatedCalls,
equals(1),
reason:
'Should not call onPlatformViewCreated for the same controller multiple times',
);
});
});
group('setMapStyles', () {
const String mapStyle = '''
[{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [{"color": "#6b9a76"}]
}]''';
testWidgets('translates styles for controller',
(WidgetTester tester) async {
plugin.debugSetMapById(<int, GoogleMapController>{0: controller});
await plugin.setMapStyle(mapStyle, mapId: 0);
final dynamic captured =
verify(controller.updateStyles(captureThat(isList))).captured[0];
final List<gmaps.MapTypeStyle> styles =
captured as List<gmaps.MapTypeStyle>;
expect(styles.length, 1);
// Let's peek inside the styles...
final gmaps.MapTypeStyle style = styles[0];
expect(style.featureType, 'poi.park');
expect(style.elementType, 'labels.text.fill');
expect(style.stylers?.length, 1);
expect(getProperty<String>(style.stylers![0]!, 'color'), '#6b9a76');
});
testWidgets('throws MapStyleException for invalid styles',
(WidgetTester tester) async {
plugin.debugSetMapById(<int, GoogleMapController>{0: controller});
expect(() async {
await plugin.setMapStyle('invalid_style', mapId: 0);
}, throwsA(isA<MapStyleException>()));
});
});
// These methods only pass-through values from the plugin to the controller
// so we verify them all together here...
group('Pass-through methods:', () {
const int mapId = 0;
setUp(() {
plugin.debugSetMapById(<int, GoogleMapController>{mapId: controller});
});
// Options
testWidgets('updateMapConfiguration', (WidgetTester tester) async {
const MapConfiguration configuration =
MapConfiguration(mapType: MapType.satellite);
await plugin.updateMapConfiguration(configuration, mapId: mapId);
verify(controller.updateMapConfiguration(configuration));
});
// Geometry
testWidgets('updateMarkers', (WidgetTester tester) async {
final MarkerUpdates expectedUpdates = MarkerUpdates.from(
const <Marker>{},
const <Marker>{},
);
await plugin.updateMarkers(expectedUpdates, mapId: mapId);
verify(controller.updateMarkers(expectedUpdates));
});
testWidgets('updatePolygons', (WidgetTester tester) async {
final PolygonUpdates expectedUpdates = PolygonUpdates.from(
const <Polygon>{},
const <Polygon>{},
);
await plugin.updatePolygons(expectedUpdates, mapId: mapId);
verify(controller.updatePolygons(expectedUpdates));
});
testWidgets('updatePolylines', (WidgetTester tester) async {
final PolylineUpdates expectedUpdates = PolylineUpdates.from(
const <Polyline>{},
const <Polyline>{},
);
await plugin.updatePolylines(expectedUpdates, mapId: mapId);
verify(controller.updatePolylines(expectedUpdates));
});
testWidgets('updateCircles', (WidgetTester tester) async {
final CircleUpdates expectedUpdates = CircleUpdates.from(
const <Circle>{},
const <Circle>{},
);
await plugin.updateCircles(expectedUpdates, mapId: mapId);
verify(controller.updateCircles(expectedUpdates));
});
// Tile Overlays
testWidgets('updateTileOverlays', (WidgetTester tester) async {
final Set<TileOverlay> expectedOverlays = <TileOverlay>{
const TileOverlay(tileOverlayId: TileOverlayId('overlay'))
};
await plugin.updateTileOverlays(
newTileOverlays: expectedOverlays, mapId: mapId);
verify(controller.updateTileOverlays(expectedOverlays));
});
testWidgets('clearTileCache', (WidgetTester tester) async {
const TileOverlayId tileOverlayId = TileOverlayId('Dory');
await plugin.clearTileCache(tileOverlayId, mapId: mapId);
verify(controller.clearTileCache(tileOverlayId));
});
// Camera
testWidgets('animateCamera', (WidgetTester tester) async {
final CameraUpdate expectedUpdates = CameraUpdate.newLatLng(
const LatLng(43.3626, -5.8433),
);
await plugin.animateCamera(expectedUpdates, mapId: mapId);
verify(controller.moveCamera(expectedUpdates));
});
testWidgets('moveCamera', (WidgetTester tester) async {
final CameraUpdate expectedUpdates = CameraUpdate.newLatLng(
const LatLng(43.3628, -5.8478),
);
await plugin.moveCamera(expectedUpdates, mapId: mapId);
verify(controller.moveCamera(expectedUpdates));
});
// Viewport
testWidgets('getVisibleRegion', (WidgetTester tester) async {
when(controller.getVisibleRegion())
.thenAnswer((_) async => LatLngBounds(
northeast: const LatLng(47.2359634, -68.0192019),
southwest: const LatLng(34.5019594, -120.4974629),
));
await plugin.getVisibleRegion(mapId: mapId);
verify(controller.getVisibleRegion());
});
testWidgets('getZoomLevel', (WidgetTester tester) async {
when(controller.getZoomLevel()).thenAnswer((_) async => 10);
await plugin.getZoomLevel(mapId: mapId);
verify(controller.getZoomLevel());
});
testWidgets('getScreenCoordinate', (WidgetTester tester) async {
when(controller.getScreenCoordinate(any)).thenAnswer(
(_) async => const ScreenCoordinate(x: 320, y: 240) // fake return
);
const LatLng latLng = LatLng(43.3613, -5.8499);
await plugin.getScreenCoordinate(latLng, mapId: mapId);
verify(controller.getScreenCoordinate(latLng));
});
testWidgets('getLatLng', (WidgetTester tester) async {
when(controller.getLatLng(any)).thenAnswer(
(_) async => const LatLng(43.3613, -5.8499) // fake return
);
const ScreenCoordinate coordinates = ScreenCoordinate(x: 19, y: 26);
await plugin.getLatLng(coordinates, mapId: mapId);
verify(controller.getLatLng(coordinates));
});
// InfoWindows
testWidgets('showMarkerInfoWindow', (WidgetTester tester) async {
const MarkerId markerId = MarkerId('testing-123');
await plugin.showMarkerInfoWindow(markerId, mapId: mapId);
verify(controller.showInfoWindow(markerId));
});
testWidgets('hideMarkerInfoWindow', (WidgetTester tester) async {
const MarkerId markerId = MarkerId('testing-123');
await plugin.hideMarkerInfoWindow(markerId, mapId: mapId);
verify(controller.hideInfoWindow(markerId));
});
testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async {
when(controller.isInfoWindowShown(any)).thenReturn(true);
const MarkerId markerId = MarkerId('testing-123');
await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId);
verify(controller.isInfoWindowShown(markerId));
});
});
// Verify all event streams are filtered correctly from the main one...
group('Event Streams', () {
const int mapId = 0;
late StreamController<MapEvent<Object?>> streamController;
setUp(() {
streamController = StreamController<MapEvent<Object?>>.broadcast();
when(controller.events)
.thenAnswer((Invocation realInvocation) => streamController.stream);
plugin.debugSetMapById(<int, GoogleMapController>{mapId: controller});
});
// Dispatches a few events in the global streamController, and expects *only* the passed event to be there.
Future<void> testStreamFiltering(
Stream<MapEvent<Object?>> stream, MapEvent<Object?> event) async {
Timer.run(() {
streamController.add(_OtherMapEvent(mapId));
streamController.add(event);
streamController.add(_OtherMapEvent(mapId));
streamController.close();
});
final List<MapEvent<Object?>> events = await stream.toList();
expect(events.length, 1);
expect(events[0], event);
}
// Camera events
testWidgets('onCameraMoveStarted', (WidgetTester tester) async {
final CameraMoveStartedEvent event = CameraMoveStartedEvent(mapId);
final Stream<CameraMoveStartedEvent> stream =
plugin.onCameraMoveStarted(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onCameraMoveStarted', (WidgetTester tester) async {
final CameraMoveEvent event = CameraMoveEvent(
mapId,
const CameraPosition(
target: LatLng(43.3790, -5.8660),
),
);
final Stream<CameraMoveEvent> stream =
plugin.onCameraMove(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onCameraIdle', (WidgetTester tester) async {
final CameraIdleEvent event = CameraIdleEvent(mapId);
final Stream<CameraIdleEvent> stream =
plugin.onCameraIdle(mapId: mapId);
await testStreamFiltering(stream, event);
});
// Marker events
testWidgets('onMarkerTap', (WidgetTester tester) async {
final MarkerTapEvent event = MarkerTapEvent(
mapId,
const MarkerId('test-123'),
);
final Stream<MarkerTapEvent> stream = plugin.onMarkerTap(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onInfoWindowTap', (WidgetTester tester) async {
final InfoWindowTapEvent event = InfoWindowTapEvent(
mapId,
const MarkerId('test-123'),
);
final Stream<InfoWindowTapEvent> stream =
plugin.onInfoWindowTap(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onMarkerDragStart', (WidgetTester tester) async {
final MarkerDragStartEvent event = MarkerDragStartEvent(
mapId,
const LatLng(43.3677, -5.8372),
const MarkerId('test-123'),
);
final Stream<MarkerDragStartEvent> stream =
plugin.onMarkerDragStart(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onMarkerDrag', (WidgetTester tester) async {
final MarkerDragEvent event = MarkerDragEvent(
mapId,
const LatLng(43.3677, -5.8372),
const MarkerId('test-123'),
);
final Stream<MarkerDragEvent> stream =
plugin.onMarkerDrag(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onMarkerDragEnd', (WidgetTester tester) async {
final MarkerDragEndEvent event = MarkerDragEndEvent(
mapId,
const LatLng(43.3677, -5.8372),
const MarkerId('test-123'),
);
final Stream<MarkerDragEndEvent> stream =
plugin.onMarkerDragEnd(mapId: mapId);
await testStreamFiltering(stream, event);
});
// Geometry
testWidgets('onPolygonTap', (WidgetTester tester) async {
final PolygonTapEvent event = PolygonTapEvent(
mapId,
const PolygonId('test-123'),
);
final Stream<PolygonTapEvent> stream =
plugin.onPolygonTap(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onPolylineTap', (WidgetTester tester) async {
final PolylineTapEvent event = PolylineTapEvent(
mapId,
const PolylineId('test-123'),
);
final Stream<PolylineTapEvent> stream =
plugin.onPolylineTap(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onCircleTap', (WidgetTester tester) async {
final CircleTapEvent event = CircleTapEvent(
mapId,
const CircleId('test-123'),
);
final Stream<CircleTapEvent> stream = plugin.onCircleTap(mapId: mapId);
await testStreamFiltering(stream, event);
});
// Map taps
testWidgets('onTap', (WidgetTester tester) async {
final MapTapEvent event = MapTapEvent(
mapId,
const LatLng(43.3597, -5.8458),
);
final Stream<MapTapEvent> stream = plugin.onTap(mapId: mapId);
await testStreamFiltering(stream, event);
});
testWidgets('onLongPress', (WidgetTester tester) async {
final MapLongPressEvent event = MapLongPressEvent(
mapId,
const LatLng(43.3608, -5.8425),
);
final Stream<MapLongPressEvent> stream =
plugin.onLongPress(mapId: mapId);
await testStreamFiltering(stream, event);
});
});
});
}
class _OtherMapEvent extends MapEvent<Object?> {
_OtherMapEvent(int mapId) : super(mapId, null);
}