| // 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:html' as html; |
| |
| 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'; |
| |
| import 'google_maps_controller_test.mocks.dart'; |
| |
| @GenerateMocks(<Type>[], customMocks: <MockSpec<dynamic>>[ |
| MockSpec<CirclesController>(onMissingStub: OnMissingStub.returnDefault), |
| MockSpec<PolygonsController>(onMissingStub: OnMissingStub.returnDefault), |
| MockSpec<PolylinesController>(onMissingStub: OnMissingStub.returnDefault), |
| MockSpec<MarkersController>(onMissingStub: OnMissingStub.returnDefault), |
| ]) |
| |
| /// Test Google Map Controller |
| void main() { |
| IntegrationTestWidgetsFlutterBinding.ensureInitialized(); |
| |
| group('GoogleMapController', () { |
| const int mapId = 33930; |
| late GoogleMapController controller; |
| late StreamController<MapEvent<Object?>> stream; |
| |
| // Creates a controller with the default mapId and stream controller, and any `options` needed. |
| GoogleMapController createController({ |
| CameraPosition initialCameraPosition = |
| const CameraPosition(target: LatLng(0, 0)), |
| MapObjects mapObjects = const MapObjects(), |
| MapConfiguration mapConfiguration = const MapConfiguration(), |
| }) { |
| return GoogleMapController( |
| mapId: mapId, |
| streamController: stream, |
| widgetConfiguration: MapWidgetConfiguration( |
| initialCameraPosition: initialCameraPosition, |
| textDirection: TextDirection.ltr), |
| mapObjects: mapObjects, |
| mapConfiguration: mapConfiguration, |
| ); |
| } |
| |
| setUp(() { |
| stream = StreamController<MapEvent<Object?>>.broadcast(); |
| }); |
| |
| group('construct/dispose', () { |
| setUp(() { |
| controller = createController(); |
| }); |
| |
| testWidgets('constructor creates widget', (WidgetTester tester) async { |
| expect(controller.widget, isNotNull); |
| expect(controller.widget, isA<HtmlElementView>()); |
| expect((controller.widget! as HtmlElementView).viewType, |
| endsWith('$mapId')); |
| }); |
| |
| testWidgets('widget is cached when reused', (WidgetTester tester) async { |
| final Widget? first = controller.widget; |
| final Widget? again = controller.widget; |
| expect(identical(first, again), isTrue); |
| }); |
| |
| group('dispose', () { |
| testWidgets('closes the stream and removes the widget', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(stream.isClosed, isTrue); |
| expect(controller.widget, isNull); |
| }); |
| |
| testWidgets('cannot call getVisibleRegion after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() async { |
| await controller.getVisibleRegion(); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot call getScreenCoordinate after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() async { |
| await controller.getScreenCoordinate( |
| const LatLng(43.3072465, -5.6918241), |
| ); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot call getLatLng after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() async { |
| await controller.getLatLng( |
| const ScreenCoordinate(x: 640, y: 480), |
| ); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot call moveCamera after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() async { |
| await controller.moveCamera(CameraUpdate.zoomIn()); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot call getZoomLevel after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() async { |
| await controller.getZoomLevel(); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot updateCircles after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() { |
| controller.updateCircles( |
| CircleUpdates.from( |
| const <Circle>{}, |
| const <Circle>{}, |
| ), |
| ); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot updatePolygons after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() { |
| controller.updatePolygons( |
| PolygonUpdates.from( |
| const <Polygon>{}, |
| const <Polygon>{}, |
| ), |
| ); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot updatePolylines after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() { |
| controller.updatePolylines( |
| PolylineUpdates.from( |
| const <Polyline>{}, |
| const <Polyline>{}, |
| ), |
| ); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('cannot updateMarkers after dispose', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(() { |
| controller.updateMarkers( |
| MarkerUpdates.from( |
| const <Marker>{}, |
| const <Marker>{}, |
| ), |
| ); |
| }, throwsAssertionError); |
| |
| expect(() { |
| controller.showInfoWindow(const MarkerId('any')); |
| }, throwsAssertionError); |
| |
| expect(() { |
| controller.hideInfoWindow(const MarkerId('any')); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('isInfoWindowShown defaults to false', |
| (WidgetTester tester) async { |
| controller.dispose(); |
| |
| expect(controller.isInfoWindowShown(const MarkerId('any')), false); |
| }); |
| }); |
| }); |
| |
| group('init', () { |
| late MockCirclesController circles; |
| late MockMarkersController markers; |
| late MockPolygonsController polygons; |
| late MockPolylinesController polylines; |
| late gmaps.GMap map; |
| |
| setUp(() { |
| circles = MockCirclesController(); |
| markers = MockMarkersController(); |
| polygons = MockPolygonsController(); |
| polylines = MockPolylinesController(); |
| map = gmaps.GMap(html.DivElement()); |
| }); |
| |
| testWidgets('listens to map events', (WidgetTester tester) async { |
| controller = createController(); |
| controller.debugSetOverrides( |
| createMap: (_, __) => map, |
| circles: circles, |
| markers: markers, |
| polygons: polygons, |
| polylines: polylines, |
| ); |
| |
| controller.init(); |
| |
| // Trigger events on the map, and verify they've been broadcast to the stream |
| final Stream<MapEvent<Object?>> capturedEvents = stream.stream.take(5); |
| |
| gmaps.Event.trigger( |
| map, |
| 'click', |
| <Object>[gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)], |
| ); |
| gmaps.Event.trigger( |
| map, |
| 'rightclick', |
| <Object>[gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)], |
| ); |
| // The following line causes 2 events |
| gmaps.Event.trigger(map, 'bounds_changed', <Object>[]); |
| gmaps.Event.trigger(map, 'idle', <Object>[]); |
| |
| final List<MapEvent<Object?>> events = await capturedEvents.toList(); |
| |
| expect(events[0], isA<MapTapEvent>()); |
| expect(events[1], isA<MapLongPressEvent>()); |
| expect(events[2], isA<CameraMoveStartedEvent>()); |
| expect(events[3], isA<CameraMoveEvent>()); |
| expect(events[4], isA<CameraIdleEvent>()); |
| }); |
| |
| testWidgets("binds geometry controllers to map's", |
| (WidgetTester tester) async { |
| controller = createController(); |
| controller.debugSetOverrides( |
| createMap: (_, __) => map, |
| circles: circles, |
| markers: markers, |
| polygons: polygons, |
| polylines: polylines, |
| ); |
| |
| controller.init(); |
| |
| verify(circles.bindToMap(mapId, map)); |
| verify(markers.bindToMap(mapId, map)); |
| verify(polygons.bindToMap(mapId, map)); |
| verify(polylines.bindToMap(mapId, map)); |
| }); |
| |
| testWidgets('renders initial geometry', (WidgetTester tester) async { |
| controller = createController( |
| mapObjects: MapObjects(circles: <Circle>{ |
| const Circle( |
| circleId: CircleId('circle-1'), |
| zIndex: 1234, |
| ), |
| }, markers: <Marker>{ |
| const Marker( |
| markerId: MarkerId('marker-1'), |
| infoWindow: InfoWindow( |
| title: 'title for test', |
| snippet: 'snippet for test', |
| ), |
| ), |
| }, polygons: <Polygon>{ |
| const Polygon(polygonId: PolygonId('polygon-1'), points: <LatLng>[ |
| LatLng(43.355114, -5.851333), |
| LatLng(43.354797, -5.851860), |
| LatLng(43.354469, -5.851318), |
| LatLng(43.354762, -5.850824), |
| ]), |
| const Polygon( |
| polygonId: PolygonId('polygon-2-with-holes'), |
| points: <LatLng>[ |
| LatLng(43.355114, -5.851333), |
| LatLng(43.354797, -5.851860), |
| LatLng(43.354469, -5.851318), |
| LatLng(43.354762, -5.850824), |
| ], |
| holes: <List<LatLng>>[ |
| <LatLng>[ |
| LatLng(41.354797, -6.851860), |
| LatLng(41.354469, -6.851318), |
| LatLng(41.354762, -6.850824), |
| ] |
| ], |
| ), |
| }, polylines: <Polyline>{ |
| const Polyline(polylineId: PolylineId('polyline-1'), points: <LatLng>[ |
| LatLng(43.355114, -5.851333), |
| LatLng(43.354797, -5.851860), |
| LatLng(43.354469, -5.851318), |
| LatLng(43.354762, -5.850824), |
| ]) |
| })); |
| |
| controller.debugSetOverrides( |
| circles: circles, |
| markers: markers, |
| polygons: polygons, |
| polylines: polylines, |
| ); |
| |
| controller.init(); |
| |
| final Set<Circle> capturedCircles = |
| verify(circles.addCircles(captureAny)).captured[0] as Set<Circle>; |
| final Set<Marker> capturedMarkers = |
| verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>; |
| final Set<Polygon> capturedPolygons = |
| verify(polygons.addPolygons(captureAny)).captured[0] |
| as Set<Polygon>; |
| final Set<Polyline> capturedPolylines = |
| verify(polylines.addPolylines(captureAny)).captured[0] |
| as Set<Polyline>; |
| |
| expect(capturedCircles.first.circleId.value, 'circle-1'); |
| expect(capturedCircles.first.zIndex, 1234); |
| expect(capturedMarkers.first.markerId.value, 'marker-1'); |
| expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test'); |
| expect(capturedMarkers.first.infoWindow.title, 'title for test'); |
| expect(capturedPolygons.first.polygonId.value, 'polygon-1'); |
| expect(capturedPolygons.elementAt(1).polygonId.value, |
| 'polygon-2-with-holes'); |
| expect(capturedPolygons.elementAt(1).holes, isNot(null)); |
| expect(capturedPolylines.first.polylineId.value, 'polyline-1'); |
| }); |
| |
| testWidgets('empty infoWindow does not create InfoWindow instance.', |
| (WidgetTester tester) async { |
| controller = createController( |
| mapObjects: MapObjects(markers: <Marker>{ |
| const Marker(markerId: MarkerId('marker-1')), |
| })); |
| |
| controller.debugSetOverrides( |
| markers: markers, |
| ); |
| |
| controller.init(); |
| |
| final Set<Marker> capturedMarkers = |
| verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>; |
| |
| expect(capturedMarkers.first.infoWindow, InfoWindow.noText); |
| }); |
| |
| group('Initialization options', () { |
| gmaps.MapOptions? capturedOptions; |
| setUp(() { |
| capturedOptions = null; |
| }); |
| testWidgets('translates initial options', (WidgetTester tester) async { |
| controller = createController( |
| mapConfiguration: const MapConfiguration( |
| mapType: MapType.satellite, |
| zoomControlsEnabled: true, |
| )); |
| controller.debugSetOverrides( |
| createMap: (_, gmaps.MapOptions options) { |
| capturedOptions = options; |
| return map; |
| }); |
| |
| controller.init(); |
| |
| expect(capturedOptions, isNotNull); |
| expect(capturedOptions!.mapTypeId, gmaps.MapTypeId.SATELLITE); |
| expect(capturedOptions!.zoomControl, true); |
| expect(capturedOptions!.gestureHandling, 'auto', |
| reason: |
| 'by default the map handles zoom/pan gestures internally'); |
| }); |
| |
| testWidgets('disables gestureHandling with scrollGesturesEnabled false', |
| (WidgetTester tester) async { |
| controller = createController( |
| mapConfiguration: const MapConfiguration( |
| scrollGesturesEnabled: false, |
| )); |
| controller.debugSetOverrides( |
| createMap: (_, gmaps.MapOptions options) { |
| capturedOptions = options; |
| return map; |
| }); |
| |
| controller.init(); |
| |
| expect(capturedOptions, isNotNull); |
| expect(capturedOptions!.gestureHandling, 'none', |
| reason: |
| 'disabling scroll gestures disables all gesture handling'); |
| }); |
| |
| testWidgets('disables gestureHandling with zoomGesturesEnabled false', |
| (WidgetTester tester) async { |
| controller = createController( |
| mapConfiguration: const MapConfiguration( |
| zoomGesturesEnabled: false, |
| )); |
| controller.debugSetOverrides( |
| createMap: (_, gmaps.MapOptions options) { |
| capturedOptions = options; |
| return map; |
| }); |
| |
| controller.init(); |
| |
| expect(capturedOptions, isNotNull); |
| expect(capturedOptions!.gestureHandling, 'none', |
| reason: |
| 'disabling scroll gestures disables all gesture handling'); |
| }); |
| |
| testWidgets('sets initial position when passed', |
| (WidgetTester tester) async { |
| controller = createController( |
| initialCameraPosition: const CameraPosition( |
| target: LatLng(43.308, -5.6910), |
| zoom: 12, |
| ), |
| ); |
| |
| controller.debugSetOverrides( |
| createMap: (_, gmaps.MapOptions options) { |
| capturedOptions = options; |
| return map; |
| }); |
| |
| controller.init(); |
| |
| expect(capturedOptions, isNotNull); |
| expect(capturedOptions!.zoom, 12); |
| expect(capturedOptions!.center, isNotNull); |
| }); |
| }); |
| |
| group('Traffic Layer', () { |
| testWidgets('by default is disabled', (WidgetTester tester) async { |
| controller = createController(); |
| controller.init(); |
| expect(controller.trafficLayer, isNull); |
| }); |
| |
| testWidgets('initializes with traffic layer', |
| (WidgetTester tester) async { |
| controller = createController( |
| mapConfiguration: const MapConfiguration( |
| trafficEnabled: true, |
| )); |
| controller.debugSetOverrides(createMap: (_, __) => map); |
| controller.init(); |
| expect(controller.trafficLayer, isNotNull); |
| }); |
| }); |
| }); |
| |
| // These are the methods that are delegated to the gmaps.GMap object, that we can mock... |
| group('Map control methods', () { |
| late gmaps.GMap map; |
| |
| setUp(() { |
| map = gmaps.GMap( |
| html.DivElement(), |
| gmaps.MapOptions() |
| ..zoom = 10 |
| ..center = gmaps.LatLng(0, 0), |
| ); |
| controller = createController(); |
| controller.debugSetOverrides(createMap: (_, __) => map); |
| controller.init(); |
| }); |
| |
| group('updateRawOptions', () { |
| testWidgets('can update `options`', (WidgetTester tester) async { |
| controller.updateMapConfiguration(const MapConfiguration( |
| mapType: MapType.satellite, |
| )); |
| |
| expect(map.mapTypeId, gmaps.MapTypeId.SATELLITE); |
| }); |
| |
| testWidgets('can turn on/off traffic', (WidgetTester tester) async { |
| expect(controller.trafficLayer, isNull); |
| |
| controller.updateMapConfiguration(const MapConfiguration( |
| trafficEnabled: true, |
| )); |
| |
| expect(controller.trafficLayer, isNotNull); |
| |
| controller.updateMapConfiguration(const MapConfiguration( |
| trafficEnabled: false, |
| )); |
| |
| expect(controller.trafficLayer, isNull); |
| }); |
| }); |
| |
| group('viewport getters', () { |
| testWidgets('getVisibleRegion', (WidgetTester tester) async { |
| final gmaps.LatLng gmCenter = map.center!; |
| final LatLng center = |
| LatLng(gmCenter.lat.toDouble(), gmCenter.lng.toDouble()); |
| |
| final LatLngBounds bounds = await controller.getVisibleRegion(); |
| |
| expect(bounds.contains(center), isTrue, |
| reason: |
| 'The computed visible region must contain the center of the created map.'); |
| }); |
| |
| testWidgets('getZoomLevel', (WidgetTester tester) async { |
| expect(await controller.getZoomLevel(), map.zoom); |
| }); |
| }); |
| |
| group('moveCamera', () { |
| // Tested in projection_test.dart |
| }); |
| |
| group('map.projection methods', () { |
| // Tested in projection_test.dart |
| }); |
| }); |
| |
| // These are the methods that get forwarded to other controllers, so we just verify calls. |
| group('Pass-through methods', () { |
| setUp(() { |
| controller = createController(); |
| }); |
| |
| testWidgets('updateCircles', (WidgetTester tester) async { |
| final MockCirclesController mock = MockCirclesController(); |
| controller.debugSetOverrides(circles: mock); |
| |
| final Set<Circle> previous = <Circle>{ |
| const Circle(circleId: CircleId('to-be-updated')), |
| const Circle(circleId: CircleId('to-be-removed')), |
| }; |
| |
| final Set<Circle> current = <Circle>{ |
| const Circle(circleId: CircleId('to-be-updated'), visible: false), |
| const Circle(circleId: CircleId('to-be-added')), |
| }; |
| |
| controller.updateCircles(CircleUpdates.from(previous, current)); |
| |
| verify(mock.removeCircles(<CircleId>{ |
| const CircleId('to-be-removed'), |
| })); |
| verify(mock.addCircles(<Circle>{ |
| const Circle(circleId: CircleId('to-be-added')), |
| })); |
| verify(mock.changeCircles(<Circle>{ |
| const Circle(circleId: CircleId('to-be-updated'), visible: false), |
| })); |
| }); |
| |
| testWidgets('updateMarkers', (WidgetTester tester) async { |
| final MockMarkersController mock = MockMarkersController(); |
| controller.debugSetOverrides(markers: mock); |
| |
| final Set<Marker> previous = <Marker>{ |
| const Marker(markerId: MarkerId('to-be-updated')), |
| const Marker(markerId: MarkerId('to-be-removed')), |
| }; |
| |
| final Set<Marker> current = <Marker>{ |
| const Marker(markerId: MarkerId('to-be-updated'), visible: false), |
| const Marker(markerId: MarkerId('to-be-added')), |
| }; |
| |
| controller.updateMarkers(MarkerUpdates.from(previous, current)); |
| |
| verify(mock.removeMarkers(<MarkerId>{ |
| const MarkerId('to-be-removed'), |
| })); |
| verify(mock.addMarkers(<Marker>{ |
| const Marker(markerId: MarkerId('to-be-added')), |
| })); |
| verify(mock.changeMarkers(<Marker>{ |
| const Marker(markerId: MarkerId('to-be-updated'), visible: false), |
| })); |
| }); |
| |
| testWidgets('updatePolygons', (WidgetTester tester) async { |
| final MockPolygonsController mock = MockPolygonsController(); |
| controller.debugSetOverrides(polygons: mock); |
| |
| final Set<Polygon> previous = <Polygon>{ |
| const Polygon(polygonId: PolygonId('to-be-updated')), |
| const Polygon(polygonId: PolygonId('to-be-removed')), |
| }; |
| |
| final Set<Polygon> current = <Polygon>{ |
| const Polygon(polygonId: PolygonId('to-be-updated'), visible: false), |
| const Polygon(polygonId: PolygonId('to-be-added')), |
| }; |
| |
| controller.updatePolygons(PolygonUpdates.from(previous, current)); |
| |
| verify(mock.removePolygons(<PolygonId>{ |
| const PolygonId('to-be-removed'), |
| })); |
| verify(mock.addPolygons(<Polygon>{ |
| const Polygon(polygonId: PolygonId('to-be-added')), |
| })); |
| verify(mock.changePolygons(<Polygon>{ |
| const Polygon(polygonId: PolygonId('to-be-updated'), visible: false), |
| })); |
| }); |
| |
| testWidgets('updatePolylines', (WidgetTester tester) async { |
| final MockPolylinesController mock = MockPolylinesController(); |
| controller.debugSetOverrides(polylines: mock); |
| |
| final Set<Polyline> previous = <Polyline>{ |
| const Polyline(polylineId: PolylineId('to-be-updated')), |
| const Polyline(polylineId: PolylineId('to-be-removed')), |
| }; |
| |
| final Set<Polyline> current = <Polyline>{ |
| const Polyline( |
| polylineId: PolylineId('to-be-updated'), |
| visible: false, |
| ), |
| const Polyline(polylineId: PolylineId('to-be-added')), |
| }; |
| |
| controller.updatePolylines(PolylineUpdates.from(previous, current)); |
| |
| verify(mock.removePolylines(<PolylineId>{ |
| const PolylineId('to-be-removed'), |
| })); |
| verify(mock.addPolylines(<Polyline>{ |
| const Polyline(polylineId: PolylineId('to-be-added')), |
| })); |
| verify(mock.changePolylines(<Polyline>{ |
| const Polyline( |
| polylineId: PolylineId('to-be-updated'), |
| visible: false, |
| ), |
| })); |
| }); |
| |
| testWidgets('infoWindow visibility', (WidgetTester tester) async { |
| final MockMarkersController mock = MockMarkersController(); |
| const MarkerId markerId = MarkerId('marker-with-infowindow'); |
| when(mock.isInfoWindowShown(markerId)).thenReturn(true); |
| controller.debugSetOverrides(markers: mock); |
| |
| controller.showInfoWindow(markerId); |
| |
| verify(mock.showMarkerInfoWindow(markerId)); |
| |
| controller.hideInfoWindow(markerId); |
| |
| verify(mock.hideMarkerInfoWindow(markerId)); |
| |
| controller.isInfoWindowShown(markerId); |
| |
| verify(mock.isInfoWindowShown(markerId)); |
| }); |
| }); |
| }); |
| } |