Skip to content

Commit 1567041

Browse files
pedrox-hsfelangel
andauthored
feat(mocktail_image_network): add ability to mock images by url (#232)
Co-authored-by: Felix Angelov <felangelov@gmail.com>
1 parent fdffdc5 commit 1567041

File tree

2 files changed

+155
-13
lines changed

2 files changed

+155
-13
lines changed

packages/mocktail_image_network/lib/src/mocktail_image_network.dart

+88-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import 'dart:async';
12
import 'dart:convert';
23
import 'dart:io';
34
import 'dart:typed_data';
45

56
import 'package:mocktail/mocktail.dart';
67

8+
/// Signature for a function that returns a `List<int>` for a given [Uri].
9+
typedef ImageResolver = List<int> Function(Uri uri);
10+
711
/// {@template mocktail_image_network}
812
/// Utility method that allows you to execute a widget test when you pump a
913
/// widget that contains an `Image.network` node in its tree.
@@ -40,11 +44,19 @@ import 'package:mocktail/mocktail.dart';
4044
/// }
4145
/// ```
4246
/// {@endtemplate}
43-
T mockNetworkImages<T>(T Function() body, {Uint8List? imageBytes}) {
47+
T mockNetworkImages<T>(
48+
T Function() body, {
49+
Uint8List? imageBytes,
50+
ImageResolver? imageResolver,
51+
}) {
52+
assert(
53+
imageBytes == null || imageResolver == null,
54+
'One of imageBytes or imageResolver can be provided, but not both.',
55+
);
4456
return HttpOverrides.runZoned(
4557
body,
4658
createHttpClient: (_) => _createHttpClient(
47-
data: imageBytes ?? _transparentPixelPng,
59+
imageResolver ??= _defaultImageResolver(imageBytes),
4860
),
4961
);
5062
}
@@ -53,6 +65,7 @@ class _MockHttpClient extends Mock implements HttpClient {
5365
_MockHttpClient() {
5466
registerFallbackValue((List<int> _) {});
5567
registerFallbackValue(Uri());
68+
registerFallbackValue(const Stream<List<int>>.empty());
5669
}
5770
}
5871

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

6376
class _MockHttpHeaders extends Mock implements HttpHeaders {}
6477

65-
HttpClient _createHttpClient({required List<int> data}) {
78+
HttpClient _createHttpClient(ImageResolver imageResolver) {
6679
final client = _MockHttpClient();
80+
81+
when(() => client.getUrl(any())).thenAnswer(
82+
(invokation) async => _createRequest(
83+
invokation.positionalArguments.first as Uri,
84+
imageResolver,
85+
),
86+
);
87+
when(() => client.openUrl(any(), any())).thenAnswer(
88+
(invokation) async => _createRequest(
89+
invokation.positionalArguments.last as Uri,
90+
imageResolver,
91+
),
92+
);
93+
94+
return client;
95+
}
96+
97+
HttpClientRequest _createRequest(Uri uri, ImageResolver imageResolver) {
6798
final request = _MockHttpClientRequest();
99+
final headers = _MockHttpHeaders();
100+
101+
when(() => request.headers).thenReturn(headers);
102+
when(
103+
() => request.addStream(any()),
104+
).thenAnswer((invocation) {
105+
final stream = invocation.positionalArguments.first as Stream<List<int>>;
106+
return stream.fold<List<int>>(
107+
<int>[],
108+
(previous, element) => previous..addAll(element),
109+
);
110+
});
111+
when(
112+
request.close,
113+
).thenAnswer((_) async => _createResponse(uri, imageResolver));
114+
115+
return request;
116+
}
117+
118+
HttpClientResponse _createResponse(Uri uri, ImageResolver imageResolver) {
68119
final response = _MockHttpClientResponse();
69120
final headers = _MockHttpHeaders();
70-
when(() => response.compressionState)
71-
.thenReturn(HttpClientResponseCompressionState.notCompressed);
72-
when(() => response.contentLength).thenReturn(_transparentPixelPng.length);
121+
final data = imageResolver(uri);
122+
123+
when(() => response.headers).thenReturn(headers);
124+
when(() => response.contentLength).thenReturn(data.length);
73125
when(() => response.statusCode).thenReturn(HttpStatus.ok);
126+
when(() => response.isRedirect).thenReturn(false);
127+
when(() => response.redirects).thenReturn([]);
128+
when(() => response.persistentConnection).thenReturn(false);
129+
when(() => response.reasonPhrase).thenReturn('OK');
130+
when(
131+
() => response.compressionState,
132+
).thenReturn(HttpClientResponseCompressionState.notCompressed);
133+
when(
134+
() => response.handleError(any(), test: any(named: 'test')),
135+
).thenAnswer((_) => Stream<List<int>>.value(data));
74136
when(
75137
() => response.listen(
76138
any(),
@@ -80,17 +142,30 @@ HttpClient _createHttpClient({required List<int> data}) {
80142
),
81143
).thenAnswer((invocation) {
82144
final onData =
83-
invocation.positionalArguments[0] as void Function(List<int>);
145+
invocation.positionalArguments.first as void Function(List<int>);
84146
final onDone = invocation.namedArguments[#onDone] as void Function()?;
85-
return Stream<List<int>>.fromIterable(<List<int>>[data])
86-
.listen(onData, onDone: onDone);
147+
return Stream<List<int>>.fromIterable(
148+
<List<int>>[data],
149+
).listen(onData, onDone: onDone);
87150
});
88-
when(() => request.headers).thenReturn(headers);
89-
when(request.close).thenAnswer((_) async => response);
90-
when(() => client.getUrl(any())).thenAnswer((_) async => request);
91-
return client;
151+
return response;
92152
}
93153

154+
ImageResolver _defaultImageResolver(Uint8List? imageBytes) {
155+
if (imageBytes != null) return (_) => imageBytes;
156+
157+
return (uri) {
158+
final extension = uri.path.split('.').last;
159+
return _mockedResponses[extension] ?? _transparentPixelPng;
160+
};
161+
}
162+
163+
final _mockedResponses = <String, List<int>>{
164+
'png': _transparentPixelPng,
165+
'svg': _emptySvg,
166+
};
167+
168+
final _emptySvg = '<svg viewBox="0 0 10 10" />'.codeUnits;
94169
final _transparentPixelPng = base64Decode(
95170
'''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==''',
96171
);

packages/mocktail_image_network/test/mocktail_image_network_test.dart

+67
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,72 @@ void main() {
6666
imageBytes: greenPixel,
6767
);
6868
});
69+
70+
test('should properly mock svg response', () async {
71+
await mockNetworkImages(() async {
72+
final expectedData = '<svg viewBox="0 0 10 10" />'.codeUnits;
73+
final client = HttpClient()..autoUncompress = false;
74+
final request = await client.openUrl(
75+
'GET',
76+
Uri.https('', '/image.svg'),
77+
);
78+
await request.addStream(Stream.value(<int>[]));
79+
final response = await request.close();
80+
final data = <int>[];
81+
82+
response.listen(data.addAll);
83+
84+
// Wait for all microtasks to run
85+
await Future<void>.delayed(Duration.zero);
86+
87+
expect(response.redirects, isEmpty);
88+
expect(data, equals(expectedData));
89+
});
90+
});
91+
92+
test('should properly use custom imageResolver', () async {
93+
final bluePixel = base64Decode(
94+
'''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYPj/HwADAgH/eL9GtQAAAABJRU5ErkJggg==''',
95+
);
96+
97+
await mockNetworkImages(
98+
() async {
99+
final client = HttpClient()..autoUncompress = false;
100+
final request = await client.getUrl(Uri.https(''));
101+
final response = await request.close();
102+
final data = <int>[];
103+
104+
response.listen(data.addAll);
105+
106+
// Wait for all microtasks to run
107+
await Future<void>.delayed(Duration.zero);
108+
109+
expect(data, equals(bluePixel));
110+
},
111+
imageResolver: (_) => bluePixel,
112+
);
113+
});
114+
115+
test(
116+
'should throw assertion error '
117+
'when both imageBytes and imageResolver are used.', () async {
118+
final bluePixel = base64Decode(
119+
'''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYPj/HwADAgH/eL9GtQAAAABJRU5ErkJggg==''',
120+
);
121+
expect(
122+
() => mockNetworkImages(
123+
() {},
124+
imageBytes: bluePixel,
125+
imageResolver: (_) => bluePixel,
126+
),
127+
throwsA(
128+
isA<AssertionError>().having(
129+
(e) => e.message,
130+
'message',
131+
'One of imageBytes or imageResolver can be provided, but not both.',
132+
),
133+
),
134+
);
135+
});
69136
});
70137
}

0 commit comments

Comments
 (0)