blob: 8889e4ba9577f1f8408955cb13c46c11d4f883a2 [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: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));
});
});
});
}