diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/secrets.properties b/packages/google_maps_flutter/google_maps_flutter_android/example/android/secrets.properties new file mode 100644 index 000000000000..60c33085867e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/secrets.properties @@ -0,0 +1 @@ +maps.key=SomeKeyHere diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart index 6e7707b58f5b..1d791d909cd9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart @@ -420,41 +420,41 @@ class _ExampleGoogleMapState extends State { return; } final ExampleGoogleMapController controller = await _controller.future; - await controller._updateMapConfiguration(updates); + unawaited(controller._updateMapConfiguration(updates)); _mapConfiguration = newConfig; } Future _updateMarkers() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updateMarkers( - MarkerUpdates.from(_markers.values.toSet(), widget.markers)); + unawaited(controller._updateMarkers( + MarkerUpdates.from(_markers.values.toSet(), widget.markers))); _markers = keyByMarkerId(widget.markers); } Future _updatePolygons() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updatePolygons( - PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)); + unawaited(controller._updatePolygons( + PolygonUpdates.from(_polygons.values.toSet(), widget.polygons))); _polygons = keyByPolygonId(widget.polygons); } Future _updatePolylines() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updatePolylines( - PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)); + unawaited(controller._updatePolylines( + PolylineUpdates.from(_polylines.values.toSet(), widget.polylines))); _polylines = keyByPolylineId(widget.polylines); } Future _updateCircles() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updateCircles( - CircleUpdates.from(_circles.values.toSet(), widget.circles)); + unawaited(controller._updateCircles( + CircleUpdates.from(_circles.values.toSet(), widget.circles))); _circles = keyByCircleId(widget.circles); } Future _updateTileOverlays() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updateTileOverlays(widget.tileOverlays); + unawaited(controller._updateTileOverlays(widget.tileOverlays)); } Future onPlatformViewCreated(int id) async { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index 0bec2ff2a1a2..a80b30275677 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -27,6 +27,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + stream_transform: ^2.0.0 flutter: uses-material-design: true diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/test/example_google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/test/example_google_map_test.dart new file mode 100644 index 000000000000..57b6d1e2a673 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/test/example_google_map_test.dart @@ -0,0 +1,175 @@ +// 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 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_example/example_google_map.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'fake_google_maps_flutter_platform.dart'; + +Widget _mapWithObjects({ + Set circles = const {}, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set tileOverlays = const {}, +}) { + return Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + circles: circles, + markers: markers, + polygons: polygons, + polylines: polylines, + tileOverlays: tileOverlays, + ), + ); +} + +void main() { + late FakeGoogleMapsFlutterPlatform platform; + + setUp(() { + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; + }); + + testWidgets('circle updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_2')); + const Circle c3 = Circle(circleId: CircleId('circle_3'), radius: 1); + const Circle c3updated = Circle(circleId: CircleId('circle_3'), radius: 10); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(circles: {c1, c2})); + await tester.pumpWidget(_mapWithObjects(circles: {c1, c3})); + await tester.pumpWidget(_mapWithObjects(circles: {c1, c3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.circleUpdates.length, 3); + + expect(map.circleUpdates[0].circlesToChange.isEmpty, true); + expect(map.circleUpdates[0].circlesToAdd, {c1, c2}); + expect(map.circleUpdates[0].circleIdsToRemove.isEmpty, true); + + expect(map.circleUpdates[1].circlesToChange.isEmpty, true); + expect(map.circleUpdates[1].circlesToAdd, {c3}); + expect(map.circleUpdates[1].circleIdsToRemove, {c2.circleId}); + + expect(map.circleUpdates[2].circlesToChange, {c3updated}); + expect(map.circleUpdates[2].circlesToAdd.isEmpty, true); + expect(map.circleUpdates[2].circleIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); + + testWidgets('marker updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker(markerId: MarkerId('marker_2')); + const Marker m3 = Marker(markerId: MarkerId('marker_3')); + const Marker m3updated = + Marker(markerId: MarkerId('marker_3'), draggable: true); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(markers: {m1, m2})); + await tester.pumpWidget(_mapWithObjects(markers: {m1, m3})); + await tester.pumpWidget(_mapWithObjects(markers: {m1, m3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.markerUpdates.length, 3); + + expect(map.markerUpdates[0].markersToChange.isEmpty, true); + expect(map.markerUpdates[0].markersToAdd, {m1, m2}); + expect(map.markerUpdates[0].markerIdsToRemove.isEmpty, true); + + expect(map.markerUpdates[1].markersToChange.isEmpty, true); + expect(map.markerUpdates[1].markersToAdd, {m3}); + expect(map.markerUpdates[1].markerIdsToRemove, {m2.markerId}); + + expect(map.markerUpdates[2].markersToChange, {m3updated}); + expect(map.markerUpdates[2].markersToAdd.isEmpty, true); + expect(map.markerUpdates[2].markerIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); + + testWidgets('polygon updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + const Polygon p2 = Polygon(polygonId: PolygonId('polygon_2')); + const Polygon p3 = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 1); + const Polygon p3updated = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(polygons: {p1, p2})); + await tester.pumpWidget(_mapWithObjects(polygons: {p1, p3})); + await tester + .pumpWidget(_mapWithObjects(polygons: {p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polygonUpdates.length, 3); + + expect(map.polygonUpdates[0].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[0].polygonsToAdd, {p1, p2}); + expect(map.polygonUpdates[0].polygonIdsToRemove.isEmpty, true); + + expect(map.polygonUpdates[1].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[1].polygonsToAdd, {p3}); + expect(map.polygonUpdates[1].polygonIdsToRemove, {p2.polygonId}); + + expect(map.polygonUpdates[2].polygonsToChange, {p3updated}); + expect(map.polygonUpdates[2].polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates[2].polygonIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); + + testWidgets('polyline updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = Polyline(polylineId: PolylineId('polyline_2')); + const Polyline p3 = + Polyline(polylineId: PolylineId('polyline_3'), width: 1); + const Polyline p3updated = + Polyline(polylineId: PolylineId('polyline_3'), width: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(polylines: {p1, p2})); + await tester.pumpWidget(_mapWithObjects(polylines: {p1, p3})); + await tester + .pumpWidget(_mapWithObjects(polylines: {p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polylineUpdates.length, 3); + + expect(map.polylineUpdates[0].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[0].polylinesToAdd, {p1, p2}); + expect(map.polylineUpdates[0].polylineIdsToRemove.isEmpty, true); + + expect(map.polylineUpdates[1].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[1].polylinesToAdd, {p3}); + expect(map.polylineUpdates[1].polylineIdsToRemove, + {p2.polylineId}); + + expect(map.polylineUpdates[2].polylinesToChange, {p3updated}); + expect(map.polylineUpdates[2].polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates[2].polylineIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart new file mode 100644 index 000000000000..22447ba5ecad --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart @@ -0,0 +1,303 @@ +// 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 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:stream_transform/stream_transform.dart'; + +// A dummy implementation of the platform interface for tests. +class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + FakeGoogleMapsFlutterPlatform(); + + /// The IDs passed to each call to buildView, in call order. + List createdIds = []; + + /// A map of creation IDs to fake map instances. + Map mapInstances = + {}; + + PlatformMapStateRecorder get lastCreatedMap => mapInstances[createdIds.last]!; + + /// Whether to add a small delay to async calls to simulate more realistic + /// async behavior (simulating the platform channel calls most + /// implementations will do). + /// + /// When true, requires tests to `pumpAndSettle` at the end of the test + /// to avoid exceptions. + bool simulatePlatformDelay = false; + + /// Whether `dispose` has been called. + bool disposed = false; + + /// Stream controller to inject events for testing. + final StreamController> mapEventStreamController = + StreamController>.broadcast(); + + @override + Future init(int mapId) async {} + + @override + Future updateMapConfiguration( + MapConfiguration update, { + required int mapId, + }) async { + mapInstances[mapId]?.mapConfiguration = update; + await _fakeDelay(); + } + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.markerUpdates.add(markerUpdates); + await _fakeDelay(); + } + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polygonUpdates.add(polygonUpdates); + await _fakeDelay(); + } + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polylineUpdates.add(polylineUpdates); + await _fakeDelay(); + } + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.circleUpdates.add(circleUpdates); + await _fakeDelay(); + } + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) async { + mapInstances[mapId]?.tileOverlaySets.add(newTileOverlays); + await _fakeDelay(); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) async {} + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async {} + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + return LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); + } + + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + return const ScreenCoordinate(x: 0, y: 0); + } + + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + return const LatLng(0, 0); + } + + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + return false; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return 0.0; + } + + @override + Future takeSnapshot({ + required int mapId, + }) async { + return null; + } + + @override + Stream onCameraMoveStarted({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragStart({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + void dispose({required int mapId}) { + disposed = true; + } + + @override + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), + }) { + final PlatformMapStateRecorder? instance = mapInstances[creationId]; + if (instance == null) { + createdIds.add(creationId); + mapInstances[creationId] = PlatformMapStateRecorder( + widgetConfiguration: widgetConfiguration, + mapConfiguration: mapConfiguration, + mapObjects: mapObjects); + onPlatformViewCreated(creationId); + } + return Container(); + } + + Future _fakeDelay() async { + if (!simulatePlatformDelay) { + return; + } + return Future.delayed(const Duration(microseconds: 1)); + } +} + +/// A fake implementation of a native map, which stores all the updates it is +/// sent for inspection in tests. +class PlatformMapStateRecorder { + PlatformMapStateRecorder({ + required this.widgetConfiguration, + this.mapObjects = const MapObjects(), + this.mapConfiguration = const MapConfiguration(), + }) { + markerUpdates.add(MarkerUpdates.from(const {}, mapObjects.markers)); + polygonUpdates + .add(PolygonUpdates.from(const {}, mapObjects.polygons)); + polylineUpdates + .add(PolylineUpdates.from(const {}, mapObjects.polylines)); + circleUpdates.add(CircleUpdates.from(const {}, mapObjects.circles)); + tileOverlaySets.add(mapObjects.tileOverlays); + } + + MapWidgetConfiguration widgetConfiguration; + MapObjects mapObjects; + MapConfiguration mapConfiguration; + + final List markerUpdates = []; + final List polygonUpdates = []; + final List polylineUpdates = []; + final List circleUpdates = []; + final List> tileOverlaySets = >[]; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart index 6e7707b58f5b..1d791d909cd9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart @@ -420,41 +420,41 @@ class _ExampleGoogleMapState extends State { return; } final ExampleGoogleMapController controller = await _controller.future; - await controller._updateMapConfiguration(updates); + unawaited(controller._updateMapConfiguration(updates)); _mapConfiguration = newConfig; } Future _updateMarkers() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updateMarkers( - MarkerUpdates.from(_markers.values.toSet(), widget.markers)); + unawaited(controller._updateMarkers( + MarkerUpdates.from(_markers.values.toSet(), widget.markers))); _markers = keyByMarkerId(widget.markers); } Future _updatePolygons() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updatePolygons( - PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)); + unawaited(controller._updatePolygons( + PolygonUpdates.from(_polygons.values.toSet(), widget.polygons))); _polygons = keyByPolygonId(widget.polygons); } Future _updatePolylines() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updatePolylines( - PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)); + unawaited(controller._updatePolylines( + PolylineUpdates.from(_polylines.values.toSet(), widget.polylines))); _polylines = keyByPolylineId(widget.polylines); } Future _updateCircles() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updateCircles( - CircleUpdates.from(_circles.values.toSet(), widget.circles)); + unawaited(controller._updateCircles( + CircleUpdates.from(_circles.values.toSet(), widget.circles))); _circles = keyByCircleId(widget.circles); } Future _updateTileOverlays() async { final ExampleGoogleMapController controller = await _controller.future; - await controller._updateTileOverlays(widget.tileOverlays); + unawaited(controller._updateTileOverlays(widget.tileOverlays)); } Future onPlatformViewCreated(int id) async { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml index c60d25a1ad1c..095292153b8f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml @@ -20,5 +20,10 @@ dependencies: path: ../../../ google_maps_flutter_platform_interface: ^2.2.1 +dev_dependencies: + flutter_test: + sdk: flutter + stream_transform: ^2.0.0 + flutter: uses-material-design: true diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/example_google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/example_google_map_test.dart new file mode 100644 index 000000000000..4a1d02c7b65f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/example_google_map_test.dart @@ -0,0 +1,175 @@ +// 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 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:maps_example_dart/example_google_map.dart'; + +import 'fake_google_maps_flutter_platform.dart'; + +Widget _mapWithObjects({ + Set circles = const {}, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set tileOverlays = const {}, +}) { + return Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + circles: circles, + markers: markers, + polygons: polygons, + polylines: polylines, + tileOverlays: tileOverlays, + ), + ); +} + +void main() { + late FakeGoogleMapsFlutterPlatform platform; + + setUp(() { + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; + }); + + testWidgets('circle updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_2')); + const Circle c3 = Circle(circleId: CircleId('circle_3'), radius: 1); + const Circle c3updated = Circle(circleId: CircleId('circle_3'), radius: 10); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(circles: {c1, c2})); + await tester.pumpWidget(_mapWithObjects(circles: {c1, c3})); + await tester.pumpWidget(_mapWithObjects(circles: {c1, c3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.circleUpdates.length, 3); + + expect(map.circleUpdates[0].circlesToChange.isEmpty, true); + expect(map.circleUpdates[0].circlesToAdd, {c1, c2}); + expect(map.circleUpdates[0].circleIdsToRemove.isEmpty, true); + + expect(map.circleUpdates[1].circlesToChange.isEmpty, true); + expect(map.circleUpdates[1].circlesToAdd, {c3}); + expect(map.circleUpdates[1].circleIdsToRemove, {c2.circleId}); + + expect(map.circleUpdates[2].circlesToChange, {c3updated}); + expect(map.circleUpdates[2].circlesToAdd.isEmpty, true); + expect(map.circleUpdates[2].circleIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); + + testWidgets('marker updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker(markerId: MarkerId('marker_2')); + const Marker m3 = Marker(markerId: MarkerId('marker_3')); + const Marker m3updated = + Marker(markerId: MarkerId('marker_3'), draggable: true); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(markers: {m1, m2})); + await tester.pumpWidget(_mapWithObjects(markers: {m1, m3})); + await tester.pumpWidget(_mapWithObjects(markers: {m1, m3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.markerUpdates.length, 3); + + expect(map.markerUpdates[0].markersToChange.isEmpty, true); + expect(map.markerUpdates[0].markersToAdd, {m1, m2}); + expect(map.markerUpdates[0].markerIdsToRemove.isEmpty, true); + + expect(map.markerUpdates[1].markersToChange.isEmpty, true); + expect(map.markerUpdates[1].markersToAdd, {m3}); + expect(map.markerUpdates[1].markerIdsToRemove, {m2.markerId}); + + expect(map.markerUpdates[2].markersToChange, {m3updated}); + expect(map.markerUpdates[2].markersToAdd.isEmpty, true); + expect(map.markerUpdates[2].markerIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); + + testWidgets('polygon updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + const Polygon p2 = Polygon(polygonId: PolygonId('polygon_2')); + const Polygon p3 = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 1); + const Polygon p3updated = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(polygons: {p1, p2})); + await tester.pumpWidget(_mapWithObjects(polygons: {p1, p3})); + await tester + .pumpWidget(_mapWithObjects(polygons: {p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polygonUpdates.length, 3); + + expect(map.polygonUpdates[0].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[0].polygonsToAdd, {p1, p2}); + expect(map.polygonUpdates[0].polygonIdsToRemove.isEmpty, true); + + expect(map.polygonUpdates[1].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[1].polygonsToAdd, {p3}); + expect(map.polygonUpdates[1].polygonIdsToRemove, {p2.polygonId}); + + expect(map.polygonUpdates[2].polygonsToChange, {p3updated}); + expect(map.polygonUpdates[2].polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates[2].polygonIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); + + testWidgets('polyline updates with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = Polyline(polylineId: PolylineId('polyline_2')); + const Polyline p3 = + Polyline(polylineId: PolylineId('polyline_3'), width: 1); + const Polyline p3updated = + Polyline(polylineId: PolylineId('polyline_3'), width: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithObjects(polylines: {p1, p2})); + await tester.pumpWidget(_mapWithObjects(polylines: {p1, p3})); + await tester + .pumpWidget(_mapWithObjects(polylines: {p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polylineUpdates.length, 3); + + expect(map.polylineUpdates[0].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[0].polylinesToAdd, {p1, p2}); + expect(map.polylineUpdates[0].polylineIdsToRemove.isEmpty, true); + + expect(map.polylineUpdates[1].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[1].polylinesToAdd, {p3}); + expect(map.polylineUpdates[1].polylineIdsToRemove, + {p2.polylineId}); + + expect(map.polylineUpdates[2].polylinesToChange, {p3updated}); + expect(map.polylineUpdates[2].polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates[2].polylineIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart new file mode 100644 index 000000000000..22447ba5ecad --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart @@ -0,0 +1,303 @@ +// 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 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:stream_transform/stream_transform.dart'; + +// A dummy implementation of the platform interface for tests. +class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + FakeGoogleMapsFlutterPlatform(); + + /// The IDs passed to each call to buildView, in call order. + List createdIds = []; + + /// A map of creation IDs to fake map instances. + Map mapInstances = + {}; + + PlatformMapStateRecorder get lastCreatedMap => mapInstances[createdIds.last]!; + + /// Whether to add a small delay to async calls to simulate more realistic + /// async behavior (simulating the platform channel calls most + /// implementations will do). + /// + /// When true, requires tests to `pumpAndSettle` at the end of the test + /// to avoid exceptions. + bool simulatePlatformDelay = false; + + /// Whether `dispose` has been called. + bool disposed = false; + + /// Stream controller to inject events for testing. + final StreamController> mapEventStreamController = + StreamController>.broadcast(); + + @override + Future init(int mapId) async {} + + @override + Future updateMapConfiguration( + MapConfiguration update, { + required int mapId, + }) async { + mapInstances[mapId]?.mapConfiguration = update; + await _fakeDelay(); + } + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.markerUpdates.add(markerUpdates); + await _fakeDelay(); + } + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polygonUpdates.add(polygonUpdates); + await _fakeDelay(); + } + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polylineUpdates.add(polylineUpdates); + await _fakeDelay(); + } + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.circleUpdates.add(circleUpdates); + await _fakeDelay(); + } + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) async { + mapInstances[mapId]?.tileOverlaySets.add(newTileOverlays); + await _fakeDelay(); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) async {} + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async {} + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + return LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); + } + + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + return const ScreenCoordinate(x: 0, y: 0); + } + + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + return const LatLng(0, 0); + } + + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + return false; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return 0.0; + } + + @override + Future takeSnapshot({ + required int mapId, + }) async { + return null; + } + + @override + Stream onCameraMoveStarted({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragStart({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + void dispose({required int mapId}) { + disposed = true; + } + + @override + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), + }) { + final PlatformMapStateRecorder? instance = mapInstances[creationId]; + if (instance == null) { + createdIds.add(creationId); + mapInstances[creationId] = PlatformMapStateRecorder( + widgetConfiguration: widgetConfiguration, + mapConfiguration: mapConfiguration, + mapObjects: mapObjects); + onPlatformViewCreated(creationId); + } + return Container(); + } + + Future _fakeDelay() async { + if (!simulatePlatformDelay) { + return; + } + return Future.delayed(const Duration(microseconds: 1)); + } +} + +/// A fake implementation of a native map, which stores all the updates it is +/// sent for inspection in tests. +class PlatformMapStateRecorder { + PlatformMapStateRecorder({ + required this.widgetConfiguration, + this.mapObjects = const MapObjects(), + this.mapConfiguration = const MapConfiguration(), + }) { + markerUpdates.add(MarkerUpdates.from(const {}, mapObjects.markers)); + polygonUpdates + .add(PolygonUpdates.from(const {}, mapObjects.polygons)); + polylineUpdates + .add(PolylineUpdates.from(const {}, mapObjects.polylines)); + circleUpdates.add(CircleUpdates.from(const {}, mapObjects.circles)); + tileOverlaySets.add(mapObjects.tileOverlays); + } + + MapWidgetConfiguration widgetConfiguration; + MapObjects mapObjects; + MapConfiguration mapConfiguration; + + final List markerUpdates = []; + final List polygonUpdates = []; + final List polylineUpdates = []; + final List circleUpdates = []; + final List> tileOverlaySets = >[]; +}