blob: 290a959f53dfdf5a853edf513399b69131b01aa5 [file] [log] [blame]
// 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]';
}