diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 1292468ca..f06ea4014 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -3,6 +3,7 @@ import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart' hide Path; class CircleMarker { + final Key? key; final LatLng point; final double radius; final Color color; @@ -11,9 +12,11 @@ class CircleMarker { final bool useRadiusInMeter; Offset offset = Offset.zero; double realRadius = 0; + CircleMarker({ required this.point, required this.radius, + this.key, this.useRadiusInMeter = false, this.color = const Color(0xFF00FF00), this.borderStrokeWidth = 0.0, @@ -46,6 +49,7 @@ class CircleLayer extends StatelessWidget { circleWidgets.add( CustomPaint( + key: circle.key, painter: CirclePainter(circle), size: size, ), diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index c37577138..acf1231fa 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -7,6 +7,7 @@ import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; class Polyline { + final Key? key; final List points; final List offsets = []; final double strokeWidth; @@ -22,6 +23,7 @@ class Polyline { Polyline({ required this.points, + this.key, this.strokeWidth = 1.0, this.color = const Color(0xFF00FF00), this.borderStrokeWidth = 0.0, @@ -82,10 +84,13 @@ class PolylineLayer extends StatelessWidget { _fillOffsets(polylineOpt.offsets, polylineOpt.points, map); - polylineWidgets.add(CustomPaint( - painter: PolylinePainter(polylineOpt, saveLayers), - size: size, - )); + polylineWidgets.add( + CustomPaint( + key: polylineOpt.key, + painter: PolylinePainter(polylineOpt, saveLayers), + size: size, + ), + ); } return Stack( @@ -95,8 +100,11 @@ class PolylineLayer extends StatelessWidget { ); } - void _fillOffsets(final List offsets, final List points, - FlutterMapState map) { + void _fillOffsets( + final List offsets, + final List points, + FlutterMapState map, + ) { final len = points.length; for (var i = 0; i < len; ++i) { final point = points[i]; diff --git a/pubspec.yaml b/pubspec.yaml index 0c6289e22..318cdd988 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,6 @@ name: flutter_map -description: A versatile mapping package for Flutter, based off leaflet.js, that's simple and easy to learn, yet completely customizable and configurable. +description: A versatile mapping package for Flutter, based off leaflet.js, + that's simple and easy to learn, yet completely customizable and configurable. version: 3.0.0 repository: https://github.com/fleaflet/flutter_map issue_tracker: https://github.com/fleaflet/flutter_map/issues @@ -27,5 +28,5 @@ dev_dependencies: flutter_lints: ^2.0.1 flutter_test: sdk: flutter - mockito: ^5.3.0 + mocktail: ^0.3.0 test: ^1.21.4 diff --git a/test/flutter_map_controller_test.dart b/test/flutter_map_controller_test.dart index 2272bcf91..c219673d0 100644 --- a/test/flutter_map_controller_test.dart +++ b/test/flutter_map_controller_test.dart @@ -1,9 +1,13 @@ -import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; +import 'test_utils/mocks.dart'; +import 'test_utils/test_app.dart'; + void main() { + setupMocks(); + testWidgets('test fit bounds methods', (tester) async { final controller = MapController(); final bounds = LatLngBounds( @@ -115,31 +119,3 @@ void main() { } }); } - -class TestApp extends StatelessWidget { - final MapController controller; - - const TestApp({ - required this.controller, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - body: Center( - // ensure that map is always of the same size - child: SizedBox( - width: 200, - height: 200, - child: FlutterMap( - mapController: controller, - options: MapOptions(), - ), - ), - ), - ), - ); - } -} diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 560eee7e9..088b4604d 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -1,65 +1,31 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; -import 'package:mockito/mockito.dart'; - -class MockHttpClientResponse extends Mock implements HttpClientResponse { - final _stream = readFile(); - - @override - int get statusCode => HttpStatus.ok; - - @override - int get contentLength => File('test/res/map.png').lengthSync(); - - @override - HttpClientResponseCompressionState get compressionState => - HttpClientResponseCompressionState.notCompressed; - - @override - StreamSubscription> listen(void Function(List event)? onData, - {Function? onError, void Function()? onDone, bool? cancelOnError}) { - return _stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } - - static Stream> readFile() => File('test/res/map.png').openRead(); -} - -class MockHttpHeaders extends Mock implements HttpHeaders {} -class MockHttpClientRequest extends Mock implements HttpClientRequest { - @override - HttpHeaders get headers => MockHttpHeaders(); - - @override - Future close() => Future.value(MockHttpClientResponse()); -} - -class MockClient extends Mock implements HttpClient { - @override - Future getUrl(Uri url) { - return Future.value(MockHttpClientRequest()); - } -} - -class MockHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? securityContext) => MockClient(); -} +import 'test_utils/mocks.dart'; +import 'test_utils/test_app.dart'; void main() { + setupMocks(); + testWidgets('flutter_map', (tester) async { - HttpOverrides.global = MockHttpOverrides(); - await tester.pumpWidget(const TestApp()); + final markers = [ + Marker( + width: 80, + height: 80, + point: LatLng(45.5231, -122.6765), + builder: (_) => const FlutterLogo(), + ), + Marker( + width: 80, + height: 80, + point: LatLng(40, -120), // not visible + builder: (_) => const FlutterLogo(), + ), + ]; + + await tester.pumpWidget(TestApp(markers: markers)); expect(find.byType(FlutterMap), findsOneWidget); expect(find.byType(TileLayer), findsOneWidget); expect(find.byType(RawImage), findsWidgets); @@ -67,58 +33,3 @@ void main() { expect(find.byType(FlutterLogo), findsOneWidget); }); } - -class TestApp extends StatefulWidget { - const TestApp({Key? key}) : super(key: key); - - @override - State createState() => _TestAppState(); -} - -class _TestAppState extends State { - final List _markers = [ - Marker( - width: 80, - height: 80, - point: LatLng(45.5231, -122.6765), - builder: (ctx) => const FlutterLogo(), - ), - Marker( - width: 80, - height: 80, - point: LatLng(40, -120), // not visible - builder: (ctx) => const FlutterLogo(), - ), - ]; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - body: Center( - child: SizedBox( - width: 200, - height: 200, - child: FlutterMap( - options: MapOptions( - center: LatLng(45.5231, -122.6765), - zoom: 13, - ), - children: [ - TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - ), - MarkerLayer(markers: _markers), - ], - ), - ), - ), - ), - ); - } -} diff --git a/test/layer/circle_layer_test.dart b/test/layer/circle_layer_test.dart new file mode 100644 index 000000000..3eb06096b --- /dev/null +++ b/test/layer/circle_layer_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +import '../test_utils/mocks.dart'; +import '../test_utils/test_app.dart'; + +void main() { + setupMocks(); + + testWidgets('test circle marker key', (tester) async { + const key = Key('c-1'); + + final circles = [ + CircleMarker( + key: key, + point: LatLng(51.5, -0.09), + color: Colors.blue.withOpacity(0.7), + borderStrokeWidth: 2, + useRadiusInMeter: true, + radius: 2000, + ), + ]; + + await tester.pumpWidget(TestApp(circles: circles)); + expect(find.byType(FlutterMap), findsOneWidget); + expect(find.byType(CircleLayer), findsWidgets); + expect(find.byKey(key), findsOneWidget); + }); +} diff --git a/test/layer/marker_layer_test.dart b/test/layer/marker_layer_test.dart new file mode 100644 index 000000000..e01dc1f01 --- /dev/null +++ b/test/layer/marker_layer_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +import '../test_utils/mocks.dart'; +import '../test_utils/test_app.dart'; + +void main() { + setupMocks(); + + testWidgets('test marker key', (tester) async { + const key = Key('m-1'); + + final markers = [ + Marker( + key: key, + width: 80, + height: 80, + point: LatLng(45.5231, -122.6765), + builder: (_) => const FlutterLogo(), + ), + ]; + + await tester.pumpWidget(TestApp(markers: markers)); + expect(find.byType(FlutterMap), findsOneWidget); + expect(find.byType(MarkerLayer), findsWidgets); + expect(find.byKey(key), findsOneWidget); + }); +} diff --git a/test/layer/polygon_layer_test.dart b/test/layer/polygon_layer_test.dart new file mode 100644 index 000000000..7d405db02 --- /dev/null +++ b/test/layer/polygon_layer_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +import '../test_utils/mocks.dart'; +import '../test_utils/test_app.dart'; + +void main() { + setupMocks(); + + testWidgets('test polygon key', (tester) async { + final filledPoints = [ + LatLng(55.5, -0.09), + LatLng(54.3498, -6.2603), + LatLng(52.8566, 2.3522), + ]; + + const key = Key('p-1'); + + final polygon = Polygon( + key: key, + points: filledPoints, + isFilled: true, + color: Colors.purple, + borderColor: Colors.purple, + borderStrokeWidth: 4, + ); + + await tester.pumpWidget(TestApp(polygons: [polygon])); + expect(find.byType(FlutterMap), findsOneWidget); + expect(find.byType(PolygonLayer), findsOneWidget); + expect(find.byKey(key), findsOneWidget); + }); +} diff --git a/test/layer/polyline_layer_test.dart b/test/layer/polyline_layer_test.dart new file mode 100644 index 000000000..12b6986b6 --- /dev/null +++ b/test/layer/polyline_layer_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +import '../test_utils/mocks.dart'; +import '../test_utils/test_app.dart'; + +void main() { + setupMocks(); + + testWidgets('test polyline key', (tester) async { + const key = Key('p-1'); + + final polylines = [ + Polyline( + key: key, + points: [ + LatLng(50.5, -0.09), + LatLng(51.3498, -6.2603), + LatLng(53.8566, 2.3522), + ], + strokeWidth: 4, + color: Colors.amber, + ), + ]; + + await tester.pumpWidget(TestApp(polylines: polylines)); + expect(find.byType(FlutterMap), findsOneWidget); + expect(find.byType(PolylineLayer), findsWidgets); + expect(find.byKey(key), findsOneWidget); + }); +} diff --git a/test/test_utils/mocks.dart b/test/test_utils/mocks.dart new file mode 100644 index 000000000..8a25202e3 --- /dev/null +++ b/test/test_utils/mocks.dart @@ -0,0 +1,60 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockHttpClientResponse extends Mock implements HttpClientResponse { + final _stream = readFile(); + + @override + int get statusCode => HttpStatus.ok; + + @override + int get contentLength => File('test/res/map.png').lengthSync(); + + @override + HttpClientResponseCompressionState get compressionState => + HttpClientResponseCompressionState.notCompressed; + + @override + StreamSubscription> listen(void Function(List event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return _stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + static Stream> readFile() => File('test/res/map.png').openRead(); +} + +class MockHttpHeaders extends Mock implements HttpHeaders {} + +class MockHttpClientRequest extends Mock implements HttpClientRequest { + @override + HttpHeaders get headers => MockHttpHeaders(); + + @override + Future close() => Future.value(MockHttpClientResponse()); +} + +class MockClient extends Mock implements HttpClient { + @override + Future getUrl(Uri url) { + return Future.value(MockHttpClientRequest()); + } +} + +class MockHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? securityContext) => MockClient(); +} + +void setupMocks() { + setUpAll(() { + HttpOverrides.global = MockHttpOverrides(); + }); +} diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart new file mode 100644 index 000000000..df2b842c5 --- /dev/null +++ b/test/test_utils/test_app.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +class TestApp extends StatelessWidget { + const TestApp({ + super.key, + this.controller, + this.markers = const [], + this.polygons = const [], + this.polylines = const [], + this.circles = const [], + }); + + final MapController? controller; + final List markers; + final List polygons; + final List polylines; + final List circles; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + // ensure that map is always of the same size + child: SizedBox( + width: 200, + height: 200, + child: FlutterMap( + mapController: controller, + options: MapOptions( + center: LatLng(45.5231, -122.6765), + zoom: 13, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + ), + if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), + if (polygons.isNotEmpty) PolygonLayer(polygons: polygons), + if (circles.isNotEmpty) CircleLayer(circles: circles), + if (markers.isNotEmpty) MarkerLayer(markers: markers), + ], + ), + ), + ), + ), + ); + } +}