image_provider_network_image_test.dart 10.5 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
8
import 'dart:ui' show Codec, FrameInfo, ImmutableBuffer;
9 10 11 12 13

import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';

14
import '../image_data.dart';
15 16 17
import '../rendering/rendering_tester.dart';

void main() {
18
  TestRenderingFlutterBinding.ensureInitialized();
19
  HttpOverrides.global = _FakeHttpOverrides();
20

21 22
  Future<Codec>  basicDecoder(ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
    return PaintingBinding.instance.instantiateImageCodecFromBuffer(buffer, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false);
23
  }
24

25
  late _FakeHttpClient httpClient;
26 27

  setUp(() {
28
    _FakeHttpOverrides.createHttpClientCalls = 0;
29
    httpClient = _FakeHttpClient();
30 31 32 33 34
    debugNetworkImageHttpClientProvider = () => httpClient;
  });

  tearDown(() {
    debugNetworkImageHttpClientProvider = null;
35
    expect(_FakeHttpOverrides.createHttpClientCalls, 0);
36 37
    PaintingBinding.instance.imageCache.clear();
    PaintingBinding.instance.imageCache.clearLiveImages();
38 39
  });

40
  test('Expect thrown exception with statusCode - evicts from cache and drains', () async {
41
    const int errorStatusCode = HttpStatus.notFound;
42 43
    const String requestUrl = 'foo-url';

44
    httpClient.request.response.statusCode = errorStatusCode;
45 46 47 48

    final Completer<dynamic> caughtError = Completer<dynamic>();

    final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
49 50
    expect(imageCache.pendingImageCount, 0);
    expect(imageCache.statusForKey(imageProvider).untracked, true);
51 52 53

    final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);

54 55
    expect(imageCache.pendingImageCount, 1);
    expect(imageCache.statusForKey(imageProvider).pending, true);
56 57

    result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
58
    }, onError: (dynamic error, StackTrace? stackTrace) {
59 60 61 62 63
      caughtError.complete(error);
    }));

    final dynamic err = await caughtError.future;

64 65
    expect(imageCache.pendingImageCount, 0);
    expect(imageCache.statusForKey(imageProvider).untracked, true);
66 67 68 69 70 71 72

    expect(
      err,
      isA<NetworkImageLoadException>()
        .having((NetworkImageLoadException e) => e.statusCode, 'statusCode', errorStatusCode)
        .having((NetworkImageLoadException e) => e.uri, 'uri', Uri.base.resolve(requestUrl)),
    );
73
    expect(httpClient.request.response.drained, true);
74
  }, skip: isBrowser); // [intended] Browser implementation does not use HTTP client but an <img> tag.
75 76

  test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
77
    httpClient.thrownError = 'client1';
78 79 80 81
    final List<dynamic> capturedErrors = <dynamic>[];

    Future<void> loadNetworkImage() async {
      final NetworkImage networkImage = NetworkImage(nonconst('foo'));
82
      final ImageStreamCompleter completer = networkImage.loadBuffer(networkImage, basicDecoder);
83 84
      completer.addListener(ImageStreamListener(
        (ImageInfo image, bool synchronousCall) { },
85
        onError: (dynamic error, StackTrace? stackTrace) {
86 87 88 89 90 91 92 93
          capturedErrors.add(error);
        },
      ));
      await Future<void>.value();
    }

    await loadNetworkImage();
    expect(capturedErrors, <dynamic>['client1']);
94 95
    final _FakeHttpClient client2 = _FakeHttpClient();
    client2.thrownError = 'client2';
96 97 98
    debugNetworkImageHttpClientProvider = () => client2;
    await loadNetworkImage();
    expect(capturedErrors, <dynamic>['client1', 'client2']);
99
  }, skip: isBrowser); // [intended] Browser implementation does not use HTTP client but an <img> tag.
100 101

  test('Propagates http client errors during resolve()', () async {
102
    httpClient.thrownError = Error();
103 104
    bool uncaught = false;

105
    final FlutterExceptionHandler? oldError = FlutterError.onError;
106 107 108 109 110 111 112 113
    await runZoned(() async {
      const ImageProvider imageProvider = NetworkImage('asdasdasdas');
      final Completer<bool> caughtError = Completer<bool>();
      FlutterError.onError = (FlutterErrorDetails details) {
        throw Error();
      };
      final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
      result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
114
      }, onError: (dynamic error, StackTrace? stackTrace) {
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
        caughtError.complete(true);
      }));
      expect(await caughtError.future, true);
    }, zoneSpecification: ZoneSpecification(
      handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
        uncaught = true;
      },
    ));
    expect(uncaught, false);
    FlutterError.onError = oldError;
  });

  test('Notifies listeners of chunk events', () async {
    const int chunkSize = 8;
    final List<Uint8List> chunks = <Uint8List>[
      for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
        Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
    ];
    final Completer<void> imageAvailable = Completer<void>();
134 135 136 137 138

    httpClient.request.response
      ..statusCode = HttpStatus.ok
      ..contentLength = kTransparentImage.length
      ..content = chunks;
139 140 141 142 143 144 145 146 147 148 149

    final ImageProvider imageProvider = NetworkImage(nonconst('foo'));
    final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
    final List<ImageChunkEvent> events = <ImageChunkEvent>[];
    result.addListener(ImageStreamListener(
      (ImageInfo image, bool synchronousCall) {
        imageAvailable.complete();
      },
      onChunk: (ImageChunkEvent event) {
        events.add(event);
      },
150 151
      onError: (dynamic error, StackTrace? stackTrace) {
        imageAvailable.completeError(error as Object, stackTrace);
152 153 154 155 156 157 158 159
      },
    ));
    await imageAvailable.future;
    expect(events.length, chunks.length);
    for (int i = 0; i < events.length; i++) {
      expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
      expect(events[i].expectedTotalBytes, kTransparentImage.length);
    }
160
  }, skip: isBrowser); // [intended] Browser loads images through <img> not Http.
