Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed bugs and added tests for FlutterMapNetworkImageProvider #1662

Merged
merged 3 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Loading