mirrored_repositories/awesome-flutter/examples/plugins/packages/google_maps_flutter/google_maps_flutter_web/example
// 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';

// This value is used when comparing long~num, like
// LatLng values.
const _acceptableDelta = 0.0000000001;

@GenerateMocks([], customMocks: [
  MockSpec<CirclesController>(returnNullOnMissingStub: true),
  MockSpec<PolygonsController>(returnNullOnMissingStub: true),
  MockSpec<PolylinesController>(returnNullOnMissingStub: true),
  MockSpec<MarkersController>(returnNullOnMissingStub: true),
])

/// Test Google Map Controller
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('GoogleMapController', () {
    final int mapId = 33930;
    late GoogleMapController controller;
    late StreamController<MapEvent> 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)),
      Set<Marker> markers = const <Marker>{},
      Set<Polygon> polygons = const <Polygon>{},
      Set<Polyline> polylines = const <Polyline>{},
      Set<Circle> circles = const <Circle>{},
      Map<String, dynamic> options = const <String, dynamic>{},
    }) {
      return GoogleMapController(
        mapId: mapId,
        streamController: stream,
        initialCameraPosition: initialCameraPosition,
        markers: markers,
        polygons: polygons,
        polylines: polylines,
        circles: circles,
        mapOptions: options,
      );
    }

    setUp(() {
      stream = StreamController<MapEvent>.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 first = controller.widget;
        final 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(
              LatLng(43.3072465, -5.6918241),
            );
          }, throwsAssertionError);
        });

        testWidgets('cannot call getLatLng after dispose',
            (WidgetTester tester) async {
          controller.dispose();

          expect(() async {
            await controller.getLatLng(
              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({}, {}));
          }, throwsAssertionError);
        });

        testWidgets('cannot updatePolygons after dispose',
            (WidgetTester tester) async {
          controller.dispose();

          expect(() {
            controller.updatePolygons(PolygonUpdates.from({}, {}));
          }, throwsAssertionError);
        });

        testWidgets('cannot updatePolylines after dispose',
            (WidgetTester tester) async {
          controller.dispose();

          expect(() {
            controller.updatePolylines(PolylineUpdates.from({}, {}));
          }, throwsAssertionError);
        });

        testWidgets('cannot updateMarkers after dispose',
            (WidgetTester tester) async {
          controller.dispose();

          expect(() {
            controller.updateMarkers(MarkerUpdates.from({}, {}));
          }, throwsAssertionError);

          expect(() {
            controller.showInfoWindow(MarkerId('any'));
          }, throwsAssertionError);

          expect(() {
            controller.hideInfoWindow(MarkerId('any'));
          }, throwsAssertionError);
        });

        testWidgets('isInfoWindowShown defaults to false',
            (WidgetTester tester) async {
          controller.dispose();

          expect(controller.isInfoWindowShown(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 capturedEvents = stream.stream.take(5);

        gmaps.Event.trigger(
            map, 'click', [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]);
        gmaps.Event.trigger(map, 'rightclick',
            [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]);
        gmaps.Event.trigger(map, 'bounds_changed', []); // Causes 2 events
        gmaps.Event.trigger(map, 'idle', []);

        final 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(circles: <Circle>{
          Circle(circleId: CircleId('circle-1'))
        }, markers: <Marker>{
          Marker(
              markerId: MarkerId('marker-1'),
              infoWindow: InfoWindow(
                  title: 'title for test', snippet: 'snippet for test'))
        }, polygons: {
          Polygon(polygonId: PolygonId('polygon-1'), points: [
            LatLng(43.355114, -5.851333),
            LatLng(43.354797, -5.851860),
            LatLng(43.354469, -5.851318),
            LatLng(43.354762, -5.850824),
          ]),
          Polygon(
            polygonId: PolygonId('polygon-2-with-holes'),
            points: [
              LatLng(43.355114, -5.851333),
              LatLng(43.354797, -5.851860),
              LatLng(43.354469, -5.851318),
              LatLng(43.354762, -5.850824),
            ],
            holes: [
              [
                LatLng(41.354797, -6.851860),
                LatLng(41.354469, -6.851318),
                LatLng(41.354762, -6.850824),
              ]
            ],
          ),
        }, polylines: {
          Polyline(polylineId: PolylineId('polyline-1'), points: [
            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 capturedCircles =
            verify(circles.addCircles(captureAny)).captured[0] as Set<Circle>;
        final capturedMarkers =
            verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>;
        final capturedPolygons = verify(polygons.addPolygons(captureAny))
            .captured[0] as Set<Polygon>;
        final capturedPolylines = verify(polylines.addPolylines(captureAny))
            .captured[0] as Set<Polyline>;

        expect(capturedCircles.first.circleId.value, 'circle-1');
        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(markers: {
          Marker(markerId: MarkerId('marker-1')),
        });

        controller.debugSetOverrides(
          markers: markers,
        );

        controller.init();

        final 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(options: {
            'mapType': 2,
            'zoomControlsEnabled': true,
          });
          controller.debugSetOverrides(createMap: (_, 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(options: {
            'scrollGesturesEnabled': false,
          });
          controller.debugSetOverrides(createMap: (_, 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(options: {
            'zoomGesturesEnabled': false,
          });
          controller.debugSetOverrides(createMap: (_, 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: CameraPosition(
              target: LatLng(43.308, -5.6910),
              zoom: 12,
              bearing: 0,
              tilt: 0,
            ),
          );

          controller.debugSetOverrides(createMap: (_, 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(options: {
            '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.updateRawOptions({
            'mapType': 2,
          });

          expect(map.mapTypeId, gmaps.MapTypeId.SATELLITE);
        });

        testWidgets('can turn on/off traffic', (WidgetTester tester) async {
          expect(controller.trafficLayer, isNull);

          controller.updateRawOptions({
            'trafficEnabled': true,
          });

          expect(controller.trafficLayer, isNotNull);

          controller.updateRawOptions({
            'trafficEnabled': false,
          });

          expect(controller.trafficLayer, isNull);
        });
      });

      group('viewport getters', () {
        testWidgets('getVisibleRegion', (WidgetTester tester) async {
          final gmCenter = map.center!;
          final center =
              LatLng(gmCenter.lat.toDouble(), gmCenter.lng.toDouble());

          final 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', () {
        testWidgets('newLatLngZoom', (WidgetTester tester) async {
          await (controller
              .moveCamera(CameraUpdate.newLatLngZoom(LatLng(19, 26), 12)));

          final gmCenter = map.center!;

          expect(map.zoom, 12);
          expect(gmCenter.lat, closeTo(19, _acceptableDelta));
          expect(gmCenter.lng, closeTo(26, _acceptableDelta));
        });
      });

      group('map.projection methods', () {
        // These are too much for dart mockito, can't mock:
        // map.projection.method() (in Javascript ;) )

        // Caused https://github.com/flutter/flutter/issues/67606
      });
    });

    // 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 mock = MockCirclesController();
        controller.debugSetOverrides(circles: mock);

        final previous = {
          Circle(circleId: CircleId('to-be-updated')),
          Circle(circleId: CircleId('to-be-removed')),
        };

        final current = {
          Circle(circleId: CircleId('to-be-updated'), visible: false),
          Circle(circleId: CircleId('to-be-added')),
        };

        controller.updateCircles(CircleUpdates.from(previous, current));

        verify(mock.removeCircles({
          CircleId('to-be-removed'),
        }));
        verify(mock.addCircles({
          Circle(circleId: CircleId('to-be-added')),
        }));
        verify(mock.changeCircles({
          Circle(circleId: CircleId('to-be-updated'), visible: false),
        }));
      });

      testWidgets('updateMarkers', (WidgetTester tester) async {
        final mock = MockMarkersController();
        controller.debugSetOverrides(markers: mock);

        final previous = {
          Marker(markerId: MarkerId('to-be-updated')),
          Marker(markerId: MarkerId('to-be-removed')),
        };

        final current = {
          Marker(markerId: MarkerId('to-be-updated'), visible: false),
          Marker(markerId: MarkerId('to-be-added')),
        };

        controller.updateMarkers(MarkerUpdates.from(previous, current));

        verify(mock.removeMarkers({
          MarkerId('to-be-removed'),
        }));
        verify(mock.addMarkers({
          Marker(markerId: MarkerId('to-be-added')),
        }));
        verify(mock.changeMarkers({
          Marker(markerId: MarkerId('to-be-updated'), visible: false),
        }));
      });

      testWidgets('updatePolygons', (WidgetTester tester) async {
        final mock = MockPolygonsController();
        controller.debugSetOverrides(polygons: mock);

        final previous = {
          Polygon(polygonId: PolygonId('to-be-updated')),
          Polygon(polygonId: PolygonId('to-be-removed')),
        };

        final current = {
          Polygon(polygonId: PolygonId('to-be-updated'), visible: false),
          Polygon(polygonId: PolygonId('to-be-added')),
        };

        controller.updatePolygons(PolygonUpdates.from(previous, current));

        verify(mock.removePolygons({
          PolygonId('to-be-removed'),
        }));
        verify(mock.addPolygons({
          Polygon(polygonId: PolygonId('to-be-added')),
        }));
        verify(mock.changePolygons({
          Polygon(polygonId: PolygonId('to-be-updated'), visible: false),
        }));
      });

      testWidgets('updatePolylines', (WidgetTester tester) async {
        final mock = MockPolylinesController();
        controller.debugSetOverrides(polylines: mock);

        final previous = {
          Polyline(polylineId: PolylineId('to-be-updated')),
          Polyline(polylineId: PolylineId('to-be-removed')),
        };

        final current = {
          Polyline(polylineId: PolylineId('to-be-updated'), visible: false),
          Polyline(polylineId: PolylineId('to-be-added')),
        };

        controller.updatePolylines(PolylineUpdates.from(previous, current));

        verify(mock.removePolylines({
          PolylineId('to-be-removed'),
        }));
        verify(mock.addPolylines({
          Polyline(polylineId: PolylineId('to-be-added')),
        }));
        verify(mock.changePolylines({
          Polyline(polylineId: PolylineId('to-be-updated'), visible: false),
        }));
      });

      testWidgets('infoWindow visibility', (WidgetTester tester) async {
        final mock = MockMarkersController();
        final 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));
      });
    });
  });
}
