diff --git a/packages/mocktail_image_network/lib/src/mocktail_image_network.dart b/packages/mocktail_image_network/lib/src/mocktail_image_network.dart index 2a6b0f3..694b089 100644 --- a/packages/mocktail_image_network/lib/src/mocktail_image_network.dart +++ b/packages/mocktail_image_network/lib/src/mocktail_image_network.dart @@ -1,9 +1,13 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:mocktail/mocktail.dart'; +/// Signature for a function that returns a `List` for a given [Uri]. +typedef ImageResolver = List Function(Uri uri); + /// {@template mocktail_image_network} /// Utility method that allows you to execute a widget test when you pump a /// widget that contains an `Image.network` node in its tree. @@ -40,11 +44,19 @@ import 'package:mocktail/mocktail.dart'; /// } /// ``` /// {@endtemplate} -T mockNetworkImages(T Function() body, {Uint8List? imageBytes}) { +T mockNetworkImages( + T Function() body, { + Uint8List? imageBytes, + ImageResolver? imageResolver, +}) { + assert( + imageBytes == null || imageResolver == null, + 'One of imageBytes or imageResolver can be provided, but not both.', + ); return HttpOverrides.runZoned( body, createHttpClient: (_) => _createHttpClient( - data: imageBytes ?? _transparentPixelPng, + imageResolver ??= _defaultImageResolver(imageBytes), ), ); } @@ -53,6 +65,7 @@ class _MockHttpClient extends Mock implements HttpClient { _MockHttpClient() { registerFallbackValue((List _) {}); registerFallbackValue(Uri()); + registerFallbackValue(const Stream>.empty()); } } @@ -62,15 +75,64 @@ class _MockHttpClientResponse extends Mock implements HttpClientResponse {} class _MockHttpHeaders extends Mock implements HttpHeaders {} -HttpClient _createHttpClient({required List data}) { +HttpClient _createHttpClient(ImageResolver imageResolver) { final client = _MockHttpClient(); + + when(() => client.getUrl(any())).thenAnswer( + (invokation) async => _createRequest( + invokation.positionalArguments.first as Uri, + imageResolver, + ), + ); + when(() => client.openUrl(any(), any())).thenAnswer( + (invokation) async => _createRequest( + invokation.positionalArguments.last as Uri, + imageResolver, + ), + ); + + return client; +} + +HttpClientRequest _createRequest(Uri uri, ImageResolver imageResolver) { final request = _MockHttpClientRequest(); + final headers = _MockHttpHeaders(); + + when(() => request.headers).thenReturn(headers); + when( + () => request.addStream(any()), + ).thenAnswer((invocation) { + final stream = invocation.positionalArguments.first as Stream>; + return stream.fold>( + [], + (previous, element) => previous..addAll(element), + ); + }); + when( + request.close, + ).thenAnswer((_) async => _createResponse(uri, imageResolver)); + + return request; +} + +HttpClientResponse _createResponse(Uri uri, ImageResolver imageResolver) { final response = _MockHttpClientResponse(); final headers = _MockHttpHeaders(); - when(() => response.compressionState) - .thenReturn(HttpClientResponseCompressionState.notCompressed); - when(() => response.contentLength).thenReturn(_transparentPixelPng.length); + final data = imageResolver(uri); + + when(() => response.headers).thenReturn(headers); + when(() => response.contentLength).thenReturn(data.length); when(() => response.statusCode).thenReturn(HttpStatus.ok); + when(() => response.isRedirect).thenReturn(false); + when(() => response.redirects).thenReturn([]); + when(() => response.persistentConnection).thenReturn(false); + when(() => response.reasonPhrase).thenReturn('OK'); + when( + () => response.compressionState, + ).thenReturn(HttpClientResponseCompressionState.notCompressed); + when( + () => response.handleError(any(), test: any(named: 'test')), + ).thenAnswer((_) => Stream>.value(data)); when( () => response.listen( any(), @@ -80,17 +142,30 @@ HttpClient _createHttpClient({required List data}) { ), ).thenAnswer((invocation) { final onData = - invocation.positionalArguments[0] as void Function(List); + invocation.positionalArguments.first as void Function(List); final onDone = invocation.namedArguments[#onDone] as void Function()?; - return Stream>.fromIterable(>[data]) - .listen(onData, onDone: onDone); + return Stream>.fromIterable( + >[data], + ).listen(onData, onDone: onDone); }); - when(() => request.headers).thenReturn(headers); - when(request.close).thenAnswer((_) async => response); - when(() => client.getUrl(any())).thenAnswer((_) async => request); - return client; + return response; } +ImageResolver _defaultImageResolver(Uint8List? imageBytes) { + if (imageBytes != null) return (_) => imageBytes; + + return (uri) { + final extension = uri.path.split('.').last; + return _mockedResponses[extension] ?? _transparentPixelPng; + }; +} + +final _mockedResponses = >{ + 'png': _transparentPixelPng, + 'svg': _emptySvg, +}; + +final _emptySvg = ''.codeUnits; final _transparentPixelPng = base64Decode( '''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==''', ); diff --git a/packages/mocktail_image_network/test/mocktail_image_network_test.dart b/packages/mocktail_image_network/test/mocktail_image_network_test.dart index 67cb956..ab6877a 100644 --- a/packages/mocktail_image_network/test/mocktail_image_network_test.dart +++ b/packages/mocktail_image_network/test/mocktail_image_network_test.dart @@ -66,5 +66,72 @@ void main() { imageBytes: greenPixel, ); }); + + test('should properly mock svg response', () async { + await mockNetworkImages(() async { + final expectedData = ''.codeUnits; + final client = HttpClient()..autoUncompress = false; + final request = await client.openUrl( + 'GET', + Uri.https('', '/image.svg'), + ); + await request.addStream(Stream.value([])); + final response = await request.close(); + final data = []; + + response.listen(data.addAll); + + // Wait for all microtasks to run + await Future.delayed(Duration.zero); + + expect(response.redirects, isEmpty); + expect(data, equals(expectedData)); + }); + }); + + test('should properly use custom imageResolver', () async { + final bluePixel = base64Decode( + '''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYPj/HwADAgH/eL9GtQAAAABJRU5ErkJggg==''', + ); + + await mockNetworkImages( + () async { + final client = HttpClient()..autoUncompress = false; + final request = await client.getUrl(Uri.https('')); + final response = await request.close(); + final data = []; + + response.listen(data.addAll); + + // Wait for all microtasks to run + await Future.delayed(Duration.zero); + + expect(data, equals(bluePixel)); + }, + imageResolver: (_) => bluePixel, + ); + }); + + test( + 'should throw assertion error ' + 'when both imageBytes and imageResolver are used.', () async { + final bluePixel = base64Decode( + '''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYPj/HwADAgH/eL9GtQAAAABJRU5ErkJggg==''', + ); + expect( + () => mockNetworkImages( + () {}, + imageBytes: bluePixel, + imageResolver: (_) => bluePixel, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'One of imageBytes or imageResolver can be provided, but not both.', + ), + ), + ); + }); }); }