Skip to content

Commit

Permalink
Fixed bugs and added tests for FlutterMapNetworkImageProvider (#1662)
Browse files Browse the repository at this point in the history
Co-authored-by: JaffaKetchup <[email protected]>
  • Loading branch information
bramp and JaffaKetchup authored Sep 27, 2023
1 parent eb3b790 commit 3247d60
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,13 @@ class FlutterMapNetworkImageProvider
headers: headers,
),
),
);
).catchError((dynamic e) {
// ignore: only_throw_errors
if (useFallback || fallbackUrl == null) throw e as Object;
return _loadAsync(key, chunkEvents, decode, useFallback: true);
});
} catch (_) {
// This redundancy necessary, do not remove
if (useFallback || fallbackUrl == null) rethrow;
return _loadAsync(key, chunkEvents, decode, useFallback: true);
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dev_dependencies:
flutter_lints: ^2.0.1
flutter_test:
sdk: flutter
mocktail: ^0.3.0
mocktail: ^1.0.0
test: ^1.21.4

flutter:
Expand Down
231 changes: 231 additions & 0 deletions test/layer/tile_layer/tile_provider/network_image_provider_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// ignore_for_file: avoid_print

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_provider/network_image_provider.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mocktail/mocktail.dart';

import '../../../test_utils/test_tile_image.dart';

class MockHttpClient extends Mock implements BaseClient {}

// Helper function to resolve the ImageInfo from the ImageProvider.
Future<ImageInfo> getImageInfo(ImageProvider provider) {
final completer = Completer<ImageInfo>();

final ImageStream stream = provider.resolve(ImageConfiguration.empty);
stream.addListener(
ImageStreamListener(
(imageInfo, _) {
print('completer.complete($imageInfo)');
return completer.complete(imageInfo);
},
onError: (exception, stackTrace) {
print('completer.completeError($exception)');
return completer.completeError(exception, stackTrace);
},
),
);

return completer.future;
}

/// Returns a random URL to use for testing. Due to Flutter caching images
/// we need to use a different URL each time.
int _urlId = 0;
Uri randomUrl({bool fallback = false}) {
_urlId++;
if (fallback) {
return Uri.parse('https://example.net/fallback/$_urlId.png');
} else {
return Uri.parse('https://example.com/main/$_urlId.png');
}
}

void main() {
const headers = {
'user-agent': 'flutter_map',
'x-whatever': '123',
};

const defaultTimeout = Timeout(Duration(seconds: 1));

setUpAll(() {
// Ensure the Mock library has example values for Uri.
registerFallbackValue(Uri());
});

// We expect a request to be made to the correct URL with the appropriate headers.
testWidgets('test load with correct url/headers', (tester) async {
final mockClient = MockHttpClient();
final url = randomUrl();
when(() => mockClient.readBytes(any(), headers: any(named: 'headers')))
.thenAnswer((_) async {
print('1');
return testWhiteTileBytes;
});

final provider = FlutterMapNetworkImageProvider(
url: url.toString(),
fallbackUrl: null,
headers: headers,
httpClient: mockClient,
);

final img = await tester.runAsync(() => getImageInfo(provider));
expect(img, isNotNull);
expect(img!.image.width, equals(256));
expect(img.image.height, equals(256));

verify(() => mockClient.readBytes(url, headers: headers)).called(1);
}, timeout: defaultTimeout);

// We expect the request to be made, and a HTTP ClientException to be bubbled
// up to the caller.
testWidgets('test load with server failure (no fallback)', (tester) async {
final mockClient = MockHttpClient();
final url = randomUrl();
when(() => mockClient.readBytes(any(), headers: any(named: 'headers')))
.thenAnswer((_) async {
print('2');
throw ClientException(
'Server error',
);
});

final provider = FlutterMapNetworkImageProvider(
url: url.toString(),
fallbackUrl: null,
headers: headers,
httpClient: mockClient,
);

final img = await tester.runAsync(() => getImageInfo(provider));
expect(img, isNull);
expect(tester.takeException(), isInstanceOf<ClientException>());

verify(() => mockClient.readBytes(url, headers: headers)).called(1);
}, timeout: defaultTimeout);

// We expect the regular URL to be called once, then the fallback URL.
testWidgets('test load with server error (with successful fallback)',
(tester) async {
final mockClient = MockHttpClient();
final url = randomUrl();
when(() => mockClient.readBytes(url, headers: any(named: 'headers')))
.thenAnswer((_) async {
throw ClientException(
'Server error',
);
});
final fallbackUrl = randomUrl(fallback: true);
when(() =>
mockClient.readBytes(fallbackUrl, headers: any(named: 'headers')))
.thenAnswer((_) async {
return testWhiteTileBytes;
});

final provider = FlutterMapNetworkImageProvider(
url: url.toString(),
fallbackUrl: fallbackUrl.toString(),
headers: headers,
httpClient: mockClient,
);

final img = await tester.runAsync(() => getImageInfo(provider));
expect(img, isNotNull);
expect(img!.image.width, equals(256));
expect(img.image.height, equals(256));

verify(() => mockClient.readBytes(url, headers: headers)).called(1);
verify(() => mockClient.readBytes(fallbackUrl, headers: headers)).called(1);
}, timeout: defaultTimeout);

testWidgets('test load with server error (with failed fallback)',
(tester) async {
final mockClient = MockHttpClient();
final url = randomUrl();
final fallbackUrl = randomUrl(fallback: true);
when(() => mockClient.readBytes(any(), headers: any(named: 'headers')))
.thenAnswer((_) async {
throw ClientException(
'Server error',
);
});

final provider = FlutterMapNetworkImageProvider(
url: url.toString(),
fallbackUrl: fallbackUrl.toString(),
headers: headers,
httpClient: mockClient,
);

final img = await tester.runAsync(() => getImageInfo(provider));
expect(img, isNull);
expect(tester.takeException(), isInstanceOf<ClientException>());

verify(() => mockClient.readBytes(url, headers: headers)).called(1);
verify(() => mockClient.readBytes(fallbackUrl, headers: headers)).called(1);
}, timeout: defaultTimeout);

testWidgets('test load with invalid response (no fallback)', (tester) async {
final mockClient = MockHttpClient();
final url = randomUrl();
when(() => mockClient.readBytes(any(), headers: any(named: 'headers')))
.thenAnswer((_) async {
// 200 OK with html
return Uint8List.fromList(utf8.encode('<html>Server Error</html>'));
});

final provider = FlutterMapNetworkImageProvider(
url: url.toString(),
fallbackUrl: null,
headers: headers,
httpClient: mockClient,
);

final img = await tester.runAsync(() => getImageInfo(provider));
expect(img, isNull);
expect(tester.takeException(), isInstanceOf<Exception>());

verify(() => mockClient.readBytes(url, headers: headers)).called(1);
}, timeout: defaultTimeout);

testWidgets('test load with invalid response (with successful fallback)',
(tester) async {
final mockClient = MockHttpClient();
final url = randomUrl();
when(() => mockClient.readBytes(url, headers: any(named: 'headers')))
.thenAnswer((_) async {
// 200 OK with html
return Uint8List.fromList(utf8.encode('<html>Server Error</html>'));
});
final fallbackUrl = randomUrl(fallback: true);
when(() =>
mockClient.readBytes(fallbackUrl, headers: any(named: 'headers')))
.thenAnswer((_) async {
return testWhiteTileBytes;
});

final provider = FlutterMapNetworkImageProvider(
url: url.toString(),
fallbackUrl: fallbackUrl.toString(),
headers: headers,
httpClient: mockClient,
);

final img = await tester.runAsync(() => getImageInfo(provider));
expect(img, isNotNull);
expect(img!.image.width, equals(256));
expect(img.image.height, equals(256));

verify(() => mockClient.readBytes(url, headers: headers)).called(1);
verify(() => mockClient.readBytes(fallbackUrl, headers: headers)).called(1);
}, timeout: defaultTimeout);
}
3 changes: 2 additions & 1 deletion test/test_utils/test_tile_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import 'package:flutter/painting.dart';
// Base 64 encoded 256x256 white tile.
const _whiteTile =
'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAB9JREFUeJztwQENAAAAwqD3T20ON6AAAAAAAAAAAL4NIQAAAfFnIe4AAAAASUVORK5CYII=';
final testWhiteTileImage = MemoryImage(base64Decode(_whiteTile));
final testWhiteTileBytes = base64Decode(_whiteTile);
final testWhiteTileImage = MemoryImage(testWhiteTileBytes);

0 comments on commit 3247d60

Please sign in to comment.