Unverified Commit 66659229 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Show an X when images can't load. (#74972)

parent 69882d96
......@@ -10,6 +10,15 @@
/// Since this is a const value, it can be used to indicate to the compiler that
/// a particular block of code will not be executed in release mode, and hence
/// can be removed.
///
/// Generally it is better to use [kDebugMode] or `assert` to gate code, since
/// using [kReleaseMode] will introduce differences between release and profile
/// builds, which makes performance testing less representative.
///
/// See also:
///
/// * [kDebugMode], which is true in debug builds.
/// * [kProfileMode], which is true in profile builds.
const bool kReleaseMode = bool.fromEnvironment('dart.vm.product', defaultValue: false);
/// A constant that is true if the application was compiled in profile mode.
......@@ -20,6 +29,11 @@ const bool kReleaseMode = bool.fromEnvironment('dart.vm.product', defaultValue:
/// Since this is a const value, it can be used to indicate to the compiler that
/// a particular block of code will not be executed in profile mode, an hence
/// can be removed.
///
/// See also:
///
/// * [kDebugMode], which is true in debug builds.
/// * [kReleaseMode], which is true in release builds.
const bool kProfileMode = bool.fromEnvironment('dart.vm.profile', defaultValue: false);
/// A constant that is true if the application was compiled in debug mode.
......@@ -30,6 +44,20 @@ const bool kProfileMode = bool.fromEnvironment('dart.vm.profile', defaultValue:
/// Since this is a const value, it can be used to indicate to the compiler that
/// a particular block of code will not be executed in debug mode, and hence
/// can be removed.
///
/// An alternative strategy is to use asserts, as in:
///
/// ```dart
/// assert(() {
/// // ...debug-only code here...
/// return true;
/// }());
/// ```
///
/// See also:
///
/// * [kReleaseMode], which is true in release builds.
/// * [kProfileMode], which is true in profile builds.
const bool kDebugMode = !kReleaseMode && !kProfileMode;
/// The epsilon of tolerable double precision error.
......
......@@ -194,6 +194,15 @@ class ImageStreamListener {
///
/// If an error occurs during loading, [onError] will be called instead of
/// [onImage].
///
/// If [onError] is called and does not throw, then the error is considered to
/// be handled. An error handler can explicitly rethrow the exception reported
/// to it to safely indicate that it did not handle the exception.
///
/// If an image stream has no listeners that handled the error when the error
/// was first encountered, then the error is reported using
/// [FlutterError.reportError], with the [FlutterErrorDetails.silent] flag set
/// to true.
final ImageErrorListener? onError;
@override
......@@ -504,15 +513,17 @@ abstract class ImageStreamCompleter with Diagnosticable {
if (_currentError != null && listener.onError != null) {
try {
listener.onError!(_currentError!.exception, _currentError!.stack);
} catch (exception, stack) {
FlutterError.reportError(
FlutterErrorDetails(
exception: exception,
library: 'image resource service',
context: ErrorDescription('by a synchronously-called image error listener'),
stack: stack,
),
);
} catch (newException, newStack) {
if (newException != _currentError!.exception) {
FlutterError.reportError(
FlutterErrorDetails(
exception: newException,
library: 'image resource service',
context: ErrorDescription('by a synchronously-called image error listener'),
stack: newStack,
),
);
}
}
}
}
......@@ -630,7 +641,9 @@ abstract class ImageStreamCompleter with Diagnosticable {
/// occurred while resolving the image.
///
/// If no error listeners (listeners with an [ImageStreamListener.onError]
/// specified) are attached, a [FlutterError] will be reported instead.
/// specified) are attached, or if the handlers all rethrow the exception
/// verbatim (with `throw exception`), a [FlutterError] will be reported using
/// [FlutterError.reportError].
///
/// The `context` should be a string describing where the error was caught, in
/// a form that will make sense in English when following the word "thrown",
......@@ -677,24 +690,27 @@ abstract class ImageStreamCompleter with Diagnosticable {
.whereType<ImageErrorListener>()
.toList();
if (localErrorListeners.isEmpty) {
FlutterError.reportError(_currentError!);
} else {
for (final ImageErrorListener errorListener in localErrorListeners) {
try {
errorListener(exception, stack);
} catch (exception, stack) {
bool handled = false;
for (final ImageErrorListener errorListener in localErrorListeners) {
try {
errorListener(exception, stack);
handled = true;
} catch (newException, newStack) {
if (newException != exception) {
FlutterError.reportError(
FlutterErrorDetails(
context: ErrorDescription('when reporting an error to an image listener'),
library: 'image resource service',
exception: exception,
stack: stack,
exception: newException,
stack: newStack,
),
);
}
}
}
if (!handled) {
FlutterError.reportError(_currentError!);
}
}
/// Calls all the registered [ImageChunkListener]s (listeners with an
......
......@@ -17,7 +17,9 @@ import 'disposable_build_context.dart';
import 'framework.dart';
import 'localizations.dart';
import 'media_query.dart';
import 'placeholder.dart';
import 'scroll_aware_image_provider.dart';
import 'text.dart';
import 'ticker_provider.dart';
export 'package:flutter/painting.dart' show
......@@ -471,7 +473,6 @@ class Image extends StatefulWidget {
assert(isAntiAlias != null),
super(key: key);
// TODO(ianh): Implement the following (see ../services/image_resolution.dart):
//
// * If [width] and [height] are both specified, and [scale] is not, then
......@@ -1176,12 +1177,17 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
_imageStreamListener = ImageStreamListener(
_handleImageFrame,
onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
onError: widget.errorBuilder != null
? (dynamic error, StackTrace? stackTrace) {
onError: widget.errorBuilder != null || kDebugMode
? (Object error, StackTrace? stackTrace) {
setState(() {
_lastException = error;
_lastStack = stackTrace;
});
assert(() {
if (widget.errorBuilder == null)
throw error; // Ensures the error message is printed to the console.
return true;
}());
}
: null,
);
......@@ -1268,11 +1274,41 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
_isListeningToStream = false;
}
Widget _debugBuildErrorWidget(BuildContext context, Object error) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
const Positioned.fill(
child: Placeholder(
color: Color(0xCF8D021F),
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: FittedBox(
child: Text(
'$error',
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
style: const TextStyle(
shadows: <Shadow>[
Shadow(blurRadius: 1.0),
],
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
if (_lastException != null) {
assert(widget.errorBuilder != null);
return widget.errorBuilder!(context, _lastException!, _lastStack);
if (_lastException != null) {
if (widget.errorBuilder != null)
return widget.errorBuilder!(context, _lastException!, _lastStack);
if (kDebugMode)
return _debugBuildErrorWidget(context, _lastException!);
}
Widget result = RawImage(
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart: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';
void main() {
testWidgets('Image at default filterQuality', (WidgetTester tester) async {
await testImageQuality(tester, null);
});
testWidgets('Image at high filterQuality', (WidgetTester tester) async {
await testImageQuality(tester, ui.FilterQuality.high);
});
testWidgets('Image at none filterQuality', (WidgetTester tester) async {
await testImageQuality(tester, ui.FilterQuality.none);
});
}
Future<void> testImageQuality(WidgetTester tester, ui.FilterQuality? quality) async {
await tester.binding.setSurfaceSize(const ui.Size(3, 3));
// A 3x3 image encoded as PNG with white background and black pixels on the diagonal:
// ┌──────┐
// │▓▓ │
// │ ▓▓ │
// │ ▓▓│
// └──────┘
// At different levels of quality these pixels are blurred differently.
final Uint8List test3x3Image = Uint8List.fromList(<int>[
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03,
0x08, 0x02, 0x00, 0x00, 0x00, 0xd9, 0x4a, 0x22, 0xe8, 0x00, 0x00, 0x00,
0x1b, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x64, 0x60, 0x60, 0xf8,
0xff, 0xff, 0x3f, 0x03, 0x9c, 0xfa, 0xff, 0xff, 0x3f, 0xc3, 0xff, 0xff,
0xff, 0x21, 0x1c, 0x00, 0xcb, 0x70, 0x0e, 0xf3, 0x5d, 0x11, 0xc2, 0xf8,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]);
final ui.Image image = (await tester.runAsync(() async {
final ui.Codec codec = await ui.instantiateImageCodec(test3x3Image);
return (await codec.getNextFrame()).image;
}))!;
expect(image.width, 3);
expect(image.height, 3);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
streamCompleter.setData(imageInfo: ImageInfo(image: image));
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
quality == null
? Image(image: imageProvider)
: Image(
image: imageProvider,
filterQuality: quality,
),
);
await expectLater(
find.byType(Image),
matchesGoldenFile('image_quality_${quality ?? 'default'}.png'),
);
}
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) {
if (listener.onError != null) {
listener.onError!(exception, stackTrace);
}
}
}
}
class _TestImageProvider extends ImageProvider<Object> {
_TestImageProvider({ImageStreamCompleter? streamCompleter}) {
_streamCompleter = streamCompleter
?? OneFrameImageStreamCompleter(_completer.future);
}
final Completer<ImageInfo> _completer = Completer<ImageInfo>();
late ImageStreamCompleter _streamCompleter;
bool get loadCalled => _loadCallCount > 0;
int get loadCallCount => _loadCallCount;
int _loadCallCount = 0;
@override
Future<Object> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<_TestImageProvider>(this);
}
@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)}()';
}
......@@ -33,7 +33,7 @@ void main() {
testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider();
final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget(
Container(
key: key,
......@@ -55,7 +55,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider();
final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget(
Container(
key: key,
......@@ -74,7 +74,7 @@ void main() {
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();
final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget(
Container(
key: key,
......@@ -97,7 +97,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider();
final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget(
Container(
key: key,
......@@ -117,7 +117,7 @@ void main() {
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();
final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget(
Image(
key: key,
......@@ -137,7 +137,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider();
final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget(
Image(
key: key,
......@@ -154,7 +154,7 @@ void main() {
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();
final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget(
Image(
key: key,
......@@ -175,7 +175,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider();
final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget(
Image(
key: key,
......@@ -195,9 +195,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final ConfigurationKeyedTestImageProvider imageProvider = ConfigurationKeyedTestImageProvider();
final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider();
final Set<Object> seenKeys = <Object>{};
final DebouncingImageProvider debouncingProvider = DebouncingImageProvider(imageProvider, seenKeys);
final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
// Of the two nested MediaQuery objects, the innermost one,
// mediaQuery2, should define the configuration of the imageProvider.
......@@ -257,9 +257,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final ConfigurationKeyedTestImageProvider imageProvider = ConfigurationKeyedTestImageProvider();
final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider();
final Set<Object> seenKeys = <Object>{};
final DebouncingImageProvider debouncingProvider = DebouncingImageProvider(imageProvider, seenKeys);
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.
......@@ -328,9 +328,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final TestImageProvider imageProvider = TestImageProvider();
final _TestImageProvider imageProvider = _TestImageProvider();
final Set<Object> seenKeys = <Object>{};
final DebouncingImageProvider debouncingProvider = DebouncingImageProvider(imageProvider, seenKeys);
final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
// Of the two nested MediaQuery objects, the innermost one,
// mediaQuery2, should define the configuration of the imageProvider.
......@@ -390,9 +390,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final TestImageProvider imageProvider = TestImageProvider();
final _TestImageProvider imageProvider = _TestImageProvider();
final Set<Object> seenKeys = <Object>{};
final DebouncingImageProvider debouncingProvider = DebouncingImageProvider(imageProvider, seenKeys);
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.
......@@ -462,7 +462,7 @@ void main() {
// Web does not override the toString, whereas VM does
final String imageString = image100x100.toString();
final TestImageProvider imageProvider = TestImageProvider();
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)'));
......@@ -487,7 +487,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
final _TestImageProvider imageProvider = _TestImageProvider();
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
late ImageConfiguration configuration;
await tester.pumpWidget(
......@@ -533,7 +533,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
final _TestImageProvider imageProvider = _TestImageProvider();
late ImageConfiguration configuration;
await tester.pumpWidget(
Builder(
......@@ -578,7 +578,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
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));
......@@ -622,7 +622,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
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));
......@@ -667,7 +667,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
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.
......@@ -707,7 +707,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
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));
......@@ -743,7 +743,7 @@ void main() {
await tester.pumpWidget(
Image(
excludeFromSemantics: true,
image: TestImageProvider(),
image: _TestImageProvider(),
color: const Color(0xFF00FF00),
colorBlendMode: BlendMode.clear,
),
......@@ -754,7 +754,7 @@ void main() {
});
testWidgets('Precache', (WidgetTester tester) async {
final TestImageProvider provider = TestImageProvider();
final _TestImageProvider provider = _TestImageProvider();
late Future<void> precache;
await tester.pumpWidget(
Builder(
......@@ -776,8 +776,8 @@ void main() {
});
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);
final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final _TestImageProvider provider = _TestImageProvider(streamCompleter: imageStreamCompleter);
await tester.pumpWidget(
Builder(
......@@ -812,7 +812,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
final _TestImageProvider imageProvider = _TestImageProvider();
late Future<void> precache;
await tester.pumpWidget(
Builder(
......@@ -833,10 +833,10 @@ void main() {
});
testWidgets('TickerMode controls stream registration', (WidgetTester tester) async {
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final Image image = Image(
excludeFromSemantics: true,
image: TestImageProvider(streamCompleter: imageStreamCompleter),
image: _TestImageProvider(streamCompleter: imageStreamCompleter),
);
await tester.pumpWidget(
TickerMode(
......@@ -857,8 +857,8 @@ void main() {
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 _TestImageProvider imageProvider1 = _TestImageProvider();
final _TestImageProvider imageProvider2 = _TestImageProvider();
final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!;
await tester.pumpWidget(
......@@ -903,8 +903,8 @@ void main() {
});
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 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);
......@@ -928,7 +928,7 @@ void main() {
child: Row(
children: <Widget>[
Image(
image: TestImageProvider(),
image: _TestImageProvider(),
width: 100.0,
height: 100.0,
semanticLabel: 'test',
......@@ -958,7 +958,7 @@ void main() {
Directionality(
textDirection: TextDirection.ltr,
child: Image(
image: TestImageProvider(),
image: _TestImageProvider(),
width: 100.0,
height: 100.0,
excludeFromSemantics: true,
......@@ -982,8 +982,8 @@ void main() {
return frameInfo.image;
}
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame;
await tester.pumpWidget(
......@@ -1012,8 +1012,8 @@ void main() {
});
testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=false', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame;
late bool lastFrameWasSync;
......@@ -1038,8 +1038,8 @@ void main() {
});
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);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(ImageInfo(image: image10x10.clone()));
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame;
late bool lastFrameWasSync;
......@@ -1064,8 +1064,8 @@ void main() {
});
testWidgets('Image state handles frameBuilder update', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
Image(
......@@ -1105,8 +1105,8 @@ void main() {
return frameInfo.image;
}
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame;
int buildCount = 0;
......@@ -1169,8 +1169,8 @@ void main() {
});
testWidgets('Image invokes loadingBuilder on chunk event notification', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
final List<ImageChunkEvent?> chunkEvents = <ImageChunkEvent?>[];
await tester.pumpWidget(
......@@ -1211,8 +1211,8 @@ void main() {
});
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);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
Image(
......@@ -1233,8 +1233,8 @@ void main() {
});
testWidgets('Image chains the results of frameBuilder and loadingBuilder', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
Image(
......@@ -1263,8 +1263,8 @@ void main() {
});
testWidgets('Image state handles loadingBuilder update from null to non-null', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
Image(image: imageProvider),
......@@ -1295,8 +1295,8 @@ void main() {
});
testWidgets('Image state handles loadingBuilder update from non-null to null', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
Image(
......@@ -1329,8 +1329,8 @@ void main() {
testWidgets('Verify Image resets its ImageListeners', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider1 = TestImageProvider(streamCompleter: imageStreamCompleter);
final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter);
await tester.pumpWidget(
Container(
key: key,
......@@ -1343,7 +1343,7 @@ void main() {
expect(imageStreamCompleter.listeners.length, 2);
final TestImageProvider imageProvider2 = TestImageProvider();
final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget(
Container(
key: key,
......@@ -1362,8 +1362,8 @@ void main() {
testWidgets('Verify Image resets its ErrorListeners', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
final TestImageProvider imageProvider1 = TestImageProvider(streamCompleter: imageStreamCompleter);
final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter);
await tester.pumpWidget(
Container(
key: key,
......@@ -1377,7 +1377,7 @@ void main() {
expect(imageStreamCompleter.listeners.length, 2);
final TestImageProvider imageProvider2 = TestImageProvider();
final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget(
Container(
key: key,
......@@ -1396,7 +1396,7 @@ void main() {
testWidgets('Image defers loading while fast scrolling', (WidgetTester tester) async {
const int gridCells = 1000;
final List<TestImageProvider> imageProviders = <TestImageProvider>[];
final List<_TestImageProvider> imageProviders = <_TestImageProvider>[];
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
......@@ -1405,7 +1405,7 @@ void main() {
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemCount: gridCells,
itemBuilder: (_, int index) {
final TestImageProvider provider = TestImageProvider();
final _TestImageProvider provider = _TestImageProvider();
imageProviders.add(provider);
return SizedBox(
height: 250,
......@@ -1419,8 +1419,8 @@ void main() {
),
));
final bool Function(TestImageProvider) loadCalled = (TestImageProvider provider) => provider.loadCalled;
final bool Function(TestImageProvider) loadNotCalled = (TestImageProvider provider) => !provider.loadCalled;
final bool Function(_TestImageProvider) loadCalled = (_TestImageProvider provider) => provider.loadCalled;
final bool Function(_TestImageProvider) loadNotCalled = (_TestImageProvider provider) => !provider.loadCalled;
expect(find.bySemanticsLabel('5'), findsOneWidget);
expect(imageProviders.length, 12);
......@@ -1445,8 +1445,8 @@ void main() {
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();
final _TestImageProvider provider1 = _TestImageProvider();
final _TestImageProvider provider2 = _TestImageProvider();
expect(provider1.loadCallCount, 0);
expect(provider2.loadCallCount, 0);
......@@ -1499,7 +1499,7 @@ void main() {
testWidgets('precacheImage does not hold weak ref for more than a frame', (WidgetTester tester) async {
imageCache!.maximumSize = 0;
final TestImageProvider provider = TestImageProvider();
final _TestImageProvider provider = _TestImageProvider();
late Future<void> precache;
await tester.pumpWidget(
Builder(
......@@ -1549,7 +1549,7 @@ void main() {
});
testWidgets('precacheImage allows time to take over weak reference', (WidgetTester tester) async {
final TestImageProvider provider = TestImageProvider();
final _TestImageProvider provider = _TestImageProvider();
late Future<void> precache;
await tester.pumpWidget(
Builder(
......@@ -1637,7 +1637,7 @@ void main() {
late Object caughtException;
await tester.pumpWidget(
Image(
image: FailingImageProvider(failOnObtainKey: true, throws: 'threw', image: image10x10),
image: _FailingImageProvider(failOnObtainKey: true, throws: 'threw', image: image10x10),
errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
caughtException = error;
return SizedBox.expand(key: errorKey);
......@@ -1657,7 +1657,7 @@ void main() {
late Object caughtException;
await tester.pumpWidget(
Image(
image: FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
caughtException = error;
return SizedBox.expand(key: errorKey);
......@@ -1675,7 +1675,7 @@ void main() {
testWidgets('no errorBuilder - failure reported to FlutterError', (WidgetTester tester) async {
await tester.pumpWidget(
Image(
image: FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
),
);
......@@ -1730,14 +1730,14 @@ void main() {
};
final ui.Image image = (await tester.runAsync(() => createTestImage(width: 100, height: 100)))!;
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(
ImageInfo(
image: image,
scale: 1.0,
debugLabel: 'test.png',
),
);
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
Center(
......@@ -1767,13 +1767,13 @@ void main() {
expect(image.debugGetOpenHandleStackTraces()!.length, 1);
final ImageProvider provider = TestImageProvider(
final ImageProvider provider = _TestImageProvider(
streamCompleter: OneFrameImageStreamCompleter(
Future<ImageInfo>.value(
ImageInfo(
image: image,
scale: 1.0,
debugLabel: 'TestImage',
debugLabel: '_TestImage',
),
),
),
......@@ -1806,7 +1806,7 @@ void main() {
testWidgets('Keeps stream alive when ticker mode is disabled', (WidgetTester tester) async {
imageCache!.maximumSize = 0;
final ui.Image image = (await tester.runAsync(() => createTestImage(width: 1, height: 1, cache: false)))!;
final TestImageProvider provider = TestImageProvider();
final _TestImageProvider provider = _TestImageProvider();
provider.complete(image);
await tester.pumpWidget(
......@@ -1835,8 +1835,8 @@ void main() {
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);
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
Center(
......@@ -1878,81 +1878,40 @@ void main() {
expect(find.byKey(errorKey), findsNothing);
});
testWidgets('Image at default filterQuality', (WidgetTester tester) async {
await testImageQuality(tester, null);
});
testWidgets('Image at high filterQuality', (WidgetTester tester) async {
await testImageQuality(tester, ui.FilterQuality.high);
});
testWidgets('Image at none filterQuality', (WidgetTester tester) async {
await testImageQuality(tester, ui.FilterQuality.none);
});
}
Future<void> testImageQuality(WidgetTester tester, ui.FilterQuality? quality) async {
await tester.binding.setSurfaceSize(const ui.Size(3, 3));
// A 3x3 image encoded as PNG with white background and black pixels on the diagonal:
// ┌──────┐
// │▓▓ │
// │ ▓▓ │
// │ ▓▓│
// └──────┘
// At different levels of quality these pixels are blurred differently.
final Uint8List test3x3Image = Uint8List.fromList(<int>[
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03,
0x08, 0x02, 0x00, 0x00, 0x00, 0xd9, 0x4a, 0x22, 0xe8, 0x00, 0x00, 0x00,
0x1b, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x64, 0x60, 0x60, 0xf8,
0xff, 0xff, 0x3f, 0x03, 0x9c, 0xfa, 0xff, 0xff, 0x3f, 0xc3, 0xff, 0xff,
0xff, 0x21, 0x1c, 0x00, 0xcb, 0x70, 0x0e, 0xf3, 0x5d, 0x11, 0xc2, 0xf8,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]);
final ui.Image image = (await tester.runAsync(() async {
final ui.Codec codec = await ui.instantiateImageCodec(test3x3Image);
return (await codec.getNextFrame()).image;
}))!;
expect(image.width, 3);
expect(image.height, 3);
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter();
streamCompleter.setData(imageInfo: ImageInfo(image: image));
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget(
quality == null
? Image(image: imageProvider)
: Image(
image: imageProvider,
filterQuality: quality,
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.byType(Image),
matchesGoldenFile('image_quality_${quality ?? 'default'}.png'),
);
}
class ImagePainter extends CustomPainter {
ImagePainter(this.image);
@override
void paint(ui.Canvas canvas, ui.Size size) {
canvas.drawImage(image, Offset.zero, Paint());
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
final ui.Image image;
),
));
await expectLater(
find.byKey(key),
matchesGoldenFile('image_test.missing.1.png'),
);
expect(tester.takeException().toString(), startsWith('Unable to load asset: '));
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)
}
@immutable
class ConfigurationAwareKey {
const ConfigurationAwareKey(this.provider, this.configuration)
class _ConfigurationAwareKey {
const _ConfigurationAwareKey(this.provider, this.configuration)
: assert(provider != null),
assert(configuration != null);
......@@ -1964,7 +1923,7 @@ class ConfigurationAwareKey {
if (other.runtimeType != runtimeType) {
return false;
}
return other is ConfigurationAwareKey
return other is _ConfigurationAwareKey
&& other.provider == provider
&& other.configuration == configuration;
}
......@@ -1973,15 +1932,15 @@ class ConfigurationAwareKey {
int get hashCode => hashValues(provider, configuration);
}
class ConfigurationKeyedTestImageProvider extends TestImageProvider {
class _ConfigurationKeyedTestImageProvider extends _TestImageProvider {
@override
Future<ConfigurationAwareKey> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<ConfigurationAwareKey>(ConfigurationAwareKey(this, configuration));
Future<_ConfigurationAwareKey> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<_ConfigurationAwareKey>(_ConfigurationAwareKey(this, configuration));
}
}
class TestImageProvider extends ImageProvider<Object> {
TestImageProvider({ImageStreamCompleter? streamCompleter}) {
class _TestImageProvider extends ImageProvider<Object> {
_TestImageProvider({ImageStreamCompleter? streamCompleter}) {
_streamCompleter = streamCompleter
?? OneFrameImageStreamCompleter(_completer.future);
}
......@@ -1996,7 +1955,7 @@ class TestImageProvider extends ImageProvider<Object> {
@override
Future<Object> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<TestImageProvider>(this);
return SynchronousFuture<_TestImageProvider>(this);
}
@override
......@@ -2023,14 +1982,8 @@ class TestImageProvider extends ImageProvider<Object> {
String toString() => '${describeIdentity(this)}()';
}
class SimpleTestImageStreamCompleter extends ImageStreamCompleter {
void testSetImage(ui.Image image) {
setImage(ImageInfo(image: image, scale: 1.0));
}
}
class TestImageStreamCompleter extends ImageStreamCompleter {
TestImageStreamCompleter([this._currentImage]);
class _TestImageStreamCompleter extends ImageStreamCompleter {
_TestImageStreamCompleter([this._currentImage]);
ImageInfo? _currentImage;
final Set<ImageStreamListener> listeners = <ImageStreamListener>{};
......@@ -2080,8 +2033,8 @@ class TestImageStreamCompleter extends ImageStreamCompleter {
}
}
class DebouncingImageProvider extends ImageProvider<Object> {
DebouncingImageProvider(this.imageProvider, this.seenKeys);
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.
///
......@@ -2107,8 +2060,8 @@ class DebouncingImageProvider extends ImageProvider<Object> {
ImageStreamCompleter load(Object key, DecoderCallback decode) => imageProvider.load(key, decode);
}
class FailingImageProvider extends ImageProvider<int> {
const FailingImageProvider({
class _FailingImageProvider extends ImageProvider<int> {
const _FailingImageProvider({
this.failOnObtainKey = false,
this.failOnLoad = false,
required this.throws,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment