diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index ae37935..5f2050c 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -20,7 +20,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.x' + channel: "stable" - name: Build APK working-directory: example run: flutter build apk diff --git a/.gitignore b/.gitignore index 52d1cbc..58c10c0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ pubspec.lock .dart_tool/ build/ .flutter-plugins -.flutter-plugins-dependencies \ No newline at end of file +.flutter-plugins-dependencies + +# ignore coverage info +**/coverage/lcov.info diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..5b27628 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,18 @@ +codecov: + require_ci_to_pass: true + +coverage: + status: + project: + default: + # the required coverage value + target: 0% + # the leniency in hitting the target + threshold: 100% + # completely disable coverage warnings and the status check for changes + patch: off + +ignore: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "example" diff --git a/flutter_map_cache/lib/src/cached_image_provider.dart b/flutter_map_cache/lib/src/cached_image_provider.dart index 9da6e45..fbaecc5 100644 --- a/flutter_map_cache/lib/src/cached_image_provider.dart +++ b/flutter_map_cache/lib/src/cached_image_provider.dart @@ -53,7 +53,7 @@ class CachedImageProvider extends ImageProvider { return MultiFrameImageStreamCompleter( // ignore: discarded_futures, not actually but the lint thinks so - codec: _loadAsync(key, chunkEvents, decode), + codec: loadAsync(key, chunkEvents, decode), chunkEvents: chunkEvents.stream, scale: 1, debugLabel: url, @@ -65,7 +65,8 @@ class CachedImageProvider extends ImageProvider { ); } - Future _loadAsync( + /// This method does the actual fetching of the image + Future loadAsync( CachedImageProvider key, StreamController chunkEvents, ImageDecoderCallback decode, { @@ -108,7 +109,7 @@ class CachedImageProvider extends ImageProvider { // check if no fallback url set if (fallbackUrl == null) rethrow; // use fallback url - return _loadAsync(key, chunkEvents, decode, useFallback: true); + return loadAsync(key, chunkEvents, decode, useFallback: true); } } } diff --git a/flutter_map_cache/pubspec.yaml b/flutter_map_cache/pubspec.yaml index 9a7c44d..8cc8c83 100644 --- a/flutter_map_cache/pubspec.yaml +++ b/flutter_map_cache/pubspec.yaml @@ -17,4 +17,9 @@ dependencies: flutter_map: ^6.0.0 dev_dependencies: + flutter_test: + sdk: flutter total_lints: ^3.0.0 + test: ^1.24.9 + http_mock_adapter: ^0.6.1 + latlong2: ^0.9.0 \ No newline at end of file diff --git a/flutter_map_cache/test/cached_image_provider_test.dart b/flutter_map_cache/test/cached_image_provider_test.dart new file mode 100644 index 0000000..55b9c81 --- /dev/null +++ b/flutter_map_cache/test/cached_image_provider_test.dart @@ -0,0 +1,28 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:flutter_map_cache/flutter_map_cache.dart'; +import 'package:test/test.dart'; + +// ignore_for_file: deprecated_member_use_from_same_package + +Future main() async { + test('create new instance', () { + final dio = Dio(); + const url = 'https://tile.openstreetmap.org/0/0/0.png'; + const headers = {}; + final cancelLoadingFuture = Future.delayed(const Duration(days: 1)); + + final provider = CachedImageProvider( + dio: dio, + url: url, + headers: headers, + cancelLoading: cancelLoadingFuture, + ); + + expect(provider.dio, equals(dio)); + expect(provider.url, equals(url)); + expect(provider.headers, equals(headers)); + expect(provider.cancelLoading, equals(cancelLoadingFuture)); + }); +} diff --git a/flutter_map_cache/test/cached_tile_provider_test.dart b/flutter_map_cache/test/cached_tile_provider_test.dart new file mode 100644 index 0000000..4126bd1 --- /dev/null +++ b/flutter_map_cache/test/cached_tile_provider_test.dart @@ -0,0 +1,100 @@ +import 'package:dio/dio.dart'; +import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_cache/flutter_map_cache.dart'; +import 'package:test/test.dart'; + +// ignore_for_file: deprecated_member_use_from_same_package + +Future main() async { + test('create new instance', () { + final store = MemCacheStore(); + + final provider = CachedTileProvider(store: store); + + expect(provider, isA()); + }); + test('check that interceptors contain a DioCacheInterceptor', () { + final store = MemCacheStore(); + + final provider = CachedTileProvider(store: store); + + expect( + provider.dio.interceptors.any((entry) => entry is DioCacheInterceptor), + isTrue, + ); + }); + test('check that provided interceptors are added', () { + final store = MemCacheStore(); + final someInterceptor = LogInterceptor( + logPrint: (object) => debugPrint(object.toString()), + ); + + final provider = CachedTileProvider( + store: store, + interceptors: [someInterceptor], + ); + + expect(provider.dio.interceptors, contains(someInterceptor)); + final cacheInterceptor = provider.dio.interceptors + .firstWhere((entry) => entry is DioCacheInterceptor); + expect(provider.dio.interceptors, contains(cacheInterceptor)); + }); + test('provided Dio instance but without BaseOptions', () { + final store = MemCacheStore(); + final dio = Dio(); + + final provider = CachedTileProvider(store: store, dio: dio); + + expect(provider.dio, equals(provider.dio)); + expect(provider.dio.options, equals(provider.dio.options)); + }); + test('provided Dio instance and custom BaseOptions', () { + final store = MemCacheStore(); + const someBaseUrl = 'https://unique-url.example.com'; + final dioOptions = BaseOptions( + baseUrl: someBaseUrl, + headers: {'X-API-TOKEN': 'test123'}, + ); + final dio = Dio(dioOptions); + + final provider = CachedTileProvider(store: store, dio: dio); + + expect(provider.dio.options, equals(provider.dio.options)); + expect(provider.dio.options.baseUrl, equals(someBaseUrl)); + expect(provider.dio.options.headers.length, equals(1)); + expect(provider.dio.options.headers['X-API-TOKEN'], equals('test123')); + }); + test('use empty dioOptions parameter', () { + final store = MemCacheStore(); + final dioOptions = BaseOptions(); + + final provider = CachedTileProvider(store: store, dioOptions: dioOptions); + + expect(provider.dio.options, equals(dioOptions)); + expect(provider.dio.options.headers.isEmpty, isTrue); + }); + test('use dioOptions parameter with values', () { + final store = MemCacheStore(); + const someBaseUrl = 'https://unique-url.example.com'; + final dioOptions = BaseOptions( + baseUrl: someBaseUrl, + headers: {'X-API-TOKEN': 'test123'}, + ); + + final provider = CachedTileProvider(store: store, dioOptions: dioOptions); + + expect(provider.dio.options, equals(provider.dio.options)); + expect(provider.dio.options.baseUrl, equals(someBaseUrl)); + expect(provider.dio.options.headers.length, equals(1)); + expect(provider.dio.options.headers['X-API-TOKEN'], equals('test123')); + }); + test('assert that this tile provider supports tile cancellation', () { + final store = MemCacheStore(); + + final provider = CachedTileProvider(store: store); + + expect(provider.supportsCancelLoading, isTrue); + }); +} diff --git a/flutter_map_cache/test/integration_test.dart b/flutter_map_cache/test/integration_test.dart new file mode 100644 index 0000000..4e92080 --- /dev/null +++ b/flutter_map_cache/test/integration_test.dart @@ -0,0 +1,14 @@ +import 'package:dio/dio.dart'; +import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils/test_app.dart'; + +Future main() async { + testWidgets('FlutterMap with CachedTileProvider', (tester) async { + final dio = Dio(); // createDioReturningEmptyTiles(); + final cacheStore = MemCacheStore(); + await tester.pumpWidget(TestApp(dio: dio, cacheStore: cacheStore)); + await tester.pumpAndSettle(); + }); +} diff --git a/flutter_map_cache/test/utils/functions.dart b/flutter_map_cache/test/utils/functions.dart new file mode 100644 index 0000000..b1f34b8 --- /dev/null +++ b/flutter_map_cache/test/utils/functions.dart @@ -0,0 +1,22 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:http_mock_adapter/http_mock_adapter.dart'; + +Dio createDioReturningEmptyTiles() { + final dio = Dio(); + + final dioAdapter = DioAdapter(dio: dio); + + const url = 'https://tile.openstreetmap.org/0/0/0.png'; + + dioAdapter.onGet( + url, + (server) => server.reply( + 200, + TileProvider.transparentImage, + delay: const Duration(seconds: 1), + ), + ); + + return dio; +} diff --git a/flutter_map_cache/test/utils/test_app.dart b/flutter_map_cache/test/utils/test_app.dart new file mode 100644 index 0000000..707c91d --- /dev/null +++ b/flutter_map_cache/test/utils/test_app.dart @@ -0,0 +1,38 @@ +import 'package:dio/dio.dart'; +import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_cache/flutter_map_cache.dart'; +import 'package:latlong2/latlong.dart'; + +// ignore_for_file: diagnostic_describe_all_properties + +class TestApp extends StatelessWidget { + final CacheStore cacheStore; + final Dio dio; + + const TestApp({super.key, required this.cacheStore, required this.dio}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: FlutterMap( + options: const MapOptions( + initialZoom: 0, + initialCenter: LatLng(0, 0), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + tileProvider: CachedTileProvider( + store: cacheStore, + dio: dio, + ), + ), + ], + ), + ), + ); + } +}