| // Copyright 2013 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' show HttpOverrides; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter_image/network.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:quiver/testing/async.dart'; |
| |
| String _imageUrl(String fileName) { |
| return 'http://localhost:11111/$fileName'; |
| } |
| |
| void main() { |
| AutomatedTestWidgetsFlutterBinding(); |
| HttpOverrides.global = null; |
| |
| group('NetworkImageWithRetry', () { |
| group('succeeds', () { |
| setUp(() { |
| FlutterError.onError = (FlutterErrorDetails error) { |
| fail('$error'); |
| }; |
| }); |
| |
| tearDown(() { |
| FlutterError.onError = FlutterError.dumpErrorToConsole; |
| }); |
| |
| test('loads image from network', () async { |
| final NetworkImageWithRetry subject = NetworkImageWithRetry( |
| _imageUrl('immediate_success.png'), |
| ); |
| |
| assertThatImageLoadingSucceeds(subject); |
| }); |
| |
| test('loads image from network with an extra header', () async { |
| final NetworkImageWithRetry subject = NetworkImageWithRetry( |
| _imageUrl('extra_header.png'), |
| headers: const <String, Object>{'ExtraHeader': 'special'}, |
| ); |
| |
| assertThatImageLoadingSucceeds(subject); |
| }); |
| |
| test('succeeds on successful retry', () async { |
| final NetworkImageWithRetry subject = NetworkImageWithRetry( |
| _imageUrl('error.png'), |
| fetchStrategy: (Uri uri, FetchFailure? failure) async { |
| if (failure == null) { |
| return FetchInstructions.attempt( |
| uri: uri, |
| timeout: const Duration(minutes: 1), |
| ); |
| } else { |
| expect(failure.attemptCount, lessThan(2)); |
| return FetchInstructions.attempt( |
| uri: Uri.parse(_imageUrl('immediate_success.png')), |
| timeout: const Duration(minutes: 1), |
| ); |
| } |
| }, |
| ); |
| assertThatImageLoadingSucceeds(subject); |
| }); |
| }); |
| |
| group('fails', () { |
| final List<FlutterErrorDetails> errorLog = <FlutterErrorDetails>[]; |
| FakeAsync fakeAsync = FakeAsync(); |
| |
| setUp(() { |
| FlutterError.onError = errorLog.add; |
| }); |
| |
| tearDown(() { |
| fakeAsync = FakeAsync(); |
| errorLog.clear(); |
| FlutterError.onError = FlutterError.dumpErrorToConsole; |
| }); |
| |
| test('retries 6 times then gives up', () async { |
| final dynamic maxAttemptCountReached = expectAsync0(() {}); |
| |
| int attemptCount = 0; |
| Future<void> onAttempt() async { |
| expect(attemptCount, lessThan(7)); |
| if (attemptCount == 6) { |
| maxAttemptCountReached(); |
| } |
| await Future<void>.delayed(Duration.zero); |
| fakeAsync.elapse(const Duration(seconds: 60)); |
| attemptCount++; |
| } |
| |
| final NetworkImageWithRetry subject = NetworkImageWithRetry( |
| _imageUrl('error.png'), |
| fetchStrategy: (Uri uri, FetchFailure? failure) { |
| Timer.run(onAttempt); |
| return fakeAsync.run((FakeAsync fakeAsync) { |
| return NetworkImageWithRetry.defaultFetchStrategy(uri, failure); |
| }); |
| }, |
| ); |
| |
| assertThatImageLoadingFails(subject, errorLog); |
| }); |
| |
| test('gives up immediately on non-retriable errors (HTTP 404)', () async { |
| int attemptCount = 0; |
| Future<void> onAttempt() async { |
| expect(attemptCount, lessThan(2)); |
| await Future<void>.delayed(Duration.zero); |
| fakeAsync.elapse(const Duration(seconds: 60)); |
| attemptCount++; |
| } |
| |
| final NetworkImageWithRetry subject = NetworkImageWithRetry( |
| _imageUrl('does_not_exist.png'), |
| fetchStrategy: (Uri uri, FetchFailure? failure) { |
| Timer.run(onAttempt); |
| return fakeAsync.run((FakeAsync fakeAsync) { |
| return NetworkImageWithRetry.defaultFetchStrategy(uri, failure); |
| }); |
| }, |
| ); |
| |
| assertThatImageLoadingFails(subject, errorLog); |
| }); |
| }); |
| }); |
| } |
| |
| void assertThatImageLoadingFails( |
| NetworkImageWithRetry subject, |
| List<FlutterErrorDetails> errorLog, |
| ) { |
| final ImageStreamCompleter completer = subject.load( |
| subject, |
| // TODO(cyanglaz): migrate to use the new APIs |
| // https://github.com/flutter/flutter/issues/105336 |
| // ignore: deprecated_member_use |
| PaintingBinding.instance.instantiateImageCodec, |
| ); |
| completer.addListener(ImageStreamListener( |
| (ImageInfo image, bool synchronousCall) {}, |
| onError: expectAsync2((Object error, StackTrace? _) { |
| expect(errorLog.single.exception, isInstanceOf<FetchFailure>()); |
| expect(error, isInstanceOf<FetchFailure>()); |
| expect(error, equals(errorLog.single.exception)); |
| }), |
| )); |
| } |
| |
| void assertThatImageLoadingSucceeds( |
| NetworkImageWithRetry subject, |
| ) { |
| final ImageStreamCompleter completer = subject.load( |
| subject, |
| // TODO(cyanglaz): migrate to use the new APIs |
| // https://github.com/flutter/flutter/issues/105336 |
| // ignore: deprecated_member_use |
| PaintingBinding.instance.instantiateImageCodec, |
| ); |
| completer.addListener(ImageStreamListener( |
| expectAsync2((ImageInfo image, bool synchronousCall) { |
| expect(image.image.height, 1); |
| expect(image.image.width, 1); |
| }), |
| )); |
| } |