| // 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. |
| |
| // This file is run as part of a reduced test set in CI on Mac and Windows |
| // machines. |
| @Tags(<String>['reduced-test-set']) |
| library; |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:math' as math; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../image_data.dart'; |
| import 'semantics_tester.dart'; |
| |
| void main() { |
| late int originalCacheSize; |
| late ui.Image image10x10; |
| |
| setUp(() async { |
| originalCacheSize = imageCache.maximumSize; |
| imageCache.clear(); |
| imageCache.clearLiveImages(); |
| image10x10 = await createTestImage(width: 10, height: 10); |
| }); |
| |
| tearDown(() { |
| imageCache.maximumSize = originalCacheSize; |
| }); |
| |
| testWidgets('Verify Image does not use disposed handles', (WidgetTester tester) async { |
| final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!; |
| |
| final _TestImageProvider imageProvider1 = _TestImageProvider(); |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| |
| final ValueNotifier<_TestImageProvider> imageListenable = ValueNotifier<_TestImageProvider>(imageProvider1); |
| final ValueNotifier<int> innerListenable = ValueNotifier<int>(0); |
| |
| bool imageLoaded = false; |
| |
| await tester.pumpWidget(ValueListenableBuilder<_TestImageProvider>( |
| valueListenable: imageListenable, |
| builder: (BuildContext context, _TestImageProvider image, Widget? child) => Image( |
| image: image, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| if (frame == 0) { |
| imageLoaded = true; |
| } |
| return LayoutBuilder( |
| builder: (BuildContext context, BoxConstraints constraints) => ValueListenableBuilder<int>( |
| valueListenable: innerListenable, |
| builder: (BuildContext context, int value, Widget? valueListenableChild) => KeyedSubtree( |
| key: UniqueKey(), |
| child: child, |
| ), |
| ), |
| ); |
| }, |
| ), |
| )); |
| |
| imageLoaded = false; |
| imageProvider1.complete(image10x10); |
| await tester.idle(); |
| await tester.pump(); |
| expect(imageLoaded, true); |
| |
| imageLoaded = false; |
| imageListenable.value = imageProvider2; |
| innerListenable.value += 1; |
| imageProvider2.complete(image100x100); |
| await tester.idle(); |
| await tester.pump(); |
| expect(imageLoaded, true); |
| }); |
| |
| testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| final _TestImageProvider imageProvider1 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| image: imageProvider1, |
| excludeFromSemantics: true, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(image10x10); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| image: imageProvider2, |
| excludeFromSemantics: true, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNull); |
| }); |
| |
| testWidgets("Verify Image doesn't reset its RenderImage when changing providers if it has gaplessPlayback set", (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| final _TestImageProvider imageProvider1 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| gaplessPlayback: true, |
| image: imageProvider1, |
| excludeFromSemantics: true, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(image10x10); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| gaplessPlayback: true, |
| image: imageProvider2, |
| excludeFromSemantics: true, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| }); |
| |
| testWidgets('Verify Image resets its RenderImage when changing providers if it has a key', (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| final _TestImageProvider imageProvider1 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Image( |
| key: key, |
| image: imageProvider1, |
| excludeFromSemantics: true, |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(image10x10); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Image( |
| key: key, |
| image: imageProvider2, |
| excludeFromSemantics: true, |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNull); |
| }); |
| |
| testWidgets("Verify Image doesn't reset its RenderImage when changing providers if it has gaplessPlayback set", (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| final _TestImageProvider imageProvider1 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Image( |
| key: key, |
| gaplessPlayback: true, |
| image: imageProvider1, |
| excludeFromSemantics: true, |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(image10x10); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Image( |
| key: key, |
| gaplessPlayback: true, |
| excludeFromSemantics: true, |
| image: imageProvider2, |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| }); |
| |
| testWidgets('Verify ImageProvider configuration inheritance', (WidgetTester tester) async { |
| final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); |
| final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); |
| final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); |
| final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider(); |
| final Set<Object> seenKeys = <Object>{}; |
| final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys); |
| |
| // Of the two nested MediaQuery objects, the innermost one, |
| // mediaQuery2, should define the configuration of the imageProvider. |
| await tester.pumpWidget( |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0); |
| |
| // This is the same widget hierarchy as before except that the |
| // two MediaQuery objects have exchanged places. The imageProvider |
| // should be resolved again, with the new innermost MediaQuery. |
| await tester.pumpWidget( |
| MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 10.0); |
| }); |
| |
| testWidgets('Verify ImageProvider configuration inheritance again', (WidgetTester tester) async { |
| final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); |
| final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); |
| final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); |
| final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider(); |
| final Set<Object> seenKeys = <Object>{}; |
| final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys); |
| |
| // This is just a variation on the previous test. In this version the location |
| // of the Image changes and the MediaQuery widgets do not. |
| await tester.pumpWidget( |
| Row( |
| textDirection: TextDirection.ltr, |
| children: <Widget> [ |
| MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: Container(width: 100.0), |
| ), |
| ], |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0); |
| |
| await tester.pumpWidget( |
| Row( |
| textDirection: TextDirection.ltr, |
| children: <Widget> [ |
| MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: Container(width: 100.0), |
| ), |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| ], |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 10.0); |
| }); |
| |
| testWidgets('Verify ImageProvider does not inherit configuration when it does not key to it', (WidgetTester tester) async { |
| final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); |
| final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); |
| final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| final Set<Object> seenKeys = <Object>{}; |
| final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys); |
| |
| // Of the two nested MediaQuery objects, the innermost one, |
| // mediaQuery2, should define the configuration of the imageProvider. |
| await tester.pumpWidget( |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0); |
| |
| // This is the same widget hierarchy as before except that the |
| // two MediaQuery objects have exchanged places. The imageProvider |
| // should not be resolved again, because it does not key to configuration. |
| await tester.pumpWidget( |
| MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0); |
| }); |
| |
| testWidgets('Verify ImageProvider does not inherit configuration when it does not key to it again', (WidgetTester tester) async { |
| final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); |
| final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); |
| final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| final Set<Object> seenKeys = <Object>{}; |
| final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys); |
| |
| // This is just a variation on the previous test. In this version the location |
| // of the Image changes and the MediaQuery widgets do not. |
| await tester.pumpWidget( |
| Row( |
| textDirection: TextDirection.ltr, |
| children: <Widget> [ |
| MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: Container(width: 100.0), |
| ), |
| ], |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0); |
| |
| await tester.pumpWidget( |
| Row( |
| textDirection: TextDirection.ltr, |
| children: <Widget> [ |
| MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| ), |
| child: Container(width: 100.0), |
| ), |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: debouncingProvider, |
| ), |
| ), |
| ], |
| ), |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0); |
| }); |
| |
| testWidgets('Verify Image stops listening to ImageStream', (WidgetTester tester) async { |
| final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!; |
| // Web does not override the toString, whereas VM does |
| final String imageString = image100x100.toString(); |
| |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| await tester.pumpWidget(Image(image: imageProvider, excludeFromSemantics: true)); |
| final State<Image> image = tester.state/*State<Image>*/(find.byType(Image)); |
| expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null, loadingProgress: null, frameNumber: null, wasSynchronouslyLoaded: false)')); |
| imageProvider.complete(image100x100); |
| await tester.pump(); |
| expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 1 listener), pixels: $imageString @ 1.0x, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)')); |
| await tester.pumpWidget(Container()); |
| expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(lifecycle state: defunct, not mounted, stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 0 listeners), pixels: null, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)')); |
| }); |
| |
| testWidgets('Stream completer errors can be listened to by attaching before resolving', (WidgetTester tester) async { |
| dynamic capturedException; |
| StackTrace? capturedStackTrace; |
| ImageInfo? capturedImage; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| } |
| void listener(ImageInfo info, bool synchronous) { |
| capturedImage = info; |
| } |
| |
| final Exception testException = Exception('cannot resolve host'); |
| final StackTrace testStack = StackTrace.current; |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); |
| late ImageConfiguration configuration; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| configuration = createLocalImageConfiguration(context); |
| return Container(); |
| }, |
| ), |
| ); |
| imageProvider.resolve(configuration); |
| imageProvider.fail(testException, testStack); |
| |
| expect(tester.binding.microtaskCount, 1); |
| await tester.idle(); // Let the failed completer's future hit the stream completer. |
| expect(tester.binding.microtaskCount, 0); |
| |
| expect(capturedImage, isNull); // The image stream listeners should never be called. |
| // The image stream error handler should have the original exception. |
| expect(capturedException, testException); |
| expect(capturedStackTrace, testStack); |
| // If there is an error listener, there should be no FlutterError reported. |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('Stream completer errors can be listened to by attaching after resolving', (WidgetTester tester) async { |
| dynamic capturedException; |
| StackTrace? capturedStackTrace; |
| dynamic reportedException; |
| StackTrace? reportedStackTrace; |
| ImageInfo? capturedImage; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| } |
| void listener(ImageInfo info, bool synchronous) { |
| capturedImage = info; |
| } |
| FlutterError.onError = (FlutterErrorDetails flutterError) { |
| reportedException = flutterError.exception; |
| reportedStackTrace = flutterError.stack; |
| }; |
| |
| final Exception testException = Exception('cannot resolve host'); |
| final StackTrace testStack = StackTrace.current; |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| late ImageConfiguration configuration; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| configuration = createLocalImageConfiguration(context); |
| return Container(); |
| }, |
| ), |
| ); |
| final ImageStream streamUnderTest = imageProvider.resolve(configuration); |
| |
| imageProvider.fail(testException, testStack); |
| |
| expect(tester.binding.microtaskCount, 1); |
| await tester.idle(); // Let the failed completer's future hit the stream completer. |
| expect(tester.binding.microtaskCount, 0); |
| |
| // Since there's no listeners attached yet, report error up via |
| // FlutterError. |
| expect(reportedException, testException); |
| expect(reportedStackTrace, testStack); |
| |
| streamUnderTest.addListener(ImageStreamListener(listener, onError: errorListener)); |
| |
| expect(capturedImage, isNull); // The image stream listeners should never be called. |
| // The image stream error handler should have the original exception. |
| expect(capturedException, testException); |
| expect(capturedStackTrace, testStack); |
| }); |
| |
| testWidgets('Duplicate listener registration does not affect error listeners', (WidgetTester tester) async { |
| dynamic capturedException; |
| StackTrace? capturedStackTrace; |
| ImageInfo? capturedImage; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| } |
| void listener(ImageInfo info, bool synchronous) { |
| capturedImage = info; |
| } |
| |
| final Exception testException = Exception('cannot resolve host'); |
| final StackTrace testStack = StackTrace.current; |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); |
| // Add the exact same listener a second time without the errorListener. |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener)); |
| late ImageConfiguration configuration; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| configuration = createLocalImageConfiguration(context); |
| return Container(); |
| }, |
| ), |
| ); |
| imageProvider.resolve(configuration); |
| imageProvider.fail(testException, testStack); |
| |
| expect(tester.binding.microtaskCount, 1); |
| await tester.idle(); // Let the failed completer's future hit the stream completer. |
| expect(tester.binding.microtaskCount, 0); |
| |
| expect(capturedImage, isNull); // The image stream listeners should never be called. |
| // The image stream error handler should have the original exception. |
| expect(capturedException, testException); |
| expect(capturedStackTrace, testStack); |
| // If there is an error listener, there should be no FlutterError reported. |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('Duplicate error listeners are all called', (WidgetTester tester) async { |
| dynamic capturedException; |
| StackTrace? capturedStackTrace; |
| ImageInfo? capturedImage; |
| int errorListenerCalled = 0; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| errorListenerCalled++; |
| } |
| void listener(ImageInfo info, bool synchronous) { |
| capturedImage = info; |
| } |
| |
| final Exception testException = Exception('cannot resolve host'); |
| final StackTrace testStack = StackTrace.current; |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); |
| // Add the exact same errorListener a second time. |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); |
| late ImageConfiguration configuration; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| configuration = createLocalImageConfiguration(context); |
| return Container(); |
| }, |
| ), |
| ); |
| imageProvider.resolve(configuration); |
| imageProvider.fail(testException, testStack); |
| |
| expect(tester.binding.microtaskCount, 1); |
| await tester.idle(); // Let the failed completer's future hit the stream completer. |
| expect(tester.binding.microtaskCount, 0); |
| |
| expect(capturedImage, isNull); // The image stream listeners should never be called. |
| // The image stream error handler should have the original exception. |
| expect(capturedException, testException); |
| expect(capturedStackTrace, testStack); |
| expect(errorListenerCalled, 2); |
| // If there is an error listener, there should be no FlutterError reported. |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('Listeners are only removed if callback tuple matches', (WidgetTester tester) async { |
| bool errorListenerCalled = false; |
| dynamic reportedException; |
| StackTrace? reportedStackTrace; |
| ImageInfo? capturedImage; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| errorListenerCalled = true; |
| reportedException = exception; |
| reportedStackTrace = stackTrace; |
| } |
| void listener(ImageInfo info, bool synchronous) { |
| capturedImage = info; |
| } |
| |
| final Exception testException = Exception('cannot resolve host'); |
| final StackTrace testStack = StackTrace.current; |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); |
| // Now remove the listener the error listener is attached to. |
| // Don't explicitly remove the error listener. |
| imageProvider._streamCompleter.removeListener(ImageStreamListener(listener)); |
| late ImageConfiguration configuration; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| configuration = createLocalImageConfiguration(context); |
| return Container(); |
| }, |
| ), |
| ); |
| imageProvider.resolve(configuration); |
| |
| imageProvider.fail(testException, testStack); |
| |
| expect(tester.binding.microtaskCount, 1); |
| await tester.idle(); // Let the failed completer's future hit the stream completer. |
| expect(tester.binding.microtaskCount, 0); |
| |
| expect(errorListenerCalled, true); |
| expect(reportedException, testException); |
| expect(reportedStackTrace, testStack); |
| expect(capturedImage, isNull); // The image stream listeners should never be called. |
| }); |
| |
| testWidgets('Removing listener removes one listener and error listener', (WidgetTester tester) async { |
| int errorListenerCalled = 0; |
| ImageInfo? capturedImage; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| errorListenerCalled++; |
| } |
| void listener(ImageInfo info, bool synchronous) { |
| capturedImage = info; |
| } |
| |
| final Exception testException = Exception('cannot resolve host'); |
| final StackTrace testStack = StackTrace.current; |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); |
| // Duplicates the same set of listener and errorListener. |
| imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); |
| // Now remove one entry of the specified listener and associated error listener. |
| // Don't explicitly remove the error listener. |
| imageProvider._streamCompleter.removeListener(ImageStreamListener(listener, onError: errorListener)); |
| late ImageConfiguration configuration; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| configuration = createLocalImageConfiguration(context); |
| return Container(); |
| }, |
| ), |
| ); |
| imageProvider.resolve(configuration); |
| |
| imageProvider.fail(testException, testStack); |
| |
| expect(tester.binding.microtaskCount, 1); |
| await tester.idle(); // Let the failed completer's future hit the stream completer. |
| expect(tester.binding.microtaskCount, 0); |
| |
| expect(errorListenerCalled, 1); |
| expect(capturedImage, isNull); // The image stream listeners should never be called. |
| }); |
| |
| testWidgets('Image.memory control test', (WidgetTester tester) async { |
| await tester.pumpWidget(Image.memory(Uint8List.fromList(kTransparentImage), excludeFromSemantics: true)); |
| }); |
| |
| testWidgets('Image color and colorBlend parameters', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Image( |
| excludeFromSemantics: true, |
| image: _TestImageProvider(), |
| color: const Color(0xFF00FF00), |
| colorBlendMode: BlendMode.clear, |
| ), |
| ); |
| final RenderImage renderer = tester.renderObject<RenderImage>(find.byType(Image)); |
| expect(renderer.color, const Color(0xFF00FF00)); |
| expect(renderer.colorBlendMode, BlendMode.clear); |
| }); |
| |
| testWidgets('Image opacity parameter', (WidgetTester tester) async { |
| const Animation<double> opacity = AlwaysStoppedAnimation<double>(0.5); |
| await tester.pumpWidget( |
| Image( |
| excludeFromSemantics: true, |
| image: _TestImageProvider(), |
| opacity: opacity, |
| ), |
| ); |
| final RenderImage renderer = tester.renderObject<RenderImage>(find.byType(Image)); |
| expect(renderer.opacity, opacity); |
| }); |
| |
| testWidgets('Precache', (WidgetTester tester) async { |
| final _TestImageProvider provider = _TestImageProvider(); |
| late Future<void> precache; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| precache = precacheImage(provider, context); |
| return Container(); |
| }, |
| ), |
| ); |
| provider.complete(image10x10); |
| await precache; |
| expect(provider._lastResolvedConfiguration, isNotNull); |
| |
| // Check that a second resolve of the same image is synchronous. |
| final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration); |
| late bool isSync; |
| stream.addListener(ImageStreamListener((ImageInfo image, bool sync) { isSync = sync; })); |
| expect(isSync, isTrue); |
| }); |
| |
| testWidgets('Precache removes original listener immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async { |
| final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider provider = _TestImageProvider(streamCompleter: imageStreamCompleter); |
| |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| precacheImage(provider, context); |
| return Container(); |
| }, |
| ), |
| ); |
| |
| // Two listeners - one is the listener added by precacheImage, the other by the ImageCache. |
| final List<ImageStreamListener> listeners = imageStreamCompleter.listeners.toList(); |
| expect(listeners.length, 2); |
| |
| // Make sure the first listener can be called re-entrantly |
| final ImageInfo imageInfo = ImageInfo(image: image10x10); |
| listeners[1].onImage(imageInfo.clone(), false); |
| listeners[1].onImage(imageInfo.clone(), false); |
| |
| // Make sure the second listener can be called re-entrantly. |
| listeners[0].onImage(imageInfo.clone(), false); |
| listeners[0].onImage(imageInfo.clone(), false); |
| }); |
| |
| testWidgets('Precache completes with onError on error', (WidgetTester tester) async { |
| dynamic capturedException; |
| StackTrace? capturedStackTrace; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| } |
| |
| final Exception testException = Exception('cannot resolve host'); |
| final StackTrace testStack = StackTrace.current; |
| final _TestImageProvider imageProvider = _TestImageProvider(); |
| late Future<void> precache; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| precache = precacheImage(imageProvider, context, onError: errorListener); |
| return Container(); |
| }, |
| ), |
| ); |
| imageProvider.fail(testException, testStack); |
| await precache; |
| |
| // The image stream error handler should have the original exception. |
| expect(capturedException, testException); |
| expect(capturedStackTrace, testStack); |
| // If there is an error listener, there should be no FlutterError reported. |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('TickerMode controls stream registration', (WidgetTester tester) async { |
| final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter(); |
| final Image image = Image( |
| excludeFromSemantics: true, |
| image: _TestImageProvider(streamCompleter: imageStreamCompleter), |
| ); |
| await tester.pumpWidget( |
| TickerMode( |
| enabled: true, |
| child: image, |
| ), |
| ); |
| expect(imageStreamCompleter.listeners.length, 2); |
| await tester.pumpWidget( |
| TickerMode( |
| enabled: false, |
| child: image, |
| ), |
| ); |
| expect(imageStreamCompleter.listeners.length, 1); |
| }); |
| |
| testWidgets('Verify Image shows correct RenderImage when changing to an already completed provider', (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| |
| final _TestImageProvider imageProvider1 = _TestImageProvider(); |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!; |
| |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| excludeFromSemantics: true, |
| image: imageProvider1, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(image10x10); |
| imageProvider2.complete(image100x100); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| |
| final ui.Image oldImage = renderImage.image!; |
| |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| excludeFromSemantics: true, |
| image: imageProvider2, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| |
| renderImage = key.currentContext!.findRenderObject()! as RenderImage; |
| expect(renderImage.image, isNotNull); |
| expect(renderImage.image, isNot(equals(oldImage))); |
| }); |
| |
| testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async { |
| final Image image1 = Image(image: _TestImageProvider()..complete(image10x10.clone()), width: 10.0, excludeFromSemantics: true); |
| final Image image2 = Image(image: _TestImageProvider()..complete(image10x10.clone()), width: 20.0, excludeFromSemantics: true); |
| |
| final Column column = Column(children: <Widget>[image1, image2]); |
| await tester.pumpWidget(column, null, EnginePhase.layout); |
| |
| final Column columnSwapped = Column(children: <Widget>[image2, image1]); |
| await tester.pumpWidget(columnSwapped, null, EnginePhase.layout); |
| |
| final List<RenderImage> renderObjects = tester.renderObjectList<RenderImage>(find.byType(Image)).toList(); |
| expect(renderObjects, hasLength(2)); |
| expect(renderObjects[0].image, isNotNull); |
| expect(renderObjects[0].width, 20.0); |
| expect(renderObjects[1].image, isNotNull); |
| expect(renderObjects[1].width, 10.0); |
| }); |
| |
| testWidgets('Image contributes semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Row( |
| children: <Widget>[ |
| Image( |
| image: _TestImageProvider(), |
| width: 100.0, |
| height: 100.0, |
| semanticLabel: 'test', |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| id: 1, |
| label: 'test', |
| rect: const Rect.fromLTWH(0.0, 0.0, 100.0, 100.0), |
| textDirection: TextDirection.ltr, |
| flags: <SemanticsFlag>[SemanticsFlag.isImage], |
| ), |
| ], |
| ), ignoreTransform: true)); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Image can exclude semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Image( |
| image: _TestImageProvider(), |
| width: 100.0, |
| height: 100.0, |
| excludeFromSemantics: true, |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics(TestSemantics.root( |
| children: <TestSemantics>[], |
| ))); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Image invokes frameBuilder with correct frameNumber argument', (WidgetTester tester) async { |
| final ui.Codec codec = (await tester.runAsync(() { |
| return ui.instantiateImageCodec(Uint8List.fromList(kAnimatedGif)); |
| }))!; |
| |
| Future<ui.Image> nextFrame() async { |
| final ui.FrameInfo frameInfo = (await tester.runAsync(codec.getNextFrame))!; |
| return frameInfo.image; |
| } |
| |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| int? lastFrame; |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| lastFrame = frame; |
| return Center(child: child); |
| }, |
| ), |
| ); |
| |
| expect(lastFrame, isNull); |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame())); |
| await tester.pump(); |
| expect(lastFrame, 0); |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame())); |
| await tester.pump(); |
| expect(lastFrame, 1); |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| }); |
| |
| testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=false', (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| int? lastFrame; |
| late bool lastFrameWasSync; |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| lastFrame = frame; |
| lastFrameWasSync = wasSynchronouslyLoaded; |
| return child; |
| }, |
| ), |
| ); |
| |
| expect(lastFrame, isNull); |
| expect(lastFrameWasSync, isFalse); |
| expect(find.byType(RawImage), findsOneWidget); |
| streamCompleter.setData(imageInfo: ImageInfo(image: image10x10)); |
| await tester.pump(); |
| expect(lastFrame, 0); |
| expect(lastFrameWasSync, isFalse); |
| }); |
| |
| testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=true', (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(ImageInfo(image: image10x10.clone())); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| int? lastFrame; |
| late bool lastFrameWasSync; |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| lastFrame = frame; |
| lastFrameWasSync = wasSynchronouslyLoaded; |
| return child; |
| }, |
| ), |
| ); |
| |
| expect(lastFrame, 0); |
| expect(lastFrameWasSync, isTrue); |
| expect(find.byType(RawImage), findsOneWidget); |
| streamCompleter.setData(imageInfo: ImageInfo(image: image10x10.clone())); |
| await tester.pump(); |
| expect(lastFrame, 1); |
| expect(lastFrameWasSync, isTrue); |
| }); |
| |
| testWidgets('Image state handles frameBuilder update', (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| return Center(child: child); |
| }, |
| ), |
| ); |
| |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| final State<Image> state = tester.state(find.byType(Image)); |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| return Padding(padding: const EdgeInsets.all(1), child: child); |
| }, |
| ), |
| ); |
| |
| expect(find.byType(Center), findsNothing); |
| expect(find.byType(Padding), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| expect(tester.state(find.byType(Image)), same(state)); |
| }); |
| |
| testWidgets('Image state handles enabling and disabling of tickers', (WidgetTester tester) async { |
| final ui.Codec codec = (await tester.runAsync(() { |
| return ui.instantiateImageCodec(Uint8List.fromList(kAnimatedGif)); |
| }))!; |
| |
| Future<ui.Image> nextFrame() async { |
| final ui.FrameInfo frameInfo = (await tester.runAsync(codec.getNextFrame))!; |
| return frameInfo.image; |
| } |
| |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| int? lastFrame; |
| int buildCount = 0; |
| |
| Widget buildFrame(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| lastFrame = frame; |
| buildCount++; |
| return child; |
| } |
| |
| await tester.pumpWidget( |
| TickerMode( |
| enabled: true, |
| child: Image( |
| image: imageProvider, |
| frameBuilder: buildFrame, |
| ), |
| ), |
| ); |
| |
| final State<Image> state = tester.state(find.byType(Image)); |
| expect(lastFrame, isNull); |
| expect(buildCount, 1); |
| streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame())); |
| await tester.pump(); |
| expect(lastFrame, 0); |
| expect(buildCount, 2); |
| |
| await tester.pumpWidget( |
| TickerMode( |
| enabled: false, |
| child: Image( |
| image: imageProvider, |
| frameBuilder: buildFrame, |
| ), |
| ), |
| ); |
| |
| expect(tester.state(find.byType(Image)), same(state)); |
| expect(lastFrame, 0); |
| expect(buildCount, 3); |
| streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame())); |
| streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame())); |
| await tester.pump(); |
| expect(lastFrame, 0); |
| expect(buildCount, 3); |
| |
| await tester.pumpWidget( |
| TickerMode( |
| enabled: true, |
| child: Image( |
| image: imageProvider, |
| frameBuilder: buildFrame, |
| ), |
| ), |
| ); |
| |
| expect(tester.state(find.byType(Image)), same(state)); |
| expect(lastFrame, 1); // missed a frame because we weren't animating at the time |
| expect(buildCount, 4); |
| }); |
| |
| testWidgets('Image invokes loadingBuilder on chunk event notification', (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| final List<ImageChunkEvent?> chunkEvents = <ImageChunkEvent?>[]; |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { |
| chunkEvents.add(loadingProgress); |
| if (loadingProgress == null) { |
| return child; |
| } |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Text('loading ${loadingProgress.cumulativeBytesLoaded} / ${loadingProgress.expectedTotalBytes}'), |
| ); |
| }, |
| ), |
| ); |
| |
| expect(chunkEvents.length, 1); |
| expect(chunkEvents.first, isNull); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isTrue); |
| await tester.pump(); |
| expect(chunkEvents.length, 2); |
| expect(find.text('loading 10 / 100'), findsOneWidget); |
| expect(find.byType(RawImage), findsNothing); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 30, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isTrue); |
| await tester.pump(); |
| expect(chunkEvents.length, 3); |
| expect(find.text('loading 30 / 100'), findsOneWidget); |
| expect(find.byType(RawImage), findsNothing); |
| streamCompleter.setData(imageInfo: ImageInfo(image: image10x10)); |
| await tester.pump(); |
| expect(chunkEvents.length, 4); |
| expect(find.byType(Text), findsNothing); |
| expect(find.byType(RawImage), findsOneWidget); |
| }); |
| |
| testWidgets("Image doesn't rebuild on chunk events if loadingBuilder is null", (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| excludeFromSemantics: true, |
| ), |
| ); |
| |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| streamCompleter.setData(imageInfo: ImageInfo(image: image10x10)); |
| expect(tester.binding.hasScheduledFrame, isTrue); |
| await tester.pump(); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| expect(find.byType(RawImage), findsOneWidget); |
| }); |
| |
| testWidgets('Image chains the results of frameBuilder and loadingBuilder', (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| excludeFromSemantics: true, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| return Padding(padding: const EdgeInsets.all(1), child: child); |
| }, |
| loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { |
| return Center(child: child); |
| }, |
| ), |
| ); |
| |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(Padding), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>()); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| await tester.pump(); |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(Padding), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| expect(tester.widget<Center>(find.byType(Center)).child, isA<Padding>()); |
| expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>()); |
| }); |
| |
| testWidgets('Image state handles loadingBuilder update from null to non-null', (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| |
| await tester.pumpWidget( |
| Image(image: imageProvider), |
| ); |
| |
| expect(find.byType(RawImage), findsOneWidget); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| final State<Image> state = tester.state(find.byType(Image)); |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { |
| return Center(child: child); |
| }, |
| ), |
| ); |
| |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| expect(tester.state(find.byType(Image)), same(state)); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isTrue); |
| await tester.pump(); |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| }); |
| |
| testWidgets('Image state handles loadingBuilder update from non-null to null', (WidgetTester tester) async { |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| |
| await tester.pumpWidget( |
| Image( |
| image: imageProvider, |
| loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { |
| return Center(child: child); |
| }, |
| ), |
| ); |
| |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isTrue); |
| await tester.pump(); |
| expect(find.byType(Center), findsOneWidget); |
| expect(find.byType(RawImage), findsOneWidget); |
| final State<Image> state = tester.state(find.byType(Image)); |
| |
| await tester.pumpWidget( |
| Image(image: imageProvider), |
| ); |
| |
| expect(find.byType(Center), findsNothing); |
| expect(find.byType(RawImage), findsOneWidget); |
| expect(tester.state(find.byType(Image)), same(state)); |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| expect(tester.binding.hasScheduledFrame, isFalse); |
| }); |
| |
| testWidgets('Verify Image resets its ImageListeners', (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| image: imageProvider1, |
| ), |
| ), |
| ); |
| // listener from resolveStreamForKey is always added. |
| expect(imageStreamCompleter.listeners.length, 2); |
| |
| |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| image: imageProvider2, |
| excludeFromSemantics: true, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| |
| // only listener from resolveStreamForKey is left. |
| expect(imageStreamCompleter.listeners.length, 1); |
| }); |
| |
| testWidgets('Verify Image resets its ErrorListeners', (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| image: imageProvider1, |
| errorBuilder: (_,__,___) => Container(), |
| ), |
| ), |
| ); |
| // listener from resolveStreamForKey is always added. |
| expect(imageStreamCompleter.listeners.length, 2); |
| |
| |
| final _TestImageProvider imageProvider2 = _TestImageProvider(); |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| image: imageProvider2, |
| excludeFromSemantics: true, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| |
| // only listener from resolveStreamForKey is left. |
| expect(imageStreamCompleter.listeners.length, 1); |
| }); |
| |
| testWidgets('Image defers loading while fast scrolling', (WidgetTester tester) async { |
| const int gridCells = 1000; |
| final List<_TestImageProvider> imageProviders = <_TestImageProvider>[]; |
| final ScrollController controller = ScrollController(); |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: GridView.builder( |
| controller: controller, |
| gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), |
| itemCount: gridCells, |
| itemBuilder: (_, int index) { |
| final _TestImageProvider provider = _TestImageProvider(); |
| imageProviders.add(provider); |
| return SizedBox( |
| height: 250, |
| width: 250, |
| child: Image( |
| image: provider, |
| semanticLabel: index.toString(), |
| ), |
| ); |
| }, |
| ), |
| )); |
| |
| bool loadCalled(_TestImageProvider provider) => provider.loadCalled; |
| bool loadNotCalled(_TestImageProvider provider) => !provider.loadCalled; |
| |
| expect(find.bySemanticsLabel('5'), findsOneWidget); |
| expect(imageProviders.length, 12); |
| expect(imageProviders.every(loadCalled), true); |
| |
| imageProviders.clear(); |
| |
| // Simulate a very fast fling. |
| controller.animateTo( |
| 30000, |
| duration: const Duration(seconds: 2), |
| curve: Curves.linear, |
| ); |
| await tester.pumpAndSettle(); |
| // The last 15 images on screen have loaded because the scrolling settled there. |
| // The rest have not loaded. |
| expect(imageProviders.length, 309); |
| expect(imageProviders.skip(309 - 15).every(loadCalled), true); |
| expect(imageProviders.take(309 - 15).every(loadNotCalled), true); |
| }); |
| |
| testWidgets('Same image provider in multiple parts of the tree, no cache room left', (WidgetTester tester) async { |
| imageCache.maximumSize = 0; |
| |
| final _TestImageProvider provider1 = _TestImageProvider(); |
| final _TestImageProvider provider2 = _TestImageProvider(); |
| |
| expect(provider1.loadCallCount, 0); |
| expect(provider2.loadCallCount, 0); |
| expect(imageCache.liveImageCount, 0); |
| |
| await tester.pumpWidget(Column( |
| children: <Widget>[ |
| Image(image: provider1), |
| Image(image: provider2), |
| Image(image: provider1), |
| Image(image: provider1), |
| Image(image: provider2), |
| ], |
| )); |
| |
| expect(imageCache.liveImageCount, 2); |
| expect(imageCache.statusForKey(provider1).live, true); |
| expect(imageCache.statusForKey(provider1).pending, false); |
| expect(imageCache.statusForKey(provider1).keepAlive, false); |
| expect(imageCache.statusForKey(provider2).live, true); |
| expect(imageCache.statusForKey(provider2).pending, false); |
| expect(imageCache.statusForKey(provider2).keepAlive, false); |
| |
| expect(provider1.loadCallCount, 1); |
| expect(provider2.loadCallCount, 1); |
| |
| provider1.complete(image10x10.clone()); |
| await tester.idle(); |
| |
| provider2.complete(image10x10.clone()); |
| await tester.idle(); |
| |
| expect(imageCache.liveImageCount, 2); |
| expect(imageCache.currentSize, 0); |
| |
| await tester.pumpWidget(Image(image: provider2)); |
| await tester.idle(); |
| expect(imageCache.statusForKey(provider1).untracked, true); |
| expect(imageCache.statusForKey(provider2).live, true); |
| expect(imageCache.statusForKey(provider2).pending, false); |
| expect(imageCache.statusForKey(provider2).keepAlive, false); |
| expect(imageCache.liveImageCount, 1); |
| |
| await tester.pumpWidget(const SizedBox()); |
| await tester.idle(); |
| expect(provider1.loadCallCount, 1); |
| expect(provider2.loadCallCount, 1); |
| expect(imageCache.liveImageCount, 0); |
| }); |
| |
| testWidgets('precacheImage does not hold weak ref for more than a frame', (WidgetTester tester) async { |
| imageCache.maximumSize = 0; |
| final _TestImageProvider provider = _TestImageProvider(); |
| late Future<void> precache; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| precache = precacheImage(provider, context); |
| return Container(); |
| }, |
| ), |
| ); |
| provider.complete(image10x10); |
| await precache; |
| |
| // Should have ended up with only a weak ref, not in cache because cache size is 0 |
| expect(imageCache.liveImageCount, 1); |
| expect(imageCache.containsKey(provider), false); |
| |
| final ImageCacheStatus providerLocation = (await provider.obtainCacheStatus(configuration: ImageConfiguration.empty))!; |
| |
| expect(providerLocation, isNotNull); |
| expect(providerLocation.live, true); |
| expect(providerLocation.keepAlive, false); |
| expect(providerLocation.pending, false); |
| |
| // Check that a second resolve of the same image is synchronous. |
| expect(provider._lastResolvedConfiguration, isNotNull); |
| final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration); |
| late bool isSync; |
| final ImageStreamListener listener = ImageStreamListener((ImageInfo image, bool syncCall) { isSync = syncCall; }); |
| |
| // Still have live ref because frame has not pumped yet. |
| await tester.pump(); |
| expect(imageCache.liveImageCount, 1); |
| |
| SchedulerBinding.instance.scheduleFrame(); |
| await tester.pump(); |
| // Live ref should be gone - we didn't listen to the stream. |
| expect(imageCache.liveImageCount, 0); |
| expect(imageCache.currentSize, 0); |
| |
| stream.addListener(listener); |
| expect(isSync, true); // because the stream still has the image. |
| |
| expect(imageCache.liveImageCount, 0); |
| expect(imageCache.currentSize, 0); |
| |
| expect(provider.loadCallCount, 1); |
| }); |
| |
| testWidgets('precacheImage allows time to take over weak reference', (WidgetTester tester) async { |
| final _TestImageProvider provider = _TestImageProvider(); |
| late Future<void> precache; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| precache = precacheImage(provider, context); |
| return Container(); |
| }, |
| ), |
| ); |
| provider.complete(image10x10); |
| await precache; |
| |
| // Should have ended up in the cache and have a weak reference. |
| expect(imageCache.liveImageCount, 1); |
| expect(imageCache.currentSize, 1); |
| expect(imageCache.containsKey(provider), true); |
| |
| // Check that a second resolve of the same image is synchronous. |
| expect(provider._lastResolvedConfiguration, isNotNull); |
| final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration); |
| late bool isSync; |
| final ImageStreamListener listener = ImageStreamListener((ImageInfo image, bool syncCall) { isSync = syncCall; }); |
| |
| // Should have ended up in the cache and still have a weak reference. |
| expect(imageCache.liveImageCount, 1); |
| expect(imageCache.currentSize, 1); |
| expect(imageCache.containsKey(provider), true); |
| |
| stream.addListener(listener); |
| expect(isSync, true); |
| |
| expect(imageCache.liveImageCount, 1); |
| expect(imageCache.currentSize, 1); |
| expect(imageCache.containsKey(provider), true); |
| |
| SchedulerBinding.instance.scheduleFrame(); |
| await tester.pump(); |
| |
| expect(imageCache.liveImageCount, 1); |
| expect(imageCache.currentSize, 1); |
| expect(imageCache.containsKey(provider), true); |
| stream.removeListener(listener); |
| |
| expect(imageCache.liveImageCount, 0); |
| expect(imageCache.currentSize, 1); |
| expect(imageCache.containsKey(provider), true); |
| expect(provider.loadCallCount, 1); |
| }); |
| |
| testWidgets('evict an image during precache', (WidgetTester tester) async { |
| // This test checks that the live image tracking does not hold on to a |
| // pending image that will never complete because it has been evicted from |
| // the cache. |
| // The scenario may arise in a test harness that is trying to load real |
| // images using `tester.runAsync()`, and wants to make sure that widgets |
| // under test have not also tried to resolve the image in a FakeAsync zone. |
| // The image loaded in the FakeAsync zone will never complete, and the |
| // runAsync call wants to make sure it gets a load attempt from the correct |
| // zone. |
| final Uint8List bytes = Uint8List.fromList(kTransparentImage); |
| final MemoryImage provider = MemoryImage(bytes); |
| |
| await tester.runAsync(() async { |
| final List<Future<void>> futures = <Future<void>>[]; |
| await tester.pumpWidget(Builder(builder: (BuildContext context) { |
| futures.add(precacheImage(provider, context)); |
| imageCache.evict(provider); |
| futures.add(precacheImage(provider, context)); |
| return const SizedBox.expand(); |
| })); |
| await Future.wait<void>(futures); |
| expect(imageCache.statusForKey(provider).keepAlive, true); |
| expect(imageCache.statusForKey(provider).live, true); |
| |
| // Schedule a frame to get precacheImage to stop listening. |
| SchedulerBinding.instance.scheduleFrame(); |
| await tester.pump(); |
| expect(imageCache.statusForKey(provider).keepAlive, true); |
| expect(imageCache.statusForKey(provider).live, false); |
| }); |
| }); |
| |
| testWidgets('errorBuilder - fails on key', (WidgetTester tester) async { |
| final UniqueKey errorKey = UniqueKey(); |
| late Object caughtException; |
| await tester.pumpWidget( |
| Image( |
| image: _FailingImageProvider(failOnObtainKey: true, throws: 'threw', image: image10x10), |
| errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { |
| caughtException = error; |
| return SizedBox.expand(key: errorKey); |
| }, |
| ), |
| ); |
| |
| await tester.pump(); |
| |
| expect(find.byKey(errorKey), findsOneWidget); |
| expect(caughtException.toString(), 'threw'); |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('errorBuilder - fails on load', (WidgetTester tester) async { |
| final UniqueKey errorKey = UniqueKey(); |
| late Object caughtException; |
| await tester.pumpWidget( |
| Image( |
| image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10), |
| errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { |
| caughtException = error; |
| return SizedBox.expand(key: errorKey); |
| }, |
| ), |
| ); |
| |
| await tester.pump(); |
| |
| expect(find.byKey(errorKey), findsOneWidget); |
| expect(caughtException.toString(), 'threw'); |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('no errorBuilder - failure reported to FlutterError', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Image( |
| image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10), |
| ), |
| ); |
| |
| await tester.pump(); |
| |
| expect(tester.takeException(), 'threw'); |
| }); |
| |
| Future<void> testRotatedImage(WidgetTester tester, bool isAntiAlias) async { |
| final Key key = UniqueKey(); |
| await tester.pumpWidget(RepaintBoundary( |
| key: key, |
| child: Transform.rotate( |
| angle: math.pi / 180, |
| child: Image.memory(Uint8List.fromList(kBlueRectPng), isAntiAlias: isAntiAlias), |
| ), |
| )); |
| |
| // precacheImage is needed, or the image in the golden file will be empty. |
| if (!kIsWeb) { |
| final Finder allImages = find.byType(Image); |
| for (final Element e in allImages.evaluate()) { |
| await tester.runAsync(() async { |
| final Image image = e.widget as Image; |
| await precacheImage(image.image, e); |
| }); |
| } |
| await tester.pumpAndSettle(); |
| } |
| |
| await expectLater( |
| find.byKey(key), |
| matchesGoldenFile('rotated_image_${isAntiAlias ? 'aa' : 'noaa'}.png'), |
| ); |
| } |
| |
| testWidgets( |
| 'Rotated images', |
| (WidgetTester tester) async { |
| await testRotatedImage(tester, true); |
| await testRotatedImage(tester, false); |
| }, |
| skip: kIsWeb, // https://github.com/flutter/flutter/issues/87933. |
| ); |
| |
| testWidgets( |
| 'Image opacity', |
| (WidgetTester tester) async { |
| final Key key = UniqueKey(); |
| await tester.pumpWidget(RepaintBoundary( |
| key: key, |
| child: Row( |
| mainAxisAlignment: MainAxisAlignment.spaceAround, |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| Image.memory( |
| Uint8List.fromList(kBlueRectPng), |
| opacity: const AlwaysStoppedAnimation<double>(0.25), |
| ), |
| Image.memory( |
| Uint8List.fromList(kBlueRectPng), |
| opacity: const AlwaysStoppedAnimation<double>(0.5), |
| ), |
| Image.memory( |
| Uint8List.fromList(kBlueRectPng), |
| opacity: const AlwaysStoppedAnimation<double>(0.75), |
| ), |
| Image.memory( |
| Uint8List.fromList(kBlueRectPng), |
| opacity: const AlwaysStoppedAnimation<double>(1.0), |
| ), |
| ], |
| ), |
| )); |
| |
| // precacheImage is needed, or the image in the golden file will be empty. |
| if (!kIsWeb) { |
| final Finder allImages = find.byType(Image); |
| for (final Element e in allImages.evaluate()) { |
| await tester.runAsync(() async { |
| final Image image = e.widget as Image; |
| await precacheImage(image.image, e); |
| }); |
| } |
| await tester.pumpAndSettle(); |
| } |
| |
| await expectLater( |
| find.byKey(key), |
| matchesGoldenFile('transparent_image.png'), |
| ); |
| }, |
| skip: kIsWeb, // https://github.com/flutter/flutter/issues/87933. |
| ); |
| |
| testWidgets('Reports image size when painted', (WidgetTester tester) async { |
| late ImageSizeInfo imageSizeInfo; |
| int count = 0; |
| debugOnPaintImage = (ImageSizeInfo info) { |
| count += 1; |
| imageSizeInfo = info; |
| }; |
| |
| final ui.Image image = (await tester.runAsync(() => createTestImage(width: 100, height: 100)))!; |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter( |
| ImageInfo( |
| image: image, |
| debugLabel: 'test.png', |
| ), |
| ); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| |
| await tester.pumpWidget( |
| Center( |
| child: SizedBox( |
| height: 50, |
| width: 50, |
| child: Image(image: imageProvider), |
| ), |
| ), |
| ); |
| |
| expect(count, 1); |
| expect( |
| imageSizeInfo, |
| const ImageSizeInfo( |
| source: 'test.png', |
| imageSize: Size(100, 100), |
| displaySize: Size(150, 150), |
| ), |
| ); |
| |
| debugOnPaintImage = null; |
| }); |
| |
| testWidgets('Disposes image handle when disposed', (WidgetTester tester) async { |
| final ui.Image image = (await tester.runAsync(() => createTestImage(cache: false)))!; |
| |
| expect(image.debugGetOpenHandleStackTraces()!.length, 1); |
| |
| final ImageProvider provider = _TestImageProvider( |
| streamCompleter: OneFrameImageStreamCompleter( |
| Future<ImageInfo>.value( |
| ImageInfo( |
| image: image, |
| debugLabel: '_TestImage', |
| ), |
| ), |
| ), |
| ); |
| |
| // creating the provider should not have changed anything, and the provider |
| // now owns the handle. |
| expect(image.debugGetOpenHandleStackTraces()!.length, 1); |
| |
| await tester.pumpWidget(Image(image: provider)); |
| |
| // Image widget + 1, render object + 1 |
| expect(image.debugGetOpenHandleStackTraces()!.length, 3); |
| |
| await tester.pumpWidget(const SizedBox()); |
| |
| // Image widget and render object go away |
| expect(image.debugGetOpenHandleStackTraces()!.length, 1); |
| |
| await provider.evict(); |
| |
| tester.binding.scheduleFrame(); |
| await tester.pump(); |
| |
| // Image cache listener go away and Image stream listeners go away. |
| // Image is now at zero. |
| expect(image.debugGetOpenHandleStackTraces()!.length, 0); |
| }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442 |
| |
| testWidgets('Keeps stream alive when ticker mode is disabled', (WidgetTester tester) async { |
| imageCache.maximumSize = 0; |
| final ui.Image image = (await tester.runAsync(() => createTestImage(cache: false)))!; |
| final _TestImageProvider provider = _TestImageProvider(); |
| provider.complete(image); |
| |
| await tester.pumpWidget( |
| TickerMode( |
| enabled: true, |
| child: Image(image: provider), |
| ), |
| ); |
| expect(find.byType(Image), findsOneWidget); |
| |
| await tester.pumpWidget(TickerMode( |
| enabled: false, |
| child: Image(image: provider), |
| ), |
| ); |
| expect(find.byType(Image), findsOneWidget); |
| |
| await tester.pumpWidget(TickerMode( |
| enabled: true, |
| child: Image(image: provider), |
| ), |
| ); |
| expect(find.byType(Image), findsOneWidget); |
| }); |
| |
| testWidgets('Load a good image after a bad image was loaded should not call errorBuilder', (WidgetTester tester) async { |
| final UniqueKey errorKey = UniqueKey(); |
| final ui.Image image = (await tester.runAsync(() => createTestImage()))!; |
| final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(); |
| final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter); |
| |
| await tester.pumpWidget( |
| Center( |
| child: SizedBox( |
| height: 50, |
| width: 50, |
| child: Image( |
| image: imageProvider, |
| excludeFromSemantics: true, |
| errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { |
| return Container(key: errorKey); |
| }, |
| frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { |
| return Padding(padding: const EdgeInsets.all(1), child: child); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // No error widget before loading a invalid image. |
| expect(find.byKey(errorKey), findsNothing); |
| |
| // Loading good image succeed |
| streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100)); |
| await tester.pump(); |
| expect(find.byType(Padding), findsOneWidget); |
| |
| // Loading bad image shows the error widget. |
| streamCompleter.setError(exception: 'thrown'); |
| await tester.pump(); |
| expect(find.byKey(errorKey), findsOneWidget); |
| |
| // Loading good image shows the image widget instead of the error widget. |
| streamCompleter.setData(imageInfo: ImageInfo(image: image)); |
| await tester.pump(); |
| expect(find.byType(Padding), findsOneWidget); |
| expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>()); |
| expect(find.byKey(errorKey), findsNothing); |
| }); |
| |
| testWidgets('Failed image loads in debug mode', (WidgetTester tester) async { |
| final Key key = UniqueKey(); |
| await tester.pumpWidget(Center( |
| child: RepaintBoundary( |
| key: key, |
| child: Container( |
| width: 150.0, |
| height: 50.0, |
| decoration: BoxDecoration( |
| border: Border.all( |
| width: 2.0, |
| color: const Color(0xFF00FF99), |
| ), |
| ), |
| child: Image.asset('missing-asset'), |
| ), |
| ), |
| )); |
| await expectLater( |
| find.byKey(key), |
| matchesGoldenFile('image_test.missing.1.png'), |
| ); |
| expect( |
| tester.takeException().toString(), |
| equals( |
| 'Unable to load asset: "missing-asset".\n' |
| 'The asset does not exist or has empty data.', |
| ), |
| ); |
| await tester.pump(); |
| await expectLater( |
| find.byKey(key), |
| matchesGoldenFile('image_test.missing.2.png'), |
| ); |
| }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/74935 (broken assets not being reported on web) |
| |
| testWidgets('Image.file throws a non-implemented error on web', (WidgetTester tester) async { |
| const String expectedError = |
| 'Image.file is not supported on Flutter Web. ' |
| 'Consider using either Image.asset or Image.network instead.'; |
| final Uri uri = Uri.parse('/home/flutter/dash.png'); |
| final File file = File.fromUri(uri); |
| expect( |
| () => Image.file(file), |
| kIsWeb |
| // Web does not support file access, expect AssertionError |
| ? throwsA(predicate((AssertionError e) => e.message == expectedError)) |
| // AOT supports file access, expect constructor to succeed |
| : isNot(throwsA(anything)), |
| ); |
| }); |
| } |
| |
| @immutable |
| class _ConfigurationAwareKey { |
| const _ConfigurationAwareKey(this.provider, this.configuration); |
| |
| final ImageProvider provider; |
| final ImageConfiguration configuration; |
| |
| @override |
| bool operator ==(Object other) { |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is _ConfigurationAwareKey |
| && other.provider == provider |
| && other.configuration == configuration; |
| } |
| |
| @override |
| int get hashCode => Object.hash(provider, configuration); |
| } |
| |
| class _ConfigurationKeyedTestImageProvider extends _TestImageProvider { |
| @override |
| Future<_ConfigurationAwareKey> obtainKey(ImageConfiguration configuration) { |
| return SynchronousFuture<_ConfigurationAwareKey>(_ConfigurationAwareKey(this, configuration)); |
| } |
| } |
| |
| class _TestImageProvider extends ImageProvider<Object> { |
| _TestImageProvider({ImageStreamCompleter? streamCompleter}) { |
| _streamCompleter = streamCompleter |
| ?? OneFrameImageStreamCompleter(_completer.future); |
| } |
| |
| final Completer<ImageInfo> _completer = Completer<ImageInfo>(); |
| late ImageStreamCompleter _streamCompleter; |
| late ImageConfiguration _lastResolvedConfiguration; |
| |
| bool get loadCalled => _loadCallCount > 0; |
| int get loadCallCount => _loadCallCount; |
| int _loadCallCount = 0; |
| |
| @override |
| Future<Object> obtainKey(ImageConfiguration configuration) { |
| return SynchronousFuture<_TestImageProvider>(this); |
| } |
| |
| @override |
| void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, Object key, ImageErrorListener handleError) { |
| _lastResolvedConfiguration = configuration; |
| super.resolveStreamForKey(configuration, stream, key, handleError); |
| } |
| |
| @override |
| ImageStreamCompleter load(Object key, DecoderCallback decode) { |
| _loadCallCount += 1; |
| return _streamCompleter; |
| } |
| |
| void complete(ui.Image image) { |
| _completer.complete(ImageInfo(image: image)); |
| } |
| |
| void fail(Object exception, StackTrace? stackTrace) { |
| _completer.completeError(exception, stackTrace); |
| } |
| |
| @override |
| String toString() => '${describeIdentity(this)}()'; |
| } |
| |
| class _TestImageStreamCompleter extends ImageStreamCompleter { |
| _TestImageStreamCompleter([this._currentImage]); |
| |
| ImageInfo? _currentImage; |
| final Set<ImageStreamListener> listeners = <ImageStreamListener>{}; |
| |
| @override |
| void addListener(ImageStreamListener listener) { |
| listeners.add(listener); |
| if (_currentImage != null) { |
| listener.onImage(_currentImage!.clone(), true); |
| } |
| } |
| |
| @override |
| void removeListener(ImageStreamListener listener) { |
| listeners.remove(listener); |
| } |
| |
| void setData({ |
| ImageInfo? imageInfo, |
| ImageChunkEvent? chunkEvent, |
| }) { |
| if (imageInfo != null) { |
| _currentImage?.dispose(); |
| _currentImage = imageInfo; |
| } |
| final List<ImageStreamListener> localListeners = listeners.toList(); |
| for (final ImageStreamListener listener in localListeners) { |
| if (imageInfo != null) { |
| listener.onImage(imageInfo.clone(), false); |
| } |
| if (chunkEvent != null && listener.onChunk != null) { |
| listener.onChunk!(chunkEvent); |
| } |
| } |
| } |
| |
| void setError({ |
| required Object exception, |
| StackTrace? stackTrace, |
| }) { |
| final List<ImageStreamListener> localListeners = listeners.toList(); |
| for (final ImageStreamListener listener in localListeners) { |
| listener.onError?.call(exception, stackTrace); |
| } |
| } |
| } |
| |
| class _DebouncingImageProvider extends ImageProvider<Object> { |
| _DebouncingImageProvider(this.imageProvider, this.seenKeys); |
| |
| /// A set of keys that will only get resolved the _first_ time they are seen. |
| /// |
| /// If an ImageProvider produces the same key for two different image |
| /// configurations, it should only actually resolve once using this provider. |
| /// However, if it does care about image configuration, it should make the |
| /// property or properties it cares about part of the key material it |
| /// produces. |
| final Set<Object> seenKeys; |
| final ImageProvider<Object> imageProvider; |
| |
| @override |
| void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, Object key, ImageErrorListener handleError) { |
| if (seenKeys.add(key)) { |
| imageProvider.resolveStreamForKey(configuration, stream, key, handleError); |
| } |
| } |
| |
| @override |
| Future<Object> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration); |
| |
| @override |
| ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) => imageProvider.loadImage(key, decode); |
| } |
| |
| class _FailingImageProvider extends ImageProvider<int> { |
| const _FailingImageProvider({ |
| this.failOnObtainKey = false, |
| this.failOnLoad = false, |
| required this.throws, |
| required this.image, |
| }) : assert(failOnLoad == true || failOnObtainKey == true); |
| |
| final bool failOnObtainKey; |
| final bool failOnLoad; |
| final Object throws; |
| final ui.Image image; |
| |
| @override |
| Future<int> obtainKey(ImageConfiguration configuration) { |
| if (failOnObtainKey) { |
| throw throws; |
| } |
| return SynchronousFuture<int>(hashCode); |
| } |
| |
| @override |
| ImageStreamCompleter load(int key, DecoderCallback decode) { |
| if (failOnLoad) { |
| throw throws; |
| } |
| return OneFrameImageStreamCompleter( |
| Future<ImageInfo>.value( |
| ImageInfo( |
| image: image, |
| scale: 0, |
| ), |
| ), |
| ); |
| } |
| } |