Skip to content

Commit

Permalink
feat(mocktail_image_network): add ability to mock images by url (#232)
Browse files Browse the repository at this point in the history
Co-authored-by: Felix Angelov <[email protected]>
  • Loading branch information
pedrox-hs and felangel committed Jun 12, 2024
1 parent fdffdc5 commit 1567041
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 13 deletions.
101 changes: 88 additions & 13 deletions packages/mocktail_image_network/lib/src/mocktail_image_network.dart
Original file line number Diff line number Diff line change
@@ -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<int>` for a given [Uri].
typedef ImageResolver = List<int> 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.
Expand Down Expand Up @@ -40,11 +44,19 @@ import 'package:mocktail/mocktail.dart';
/// }
/// ```
/// {@endtemplate}
T mockNetworkImages<T>(T Function() body, {Uint8List? imageBytes}) {
T mockNetworkImages<T>(
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),
),
);
}
Expand All @@ -53,6 +65,7 @@ class _MockHttpClient extends Mock implements HttpClient {
_MockHttpClient() {
registerFallbackValue((List<int> _) {});
registerFallbackValue(Uri());
registerFallbackValue(const Stream<List<int>>.empty());
}
}

Expand All @@ -62,15 +75,64 @@ class _MockHttpClientResponse extends Mock implements HttpClientResponse {}

class _MockHttpHeaders extends Mock implements HttpHeaders {}

HttpClient _createHttpClient({required List<int> 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<List<int>>;
return stream.fold<List<int>>(
<int>[],
(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<List<int>>.value(data));
when(
() => response.listen(
any(),
Expand All @@ -80,17 +142,30 @@ HttpClient _createHttpClient({required List<int> data}) {
),
).thenAnswer((invocation) {
final onData =
invocation.positionalArguments[0] as void Function(List<int>);
invocation.positionalArguments.first as void Function(List<int>);
final onDone = invocation.namedArguments[#onDone] as void Function()?;
return Stream<List<int>>.fromIterable(<List<int>>[data])
.listen(onData, onDone: onDone);
return Stream<List<int>>.fromIterable(
<List<int>>[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 = <String, List<int>>{
'png': _transparentPixelPng,
'svg': _emptySvg,
};

final _emptySvg = '<svg viewBox="0 0 10 10" />'.codeUnits;
final _transparentPixelPng = base64Decode(
'''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==''',
);
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,72 @@ void main() {
imageBytes: greenPixel,
);
});

test('should properly mock svg response', () async {
await mockNetworkImages(() async {
final expectedData = '<svg viewBox="0 0 10 10" />'.codeUnits;
final client = HttpClient()..autoUncompress = false;
final request = await client.openUrl(
'GET',
Uri.https('', '/image.svg'),
);
await request.addStream(Stream.value(<int>[]));
final response = await request.close();
final data = <int>[];

response.listen(data.addAll);

// Wait for all microtasks to run
await Future<void>.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 = <int>[];

response.listen(data.addAll);

// Wait for all microtasks to run
await Future<void>.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<AssertionError>().having(
(e) => e.message,
'message',
'One of imageBytes or imageResolver can be provided, but not both.',
),
),
);
});
});
}

0 comments on commit 1567041

Please sign in to comment.