@@ -13,6 +13,20 @@ import 'package:integration_test/integration_test.dart';
1313
1414import 'shared.dart' ;
1515
16+ const double _kTestCameraZoomLevel = 10 ;
17+ const double _kTestZoomByAmount = 2 ;
18+ const LatLng _kTestMapCenter = LatLng (65 , 25.5 );
19+ const CameraPosition _kTestCameraPosition = CameraPosition (
20+ target: _kTestMapCenter,
21+ zoom: _kTestCameraZoomLevel,
22+ bearing: 1.0 ,
23+ tilt: 1.0 ,
24+ );
25+ final LatLngBounds _testCameraBounds = LatLngBounds (
26+ northeast: const LatLng (50 , - 65 ), southwest: const LatLng (28.5 , - 123 ));
27+ final ValueVariant <CameraUpdateType > _cameraUpdateTypeVariants =
28+ ValueVariant <CameraUpdateType >(CameraUpdateType .values.toSet ());
29+
1630/// Integration Tests that use the [GoogleMapsInspectorPlatform] .
1731void main () {
1832 IntegrationTestWidgetsFlutterBinding .ensureInitialized ();
@@ -612,6 +626,248 @@ void runTests() {
612626 expect (clusters.length, 0 );
613627 }
614628 });
629+
630+ testWidgets (
631+ 'testAnimateCameraWithoutDuration' ,
632+ (WidgetTester tester) async {
633+ final Key key = GlobalKey ();
634+ final Completer <GoogleMapController > controllerCompleter =
635+ Completer <GoogleMapController >();
636+ final GoogleMapsInspectorPlatform inspector =
637+ GoogleMapsInspectorPlatform .instance! ;
638+
639+ /// Completer to track when the camera has come to rest.
640+ Completer <void >? cameraIdleCompleter;
641+
642+ await tester.pumpWidget (Directionality (
643+ textDirection: TextDirection .ltr,
644+ child: GoogleMap (
645+ key: key,
646+ initialCameraPosition: kInitialCameraPosition,
647+ onCameraIdle: () {
648+ if (cameraIdleCompleter != null &&
649+ ! cameraIdleCompleter.isCompleted) {
650+ cameraIdleCompleter.complete ();
651+ }
652+ },
653+ onMapCreated: (GoogleMapController controller) {
654+ controllerCompleter.complete (controller);
655+ },
656+ ),
657+ ));
658+
659+ final GoogleMapController controller = await controllerCompleter.future;
660+
661+ await tester.pumpAndSettle ();
662+ // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and
663+ // `mapControllerCompleter.complete(controller)` above should happen in
664+ // `mapRendered`.
665+ // https://github.com/flutter/flutter/issues/54758
666+ await Future <void >.delayed (const Duration (seconds: 1 ));
667+
668+ // Create completer for camera idle event.
669+ cameraIdleCompleter = Completer <void >();
670+
671+ final CameraUpdate cameraUpdate =
672+ _getCameraUpdateForType (_cameraUpdateTypeVariants.currentValue! );
673+ await controller.animateCamera (cameraUpdate);
674+
675+ // If platform supportes getting camera position, check that the camera
676+ // has moved as expected.
677+ CameraPosition ? beforeFinishedPosition;
678+ if (inspector.supportsGettingGameraPosition ()) {
679+ // Immediately after calling animateCamera, check that the camera hasn't
680+ // reached its final position. This relies on the assumption that the
681+ // camera move is animated and won't complete instantly.
682+ beforeFinishedPosition =
683+ await inspector.getCameraPosition (mapId: controller.mapId);
684+
685+ await _checkCameraUpdateByType (
686+ _cameraUpdateTypeVariants.currentValue! ,
687+ beforeFinishedPosition,
688+ null ,
689+ controller,
690+ (Matcher matcher) => isNot (matcher));
691+ }
692+
693+ // Wait for the animation to complete (onCameraIdle).
694+ expect (cameraIdleCompleter.isCompleted, isFalse);
695+ await cameraIdleCompleter.future;
696+
697+ // If platform supportes getting camera position, check that the camera
698+ // has moved as expected.
699+ if (inspector.supportsGettingGameraPosition ()) {
700+ // After onCameraIdle event, the camera should be at the final position.
701+ final CameraPosition afterFinishedPosition =
702+ await inspector.getCameraPosition (mapId: controller.mapId);
703+ await _checkCameraUpdateByType (
704+ _cameraUpdateTypeVariants.currentValue! ,
705+ afterFinishedPosition,
706+ beforeFinishedPosition,
707+ controller,
708+ (Matcher matcher) => matcher);
709+ }
710+ },
711+ variant: _cameraUpdateTypeVariants,
712+ // TODO(stuartmorgan): Remove skip for Android platform once Maps API key is
713+ // available for LUCI, https://github.com/flutter/flutter/issues/131071
714+ skip: isAndroid,
715+ );
716+
717+ /// Tests animating the camera with specified durations to verify timing
718+ /// behavior.
719+ ///
720+ /// This test checks two scenarios: short and long animation durations.
721+ /// It uses a midpoint duration to ensure the short animation completes in
722+ /// less time and the long animation takes more time than that midpoint.
723+ /// This ensures that the animation duration is respected by the platform and
724+ /// that the default camera animation duration does not affect the test
725+ /// results.
726+ testWidgets (
727+ 'testAnimateCameraWithDuration' ,
728+ (WidgetTester tester) async {
729+ final Key key = GlobalKey ();
730+ final Completer <GoogleMapController > controllerCompleter =
731+ Completer <GoogleMapController >();
732+ final GoogleMapsInspectorPlatform inspector =
733+ GoogleMapsInspectorPlatform .instance! ;
734+
735+ /// Completer to track when the camera has come to rest.
736+ Completer <void >? cameraIdleCompleter;
737+
738+ const int shortCameraAnimationDurationMS = 200 ;
739+ const int longCameraAnimationDurationMS = 1000 ;
740+
741+ /// Calculate the midpoint duration of the animation test, which will
742+ /// serve as a reference to verify that animations complete more quickly
743+ /// with shorter durations and more slowly with longer durations.
744+ const int animationDurationMiddlePoint =
745+ (shortCameraAnimationDurationMS + longCameraAnimationDurationMS) ~ / 2 ;
746+
747+ // Stopwatch to measure the time taken for the animation to complete.
748+ final Stopwatch stopwatch = Stopwatch ();
749+
750+ await tester.pumpWidget (Directionality (
751+ textDirection: TextDirection .ltr,
752+ child: GoogleMap (
753+ key: key,
754+ initialCameraPosition: kInitialCameraPosition,
755+ onCameraIdle: () {
756+ if (cameraIdleCompleter != null &&
757+ ! cameraIdleCompleter.isCompleted) {
758+ stopwatch.stop ();
759+ cameraIdleCompleter.complete ();
760+ }
761+ },
762+ onMapCreated: (GoogleMapController controller) {
763+ controllerCompleter.complete (controller);
764+ },
765+ ),
766+ ));
767+
768+ final GoogleMapController controller = await controllerCompleter.future;
769+
770+ await tester.pumpAndSettle ();
771+ // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and
772+ // `mapControllerCompleter.complete(controller)` above should happen in
773+ // `mapRendered`.
774+ // https://github.com/flutter/flutter/issues/54758
775+ await Future <void >.delayed (const Duration (seconds: 1 ));
776+
777+ // Create completer for camera idle event.
778+ cameraIdleCompleter = Completer <void >();
779+
780+ // Start stopwatch to check the time taken for the animation to complete.
781+ // Stopwatch is stopped on camera idle callback.
782+ stopwatch.reset ();
783+ stopwatch.start ();
784+
785+ // First phase with shorter animation duration.
786+ final CameraUpdate cameraUpdateShort =
787+ _getCameraUpdateForType (_cameraUpdateTypeVariants.currentValue! );
788+ await controller.animateCamera (
789+ cameraUpdateShort,
790+ duration: const Duration (milliseconds: shortCameraAnimationDurationMS),
791+ );
792+
793+ // Wait for the animation to complete (onCameraIdle).
794+ expect (cameraIdleCompleter.isCompleted, isFalse);
795+ await cameraIdleCompleter.future;
796+
797+ // For short animation duration, check that the animation is completed
798+ // faster than the midpoint benchmark.
799+ expect (stopwatch.elapsedMilliseconds,
800+ lessThan (animationDurationMiddlePoint));
801+
802+ // Reset camera to initial position before testing long duration.
803+ await controller
804+ .moveCamera (CameraUpdate .newCameraPosition (kInitialCameraPosition));
805+ await tester.pumpAndSettle ();
806+
807+ // Create completer for camera idle event.
808+ cameraIdleCompleter = Completer <void >();
809+
810+ // Start stopwatch to check the time taken for the animation to complete.
811+ // Stopwatch is stopped on camera idle callback.
812+ stopwatch.reset ();
813+ stopwatch.start ();
814+
815+ // Second phase with longer animation duration.
816+ final CameraUpdate cameraUpdateLong =
817+ _getCameraUpdateForType (_cameraUpdateTypeVariants.currentValue! );
818+ await controller.animateCamera (
819+ cameraUpdateLong,
820+ duration: const Duration (milliseconds: longCameraAnimationDurationMS),
821+ );
822+
823+ // If platform supportes getting camera position, check that the camera
824+ // has moved as expected.
825+ CameraPosition ? beforeFinishedPosition;
826+ if (inspector.supportsGettingGameraPosition ()) {
827+ // Immediately after calling animateCamera, check that the camera hasn't
828+ // reached its final position. This relies on the assumption that the
829+ // camera move is animated and won't complete instantly.
830+ beforeFinishedPosition =
831+ await inspector.getCameraPosition (mapId: controller.mapId);
832+
833+ await _checkCameraUpdateByType (
834+ _cameraUpdateTypeVariants.currentValue! ,
835+ beforeFinishedPosition,
836+ null ,
837+ controller,
838+ (Matcher matcher) => isNot (matcher));
839+ }
840+
841+ // Wait for the animation to complete (onCameraIdle).
842+ expect (cameraIdleCompleter.isCompleted, isFalse);
843+ await cameraIdleCompleter.future;
844+
845+ // For longer animation duration, check that the animation is completed
846+ // slower than the midpoint benchmark.
847+ expect (stopwatch.elapsedMilliseconds,
848+ greaterThan (animationDurationMiddlePoint));
849+
850+ // If platform supportes getting camera position, check that the camera
851+ // has moved as expected.
852+ if (inspector.supportsGettingGameraPosition ()) {
853+ // Camera should be at the final position.
854+ final CameraPosition afterFinishedPosition =
855+ await inspector.getCameraPosition (mapId: controller.mapId);
856+ await _checkCameraUpdateByType (
857+ _cameraUpdateTypeVariants.currentValue! ,
858+ afterFinishedPosition,
859+ beforeFinishedPosition,
860+ controller,
861+ (Matcher matcher) => matcher);
862+ }
863+ },
864+ variant: _cameraUpdateTypeVariants,
865+ // TODO(jokerttu): Remove skip once the web implementation is available,
866+ // https://github.com/flutter/flutter/issues/159265
867+ // TODO(stuartmorgan): Remove skip for Android platform once Maps API key is
868+ // available for LUCI, https://github.com/flutter/flutter/issues/131071
869+ skip: kIsWeb || isAndroid,
870+ );
615871}
616872
617873Marker _copyMarkerWithClusterManagerId (
@@ -636,3 +892,89 @@ Marker _copyMarkerWithClusterManagerId(
636892 clusterManagerId: clusterManagerId,
637893 );
638894}
895+
896+ CameraUpdate _getCameraUpdateForType (CameraUpdateType type) {
897+ return switch (type) {
898+ CameraUpdateType .newCameraPosition =>
899+ CameraUpdate .newCameraPosition (_kTestCameraPosition),
900+ CameraUpdateType .newLatLng => CameraUpdate .newLatLng (_kTestMapCenter),
901+ CameraUpdateType .newLatLngBounds =>
902+ CameraUpdate .newLatLngBounds (_testCameraBounds, 0 ),
903+ CameraUpdateType .newLatLngZoom =>
904+ CameraUpdate .newLatLngZoom (_kTestMapCenter, _kTestCameraZoomLevel),
905+ CameraUpdateType .scrollBy => CameraUpdate .scrollBy (10 , 10 ),
906+ CameraUpdateType .zoomBy =>
907+ CameraUpdate .zoomBy (_kTestZoomByAmount, const Offset (1 , 1 )),
908+ CameraUpdateType .zoomTo => CameraUpdate .zoomTo (_kTestCameraZoomLevel),
909+ CameraUpdateType .zoomIn => CameraUpdate .zoomIn (),
910+ CameraUpdateType .zoomOut => CameraUpdate .zoomOut (),
911+ };
912+ }
913+
914+ Future <void > _checkCameraUpdateByType (
915+ CameraUpdateType type,
916+ CameraPosition currentPosition,
917+ CameraPosition ? oldPosition,
918+ GoogleMapController controller,
919+ Matcher Function (Matcher matcher) wrapMatcher,
920+ ) async {
921+ // As the target might differ a bit from the expected target, a threshold is
922+ // used.
923+ const double latLngThreshold = 0.05 ;
924+
925+ switch (type) {
926+ case CameraUpdateType .newCameraPosition:
927+ expect (currentPosition.bearing,
928+ wrapMatcher (equals (_kTestCameraPosition.bearing)));
929+ expect (
930+ currentPosition.zoom, wrapMatcher (equals (_kTestCameraPosition.zoom)));
931+ expect (
932+ currentPosition.tilt, wrapMatcher (equals (_kTestCameraPosition.tilt)));
933+ expect (
934+ currentPosition.target.latitude,
935+ wrapMatcher (
936+ closeTo (_kTestCameraPosition.target.latitude, latLngThreshold)));
937+ expect (
938+ currentPosition.target.longitude,
939+ wrapMatcher (
940+ closeTo (_kTestCameraPosition.target.longitude, latLngThreshold)));
941+ case CameraUpdateType .newLatLng:
942+ expect (currentPosition.target.latitude,
943+ wrapMatcher (closeTo (_kTestMapCenter.latitude, latLngThreshold)));
944+ expect (currentPosition.target.longitude,
945+ wrapMatcher (closeTo (_kTestMapCenter.longitude, latLngThreshold)));
946+ case CameraUpdateType .newLatLngBounds:
947+ final LatLngBounds bounds = await controller.getVisibleRegion ();
948+ expect (
949+ bounds.northeast.longitude,
950+ wrapMatcher (
951+ closeTo (_testCameraBounds.northeast.longitude, latLngThreshold)));
952+ expect (
953+ bounds.southwest.longitude,
954+ wrapMatcher (
955+ closeTo (_testCameraBounds.southwest.longitude, latLngThreshold)));
956+ case CameraUpdateType .newLatLngZoom:
957+ expect (currentPosition.target.latitude,
958+ wrapMatcher (closeTo (_kTestMapCenter.latitude, latLngThreshold)));
959+ expect (currentPosition.target.longitude,
960+ wrapMatcher (closeTo (_kTestMapCenter.longitude, latLngThreshold)));
961+ expect (currentPosition.zoom, wrapMatcher (equals (_kTestCameraZoomLevel)));
962+ case CameraUpdateType .scrollBy:
963+ // For scrollBy, just check that the location has changed.
964+ if (oldPosition != null ) {
965+ expect (currentPosition.target.latitude,
966+ isNot (equals (oldPosition.target.latitude)));
967+ expect (currentPosition.target.longitude,
968+ isNot (equals (oldPosition.target.longitude)));
969+ }
970+ case CameraUpdateType .zoomBy:
971+ expect (currentPosition.zoom,
972+ wrapMatcher (equals (kInitialZoomLevel + _kTestZoomByAmount)));
973+ case CameraUpdateType .zoomTo:
974+ expect (currentPosition.zoom, wrapMatcher (equals (_kTestCameraZoomLevel)));
975+ case CameraUpdateType .zoomIn:
976+ expect (currentPosition.zoom, wrapMatcher (equals (kInitialZoomLevel + 1 )));
977+ case CameraUpdateType .zoomOut:
978+ expect (currentPosition.zoom, wrapMatcher (equals (kInitialZoomLevel - 1 )));
979+ }
980+ }
0 commit comments