161 162

  test('NetworkImage is evicted from cache on SocketException', () async {
163 164
    final _FakeHttpClient mockHttpClient = _FakeHttpClient();
    mockHttpClient.thrownError = const SocketException('test exception');
165 166 167
    debugNetworkImageHttpClientProvider = () => mockHttpClient;

    final ImageProvider imageProvider = NetworkImage(nonconst('testing.url'));
168 169
    expect(imageCache.pendingImageCount, 0);
    expect(imageCache.statusForKey(imageProvider).untracked, true);
170 171 172

    final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);

173 174
    expect(imageCache.pendingImageCount, 1);
    expect(imageCache.statusForKey(imageProvider).pending, true);
175 176 177
    final Completer<dynamic> caughtError = Completer<dynamic>();
    result.addListener(ImageStreamListener(
      (ImageInfo info, bool syncCall) {},
178
      onError: (dynamic error, StackTrace? stackTrace) {
179 180 181 182 183 184 185 186
        caughtError.complete(error);
      },
    ));

    final dynamic err = await caughtError.future;

    expect(err, isA<SocketException>());

187 188 189
    expect(imageCache.pendingImageCount, 0);
    expect(imageCache.statusForKey(imageProvider).untracked, true);
    expect(imageCache.containsKey(result), isFalse);
190 191

    debugNetworkImageHttpClientProvider = null;
192
  }, skip: isBrowser); // [intended] Browser does not resolve images this way.
193

194
  Future<Codec> decoder(ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async {
195 196 197 198 199 200 201 202 203 204
    return FakeCodec();
  }

  test('Network image sets tag', () async {
    const String url = 'http://test.png';
    const int chunkSize = 8;
    final List<Uint8List> chunks = <Uint8List>[
      for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
        Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
    ];
205 206 207 208
    httpClient.request.response
      ..statusCode = HttpStatus.ok
      ..contentLength = kTransparentImage.length
      ..content = chunks;
209 210 211

    const NetworkImage provider = NetworkImage(url);

212
    final MultiFrameImageStreamCompleter completer = provider.loadBuffer(provider, decoder) as MultiFrameImageStreamCompleter;
213 214 215

    expect(completer.debugLabel, url);
  });
216
}
217

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
/// Override `HttpClient()` to throw an error.
///
/// This ensures that these tests never cause a call to the [HttpClient]
/// constructor.
///
/// Regression test for <https://github.com/flutter/flutter/issues/129532>.
class _FakeHttpOverrides extends HttpOverrides {
  static int createHttpClientCalls = 0;

  @override
  HttpClient createHttpClient(SecurityContext? context) {
    createHttpClientCalls++;
    throw Exception('This test tried to create an HttpClient.');
  }
}

234 235
class _FakeHttpClient extends Fake implements HttpClient {
  final _FakeHttpClientRequest request = _FakeHttpClientRequest();
236
  Object? thrownError;
237 238 239 240

  @override
  Future<HttpClientRequest> getUrl(Uri url) async {
    if (thrownError != null) {
241
      throw thrownError!;
242 243 244 245
    }
    return request;
  }
}
246

247 248 249 250 251 252 253
class _FakeHttpClientRequest extends Fake implements HttpClientRequest {
  final _FakeHttpClientResponse response = _FakeHttpClientResponse();

  @override
  Future<HttpClientResponse> close() async {
    return response;
  }
254 255
}

256
class _FakeHttpClientResponse extends Fake implements HttpClientResponse {
257 258
  bool drained = false;

259 260 261 262 263 264 265 266 267
  @override
  int statusCode = HttpStatus.ok;

  @override
  int contentLength = 0;

  @override
  HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;

268
  late List<List<int>> content;
269 270

  @override
271
  StreamSubscription<List<int>> listen(void Function(List<int> event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
272 273 274 275 276 277 278
    return Stream<List<int>>.fromIterable(content).listen(
      onData,
      onDone: onDone,
      onError: onError,
      cancelOnError: cancelOnError,
    );
  }
279 280 281 282

  @override
  Future<E> drain<E>([E? futureValue]) async {
    drained = true;
283
    return futureValue ?? futureValue as E; // Mirrors the implementation in Stream.
284
  }
285
}
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301

class FakeCodec implements Codec {
  @override
  void dispose() {}

  @override
  int get frameCount => throw UnimplementedError();

  @override
  Future<FrameInfo> getNextFrame() {
    throw UnimplementedError();
  }

  @override
  int get repetitionCount => throw UnimplementedError();
}