| // Copyright 2016 The Chromium 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:typed_data'; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../painting/image_data.dart'; |
| import 'semantics_tester.dart'; |
| |
| // This must be run with [WidgetTester.runAsync] since it performs real async |
| // work. |
| Future<ui.Image> createTestImage([List<int> bytes = kTransparentImage]) async { |
| final ui.Codec codec = await ui.instantiateImageCodec(Uint8List.fromList(bytes)); |
| final ui.FrameInfo frameInfo = await codec.getNextFrame(); |
| return frameInfo.image; |
| } |
| |
| void main() { |
| 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(); |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext.findRenderObject(); |
| 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(); |
| 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(); |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext.findRenderObject(); |
| 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(); |
| 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(); |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext.findRenderObject(); |
| 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(); |
| 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(); |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext.findRenderObject(); |
| 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(); |
| 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 TestImageProvider imageProvider = TestImageProvider(); |
| |
| // 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, |
| padding: EdgeInsets.zero, |
| ), |
| child: MediaQuery( |
| key: mediaQueryKey2, |
| data: const MediaQueryData( |
| devicePixelRatio: 5.0, |
| padding: EdgeInsets.zero, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: imageProvider, |
| ), |
| ), |
| ) |
| ); |
| |
| 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, |
| padding: EdgeInsets.zero, |
| ), |
| child: MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| padding: EdgeInsets.zero, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: imageProvider, |
| ), |
| ), |
| ) |
| ); |
| |
| 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 TestImageProvider imageProvider = TestImageProvider(); |
| |
| // 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, |
| padding: EdgeInsets.zero, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: imageProvider, |
| ), |
| ), |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| padding: EdgeInsets.zero, |
| ), |
| 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, |
| padding: EdgeInsets.zero, |
| ), |
| child: Container(width: 100.0), |
| ), |
| MediaQuery( |
| key: mediaQueryKey1, |
| data: const MediaQueryData( |
| devicePixelRatio: 10.0, |
| padding: EdgeInsets.zero, |
| ), |
| child: Image( |
| excludeFromSemantics: true, |
| key: imageKey, |
| image: imageProvider, |
| ), |
| ), |
| ], |
| ) |
| ); |
| |
| expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 10.0); |
| }); |
| |
| testWidgets('Verify Image stops listening to ImageStream', (WidgetTester tester) async { |
| 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(); |
| await tester.pump(); |
| expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, [100×100] @ 1.0x, 1 listener), pixels: [100×100] @ 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, [100×100] @ 1.0x, 0 listeners), pixels: [100×100] @ 1.0x, 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; |
| final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| }; |
| final ImageListener 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)); |
| 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; |
| final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| }; |
| final ImageListener 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(); |
| 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; |
| final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| }; |
| final ImageListener 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)); |
| 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; |
| final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) { |
| capturedException = exception; |
| capturedStackTrace = stackTrace; |
| errorListenerCalled++; |
| }; |
| final ImageListener 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)); |
| 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; |
| final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) { |
| errorListenerCalled = true; |
| reportedException = exception; |
| reportedStackTrace = stackTrace; |
| }; |
| final ImageListener 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)); |
| 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; |
| final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) { |
| errorListenerCalled++; |
| }; |
| final ImageListener 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)); |
| 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('Precache', (WidgetTester tester) async { |
| final TestImageProvider provider = TestImageProvider(); |
| Future<void> precache; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| precache = precacheImage(provider, context); |
| return Container(); |
| } |
| ) |
| ); |
| provider.complete(); |
| await precache; |
| expect(provider._lastResolvedConfiguration, isNotNull); |
| |
| // Check that a second resolve of the same image is synchronous. |
| final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration); |
| bool isSync; |
| stream.addListener(ImageStreamListener((ImageInfo image, bool sync) { isSync = sync; })); |
| expect(isSync, isTrue); |
| }); |
| |
| testWidgets('Precache remove listeners 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(); |
| } |
| ) |
| ); |
| |
| expect(imageStreamCompleter.listeners.length, 2); |
| imageStreamCompleter.listeners.toList()[1].onImage(null, null); |
| |
| expect(imageStreamCompleter.listeners.length, 1); |
| imageStreamCompleter.listeners.toList()[0].onImage(null, null); |
| |
| expect(imageStreamCompleter.listeners.length, 0); |
| }); |
| |
| testWidgets('Precache completes with onError on error', (WidgetTester tester) async { |
| dynamic capturedException; |
| StackTrace capturedStackTrace; |
| final ImageErrorListener 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(); |
| 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(); |
| |
| await tester.pumpWidget( |
| Container( |
| key: key, |
| child: Image( |
| excludeFromSemantics: true, |
| image: imageProvider1, |
| ), |
| ), |
| null, |
| EnginePhase.layout, |
| ); |
| RenderImage renderImage = key.currentContext.findRenderObject(); |
| expect(renderImage.image, isNull); |
| |
| imageProvider1.complete(); |
| imageProvider2.complete(); |
| await tester.idle(); // resolve the future from the image provider |
| await tester.pump(null, EnginePhase.layout); |
| |
| renderImage = key.currentContext.findRenderObject(); |
| 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(); |
| 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(), width: 10.0, excludeFromSemantics: true); |
| final Image image2 = Image(image: TestImageProvider()..complete(), 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 ui.Image image = await tester.runAsync(createTestImage); |
| final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); |
| final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); |
| int lastFrame; |
| 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: image)); |
| await tester.pump(); |
| expect(lastFrame, 0); |
| expect(lastFrameWasSync, isFalse); |
| }); |
| |
| testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=true', (WidgetTester tester) async { |
| final ui.Image image = await tester.runAsync(createTestImage); |
| final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(ImageInfo(image: image)); |
| final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); |
| int lastFrame; |
| 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: image)); |
| 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 ui.Image image = await tester.runAsync(createTestImage); |
| 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: image)); |
| 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 ui.Image image = await tester.runAsync(createTestImage); |
| 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: image)); |
| 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, isInstanceOf<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, isInstanceOf<Padding>()); |
| expect(tester.widget<Padding>(find.byType(Padding)).child, isInstanceOf<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); |
| }); |
| } |
| |
| class TestImageProvider extends ImageProvider<TestImageProvider> { |
| TestImageProvider({ImageStreamCompleter streamCompleter}) { |
| _streamCompleter = streamCompleter |
| ?? OneFrameImageStreamCompleter(_completer.future); |
| } |
| |
| final Completer<ImageInfo> _completer = Completer<ImageInfo>(); |
| ImageStreamCompleter _streamCompleter; |
| ImageConfiguration _lastResolvedConfiguration; |
| |
| @override |
| Future<TestImageProvider> obtainKey(ImageConfiguration configuration) { |
| return SynchronousFuture<TestImageProvider>(this); |
| } |
| |
| @override |
| ImageStream resolve(ImageConfiguration configuration) { |
| _lastResolvedConfiguration = configuration; |
| return super.resolve(configuration); |
| } |
| |
| @override |
| ImageStreamCompleter load(TestImageProvider key) => _streamCompleter; |
| |
| void complete() { |
| _completer.complete(ImageInfo(image: TestImage())); |
| } |
| |
| void fail(dynamic 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, true); |
| } |
| } |
| |
| @override |
| void removeListener(ImageStreamListener listener) { |
| listeners.remove(listener); |
| } |
| |
| void setData({ |
| ImageInfo imageInfo, |
| ImageChunkEvent chunkEvent, |
| }) { |
| if (imageInfo != null) { |
| _currentImage = imageInfo; |
| } |
| final List<ImageStreamListener> localListeners = listeners.toList(); |
| for (ImageStreamListener listener in localListeners) { |
| if (imageInfo != null) { |
| listener.onImage(imageInfo, false); |
| } |
| if (chunkEvent != null && listener.onChunk != null) { |
| listener.onChunk(chunkEvent); |
| } |
| } |
| } |
| } |
| |
| class TestImage implements ui.Image { |
| @override |
| int get width => 100; |
| |
| @override |
| int get height => 100; |
| |
| @override |
| void dispose() { } |
| |
| @override |
| Future<ByteData> toByteData({ ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba }) async { |
| throw UnsupportedError('Cannot encode test image'); |
| } |
| |
| @override |
| String toString() => '[$width\u00D7$height]'; |
| } |