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 @@ ...@@ -10,6 +10,15 @@
/// Since this is a const value, it can be used to indicate to the compiler that /// 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 /// a particular block of code will not be executed in release mode, and hence
/// can be removed. /// 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); const bool kReleaseMode = bool.fromEnvironment('dart.vm.product', defaultValue: false);
/// A constant that is true if the application was compiled in profile mode. /// 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: ...@@ -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 /// 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 /// a particular block of code will not be executed in profile mode, an hence
/// can be removed. /// 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); const bool kProfileMode = bool.fromEnvironment('dart.vm.profile', defaultValue: false);
/// A constant that is true if the application was compiled in debug mode. /// 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: ...@@ -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 /// 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 /// a particular block of code will not be executed in debug mode, and hence
/// can be removed. /// 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; const bool kDebugMode = !kReleaseMode && !kProfileMode;
/// The epsilon of tolerable double precision error. /// The epsilon of tolerable double precision error.
......
...@@ -194,6 +194,15 @@ class ImageStreamListener { ...@@ -194,6 +194,15 @@ class ImageStreamListener {
/// ///
/// If an error occurs during loading, [onError] will be called instead of /// If an error occurs during loading, [onError] will be called instead of
/// [onImage]. /// [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; final ImageErrorListener? onError;
@override @override
...@@ -504,15 +513,17 @@ abstract class ImageStreamCompleter with Diagnosticable { ...@@ -504,15 +513,17 @@ abstract class ImageStreamCompleter with Diagnosticable {
if (_currentError != null && listener.onError != null) { if (_currentError != null && listener.onError != null) {
try { try {
listener.onError!(_currentError!.exception, _currentError!.stack); listener.onError!(_currentError!.exception, _currentError!.stack);
} catch (exception, stack) { } catch (newException, newStack) {
FlutterError.reportError( if (newException != _currentError!.exception) {
FlutterErrorDetails( FlutterError.reportError(
exception: exception, FlutterErrorDetails(
library: 'image resource service', exception: newException,
context: ErrorDescription('by a synchronously-called image error listener'), library: 'image resource service',
stack: stack, context: ErrorDescription('by a synchronously-called image error listener'),
), stack: newStack,
); ),
);
}
} }
} }
} }
...@@ -630,7 +641,9 @@ abstract class ImageStreamCompleter with Diagnosticable { ...@@ -630,7 +641,9 @@ abstract class ImageStreamCompleter with Diagnosticable {
/// occurred while resolving the image. /// occurred while resolving the image.
/// ///
/// If no error listeners (listeners with an [ImageStreamListener.onError] /// 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 /// 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", /// a form that will make sense in English when following the word "thrown",
...@@ -677,24 +690,27 @@ abstract class ImageStreamCompleter with Diagnosticable { ...@@ -677,24 +690,27 @@ abstract class ImageStreamCompleter with Diagnosticable {
.whereType<ImageErrorListener>() .whereType<ImageErrorListener>()
.toList(); .toList();
if (localErrorListeners.isEmpty) { bool handled = false;
FlutterError.reportError(_currentError!); for (final ImageErrorListener errorListener in localErrorListeners) {
} else { try {
for (final ImageErrorListener errorListener in localErrorListeners) { errorListener(exception, stack);
try { handled = true;
errorListener(exception, stack); } catch (newException, newStack) {
} catch (exception, stack) { if (newException != exception) {
FlutterError.reportError( FlutterError.reportError(
FlutterErrorDetails( FlutterErrorDetails(
context: ErrorDescription('when reporting an error to an image listener'), context: ErrorDescription('when reporting an error to an image listener'),
library: 'image resource service', library: 'image resource service',
exception: exception, exception: newException,
stack: stack, stack: newStack,
), ),
); );
} }
} }
} }
if (!handled) {
FlutterError.reportError(_currentError!);
}
} }
/// Calls all the registered [ImageChunkListener]s (listeners with an /// Calls all the registered [ImageChunkListener]s (listeners with an
......
...@@ -17,7 +17,9 @@ import 'disposable_build_context.dart'; ...@@ -17,7 +17,9 @@ import 'disposable_build_context.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'media_query.dart'; import 'media_query.dart';
import 'placeholder.dart';
import 'scroll_aware_image_provider.dart'; import 'scroll_aware_image_provider.dart';
import 'text.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
export 'package:flutter/painting.dart' show export 'package:flutter/painting.dart' show
...@@ -471,7 +473,6 @@ class Image extends StatefulWidget { ...@@ -471,7 +473,6 @@ class Image extends StatefulWidget {
assert(isAntiAlias != null), assert(isAntiAlias != null),
super(key: key); super(key: key);
// TODO(ianh): Implement the following (see ../services/image_resolution.dart): // TODO(ianh): Implement the following (see ../services/image_resolution.dart):
// //
// * If [width] and [height] are both specified, and [scale] is not, then // * If [width] and [height] are both specified, and [scale] is not, then
...@@ -1176,12 +1177,17 @@ class _ImageState extends State<Image> with WidgetsBindingObserver { ...@@ -1176,12 +1177,17 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
_imageStreamListener = ImageStreamListener( _imageStreamListener = ImageStreamListener(
_handleImageFrame, _handleImageFrame,
onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
onError: widget.errorBuilder != null onError: widget.errorBuilder != null || kDebugMode
? (dynamic error, StackTrace? stackTrace) { ? (Object error, StackTrace? stackTrace) {
setState(() { setState(() {
_lastException = error; _lastException = error;
_lastStack = stackTrace; _lastStack = stackTrace;
}); });
assert(() {
if (widget.errorBuilder == null)
throw error; // Ensures the error message is printed to the console.
return true;
}());
} }
: null, : null,
); );
...@@ -1268,11 +1274,41 @@ class _ImageState extends State<Image> with WidgetsBindingObserver { ...@@ -1268,11 +1274,41 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
_isListeningToStream = false; _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_lastException != null) { if (_lastException != null) {
assert(widget.errorBuilder != null); if (widget.errorBuilder != null)
return widget.errorBuilder!(context, _lastException!, _lastStack); return widget.errorBuilder!(context, _lastException!, _lastStack);
if (kDebugMode)
return _debugBuildErrorWidget(context, _lastException!);
} }
Widget result = RawImage( 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() { ...@@ -33,7 +33,7 @@ void main() {
testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async { testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider(); final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -55,7 +55,7 @@ void main() { ...@@ -55,7 +55,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage; renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull); expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider(); final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -74,7 +74,7 @@ void main() { ...@@ -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 { testWidgets("Verify Image doesn't reset its RenderImage when changing providers if it has gaplessPlayback set", (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider(); final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -97,7 +97,7 @@ void main() { ...@@ -97,7 +97,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage; renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull); expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider(); final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -117,7 +117,7 @@ void main() { ...@@ -117,7 +117,7 @@ void main() {
testWidgets('Verify Image resets its RenderImage when changing providers if it has a key', (WidgetTester tester) async { testWidgets('Verify Image resets its RenderImage when changing providers if it has a key', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider(); final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
key: key, key: key,
...@@ -137,7 +137,7 @@ void main() { ...@@ -137,7 +137,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage; renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull); expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider(); final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
key: key, key: key,
...@@ -154,7 +154,7 @@ void main() { ...@@ -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 { testWidgets("Verify Image doesn't reset its RenderImage when changing providers if it has gaplessPlayback set", (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider(); final _TestImageProvider imageProvider1 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
key: key, key: key,
...@@ -175,7 +175,7 @@ void main() { ...@@ -175,7 +175,7 @@ void main() {
renderImage = key.currentContext!.findRenderObject()! as RenderImage; renderImage = key.currentContext!.findRenderObject()! as RenderImage;
expect(renderImage.image, isNotNull); expect(renderImage.image, isNotNull);
final TestImageProvider imageProvider2 = TestImageProvider(); final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
key: key, key: key,
...@@ -195,9 +195,9 @@ void main() { ...@@ -195,9 +195,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final ConfigurationKeyedTestImageProvider imageProvider = ConfigurationKeyedTestImageProvider(); final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider();
final Set<Object> seenKeys = <Object>{}; 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, // Of the two nested MediaQuery objects, the innermost one,
// mediaQuery2, should define the configuration of the imageProvider. // mediaQuery2, should define the configuration of the imageProvider.
...@@ -257,9 +257,9 @@ void main() { ...@@ -257,9 +257,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final ConfigurationKeyedTestImageProvider imageProvider = ConfigurationKeyedTestImageProvider(); final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider();
final Set<Object> seenKeys = <Object>{}; 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 // This is just a variation on the previous test. In this version the location
// of the Image changes and the MediaQuery widgets do not. // of the Image changes and the MediaQuery widgets do not.
...@@ -328,9 +328,9 @@ void main() { ...@@ -328,9 +328,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
final Set<Object> seenKeys = <Object>{}; 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, // Of the two nested MediaQuery objects, the innermost one,
// mediaQuery2, should define the configuration of the imageProvider. // mediaQuery2, should define the configuration of the imageProvider.
...@@ -390,9 +390,9 @@ void main() { ...@@ -390,9 +390,9 @@ void main() {
final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1'); final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2'); final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
final GlobalKey imageKey = GlobalKey(debugLabel: 'image'); final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
final Set<Object> seenKeys = <Object>{}; 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 // This is just a variation on the previous test. In this version the location
// of the Image changes and the MediaQuery widgets do not. // of the Image changes and the MediaQuery widgets do not.
...@@ -462,7 +462,7 @@ void main() { ...@@ -462,7 +462,7 @@ void main() {
// Web does not override the toString, whereas VM does // Web does not override the toString, whereas VM does
final String imageString = image100x100.toString(); final String imageString = image100x100.toString();
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
await tester.pumpWidget(Image(image: imageProvider, excludeFromSemantics: true)); await tester.pumpWidget(Image(image: imageProvider, excludeFromSemantics: true));
final State<Image> image = tester.state/*State<Image>*/(find.byType(Image)); 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)')); 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() { ...@@ -487,7 +487,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
late ImageConfiguration configuration; late ImageConfiguration configuration;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -533,7 +533,7 @@ void main() { ...@@ -533,7 +533,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
late ImageConfiguration configuration; late ImageConfiguration configuration;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -578,7 +578,7 @@ void main() { ...@@ -578,7 +578,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Add the exact same listener a second time without the errorListener. // Add the exact same listener a second time without the errorListener.
imageProvider._streamCompleter.addListener(ImageStreamListener(listener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener));
...@@ -622,7 +622,7 @@ void main() { ...@@ -622,7 +622,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Add the exact same errorListener a second time. // Add the exact same errorListener a second time.
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
...@@ -667,7 +667,7 @@ void main() { ...@@ -667,7 +667,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Now remove the listener the error listener is attached to. // Now remove the listener the error listener is attached to.
// Don't explicitly remove the error listener. // Don't explicitly remove the error listener.
...@@ -707,7 +707,7 @@ void main() { ...@@ -707,7 +707,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Duplicates the same set of listener and errorListener. // Duplicates the same set of listener and errorListener.
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener)); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
...@@ -743,7 +743,7 @@ void main() { ...@@ -743,7 +743,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
excludeFromSemantics: true, excludeFromSemantics: true,
image: TestImageProvider(), image: _TestImageProvider(),
color: const Color(0xFF00FF00), color: const Color(0xFF00FF00),
colorBlendMode: BlendMode.clear, colorBlendMode: BlendMode.clear,
), ),
...@@ -754,7 +754,7 @@ void main() { ...@@ -754,7 +754,7 @@ void main() {
}); });
testWidgets('Precache', (WidgetTester tester) async { testWidgets('Precache', (WidgetTester tester) async {
final TestImageProvider provider = TestImageProvider(); final _TestImageProvider provider = _TestImageProvider();
late Future<void> precache; late Future<void> precache;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -776,8 +776,8 @@ void main() { ...@@ -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 { testWidgets('Precache removes original listener immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async {
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final TestImageProvider provider = TestImageProvider(streamCompleter: imageStreamCompleter); final _TestImageProvider provider = _TestImageProvider(streamCompleter: imageStreamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -812,7 +812,7 @@ void main() { ...@@ -812,7 +812,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final _TestImageProvider imageProvider = _TestImageProvider();
late Future<void> precache; late Future<void> precache;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -833,10 +833,10 @@ void main() { ...@@ -833,10 +833,10 @@ void main() {
}); });
testWidgets('TickerMode controls stream registration', (WidgetTester tester) async { testWidgets('TickerMode controls stream registration', (WidgetTester tester) async {
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final Image image = Image( final Image image = Image(
excludeFromSemantics: true, excludeFromSemantics: true,
image: TestImageProvider(streamCompleter: imageStreamCompleter), image: _TestImageProvider(streamCompleter: imageStreamCompleter),
); );
await tester.pumpWidget( await tester.pumpWidget(
TickerMode( TickerMode(
...@@ -857,8 +857,8 @@ void main() { ...@@ -857,8 +857,8 @@ void main() {
testWidgets('Verify Image shows correct RenderImage when changing to an already completed provider', (WidgetTester tester) async { testWidgets('Verify Image shows correct RenderImage when changing to an already completed provider', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider(); final _TestImageProvider imageProvider1 = _TestImageProvider();
final TestImageProvider imageProvider2 = TestImageProvider(); final _TestImageProvider imageProvider2 = _TestImageProvider();
final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!; final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -903,8 +903,8 @@ void main() { ...@@ -903,8 +903,8 @@ void main() {
}); });
testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async { 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 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 image2 = Image(image: _TestImageProvider()..complete(image10x10.clone()), width: 20.0, excludeFromSemantics: true);
final Column column = Column(children: <Widget>[image1, image2]); final Column column = Column(children: <Widget>[image1, image2]);
await tester.pumpWidget(column, null, EnginePhase.layout); await tester.pumpWidget(column, null, EnginePhase.layout);
...@@ -928,7 +928,7 @@ void main() { ...@@ -928,7 +928,7 @@ void main() {
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Image( Image(
image: TestImageProvider(), image: _TestImageProvider(),
width: 100.0, width: 100.0,
height: 100.0, height: 100.0,
semanticLabel: 'test', semanticLabel: 'test',
...@@ -958,7 +958,7 @@ void main() { ...@@ -958,7 +958,7 @@ void main() {
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Image( child: Image(
image: TestImageProvider(), image: _TestImageProvider(),
width: 100.0, width: 100.0,
height: 100.0, height: 100.0,
excludeFromSemantics: true, excludeFromSemantics: true,
...@@ -982,8 +982,8 @@ void main() { ...@@ -982,8 +982,8 @@ void main() {
return frameInfo.image; return frameInfo.image;
} }
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame; int? lastFrame;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1012,8 +1012,8 @@ void main() { ...@@ -1012,8 +1012,8 @@ void main() {
}); });
testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=false', (WidgetTester tester) async { testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=false', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame; int? lastFrame;
late bool lastFrameWasSync; late bool lastFrameWasSync;
...@@ -1038,8 +1038,8 @@ void main() { ...@@ -1038,8 +1038,8 @@ void main() {
}); });
testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=true', (WidgetTester tester) async { testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=true', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(ImageInfo(image: image10x10.clone())); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(ImageInfo(image: image10x10.clone()));
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame; int? lastFrame;
late bool lastFrameWasSync; late bool lastFrameWasSync;
...@@ -1064,8 +1064,8 @@ void main() { ...@@ -1064,8 +1064,8 @@ void main() {
}); });
testWidgets('Image state handles frameBuilder update', (WidgetTester tester) async { testWidgets('Image state handles frameBuilder update', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
...@@ -1105,8 +1105,8 @@ void main() { ...@@ -1105,8 +1105,8 @@ void main() {
return frameInfo.image; return frameInfo.image;
} }
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame; int? lastFrame;
int buildCount = 0; int buildCount = 0;
...@@ -1169,8 +1169,8 @@ void main() { ...@@ -1169,8 +1169,8 @@ void main() {
}); });
testWidgets('Image invokes loadingBuilder on chunk event notification', (WidgetTester tester) async { testWidgets('Image invokes loadingBuilder on chunk event notification', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
final List<ImageChunkEvent?> chunkEvents = <ImageChunkEvent?>[]; final List<ImageChunkEvent?> chunkEvents = <ImageChunkEvent?>[];
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1211,8 +1211,8 @@ void main() { ...@@ -1211,8 +1211,8 @@ void main() {
}); });
testWidgets("Image doesn't rebuild on chunk events if loadingBuilder is null", (WidgetTester tester) async { testWidgets("Image doesn't rebuild on chunk events if loadingBuilder is null", (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
...@@ -1233,8 +1233,8 @@ void main() { ...@@ -1233,8 +1233,8 @@ void main() {
}); });
testWidgets('Image chains the results of frameBuilder and loadingBuilder', (WidgetTester tester) async { testWidgets('Image chains the results of frameBuilder and loadingBuilder', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
...@@ -1263,8 +1263,8 @@ void main() { ...@@ -1263,8 +1263,8 @@ void main() {
}); });
testWidgets('Image state handles loadingBuilder update from null to non-null', (WidgetTester tester) async { testWidgets('Image state handles loadingBuilder update from null to non-null', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Image(image: imageProvider), Image(image: imageProvider),
...@@ -1295,8 +1295,8 @@ void main() { ...@@ -1295,8 +1295,8 @@ void main() {
}); });
testWidgets('Image state handles loadingBuilder update from non-null to null', (WidgetTester tester) async { testWidgets('Image state handles loadingBuilder update from non-null to null', (WidgetTester tester) async {
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
...@@ -1329,8 +1329,8 @@ void main() { ...@@ -1329,8 +1329,8 @@ void main() {
testWidgets('Verify Image resets its ImageListeners', (WidgetTester tester) async { testWidgets('Verify Image resets its ImageListeners', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider1 = TestImageProvider(streamCompleter: imageStreamCompleter); final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -1343,7 +1343,7 @@ void main() { ...@@ -1343,7 +1343,7 @@ void main() {
expect(imageStreamCompleter.listeners.length, 2); expect(imageStreamCompleter.listeners.length, 2);
final TestImageProvider imageProvider2 = TestImageProvider(); final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -1362,8 +1362,8 @@ void main() { ...@@ -1362,8 +1362,8 @@ void main() {
testWidgets('Verify Image resets its ErrorListeners', (WidgetTester tester) async { testWidgets('Verify Image resets its ErrorListeners', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider1 = TestImageProvider(streamCompleter: imageStreamCompleter); final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -1377,7 +1377,7 @@ void main() { ...@@ -1377,7 +1377,7 @@ void main() {
expect(imageStreamCompleter.listeners.length, 2); expect(imageStreamCompleter.listeners.length, 2);
final TestImageProvider imageProvider2 = TestImageProvider(); final _TestImageProvider imageProvider2 = _TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
Container( Container(
key: key, key: key,
...@@ -1396,7 +1396,7 @@ void main() { ...@@ -1396,7 +1396,7 @@ void main() {
testWidgets('Image defers loading while fast scrolling', (WidgetTester tester) async { testWidgets('Image defers loading while fast scrolling', (WidgetTester tester) async {
const int gridCells = 1000; const int gridCells = 1000;
final List<TestImageProvider> imageProviders = <TestImageProvider>[]; final List<_TestImageProvider> imageProviders = <_TestImageProvider>[];
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality( await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -1405,7 +1405,7 @@ void main() { ...@@ -1405,7 +1405,7 @@ void main() {
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemCount: gridCells, itemCount: gridCells,
itemBuilder: (_, int index) { itemBuilder: (_, int index) {
final TestImageProvider provider = TestImageProvider(); final _TestImageProvider provider = _TestImageProvider();
imageProviders.add(provider); imageProviders.add(provider);
return SizedBox( return SizedBox(
height: 250, height: 250,
...@@ -1419,8 +1419,8 @@ void main() { ...@@ -1419,8 +1419,8 @@ void main() {
), ),
)); ));
final bool Function(TestImageProvider) loadCalled = (TestImageProvider provider) => provider.loadCalled; final bool Function(_TestImageProvider) loadCalled = (_TestImageProvider provider) => provider.loadCalled;
final bool Function(TestImageProvider) loadNotCalled = (TestImageProvider provider) => !provider.loadCalled; final bool Function(_TestImageProvider) loadNotCalled = (_TestImageProvider provider) => !provider.loadCalled;
expect(find.bySemanticsLabel('5'), findsOneWidget); expect(find.bySemanticsLabel('5'), findsOneWidget);
expect(imageProviders.length, 12); expect(imageProviders.length, 12);
...@@ -1445,8 +1445,8 @@ void main() { ...@@ -1445,8 +1445,8 @@ void main() {
testWidgets('Same image provider in multiple parts of the tree, no cache room left', (WidgetTester tester) async { testWidgets('Same image provider in multiple parts of the tree, no cache room left', (WidgetTester tester) async {
imageCache!.maximumSize = 0; imageCache!.maximumSize = 0;
final TestImageProvider provider1 = TestImageProvider(); final _TestImageProvider provider1 = _TestImageProvider();
final TestImageProvider provider2 = TestImageProvider(); final _TestImageProvider provider2 = _TestImageProvider();
expect(provider1.loadCallCount, 0); expect(provider1.loadCallCount, 0);
expect(provider2.loadCallCount, 0); expect(provider2.loadCallCount, 0);
...@@ -1499,7 +1499,7 @@ void main() { ...@@ -1499,7 +1499,7 @@ void main() {
testWidgets('precacheImage does not hold weak ref for more than a frame', (WidgetTester tester) async { testWidgets('precacheImage does not hold weak ref for more than a frame', (WidgetTester tester) async {
imageCache!.maximumSize = 0; imageCache!.maximumSize = 0;
final TestImageProvider provider = TestImageProvider(); final _TestImageProvider provider = _TestImageProvider();
late Future<void> precache; late Future<void> precache;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -1549,7 +1549,7 @@ void main() { ...@@ -1549,7 +1549,7 @@ void main() {
}); });
testWidgets('precacheImage allows time to take over weak reference', (WidgetTester tester) async { testWidgets('precacheImage allows time to take over weak reference', (WidgetTester tester) async {
final TestImageProvider provider = TestImageProvider(); final _TestImageProvider provider = _TestImageProvider();
late Future<void> precache; late Future<void> precache;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -1637,7 +1637,7 @@ void main() { ...@@ -1637,7 +1637,7 @@ void main() {
late Object caughtException; late Object caughtException;
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
image: FailingImageProvider(failOnObtainKey: true, throws: 'threw', image: image10x10), image: _FailingImageProvider(failOnObtainKey: true, throws: 'threw', image: image10x10),
errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
caughtException = error; caughtException = error;
return SizedBox.expand(key: errorKey); return SizedBox.expand(key: errorKey);
...@@ -1657,7 +1657,7 @@ void main() { ...@@ -1657,7 +1657,7 @@ void main() {
late Object caughtException; late Object caughtException;
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
image: FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10), image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
caughtException = error; caughtException = error;
return SizedBox.expand(key: errorKey); return SizedBox.expand(key: errorKey);
...@@ -1675,7 +1675,7 @@ void main() { ...@@ -1675,7 +1675,7 @@ void main() {
testWidgets('no errorBuilder - failure reported to FlutterError', (WidgetTester tester) async { testWidgets('no errorBuilder - failure reported to FlutterError', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Image( Image(
image: FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10), image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
), ),
); );
...@@ -1730,14 +1730,14 @@ void main() { ...@@ -1730,14 +1730,14 @@ void main() {
}; };
final ui.Image image = (await tester.runAsync(() => createTestImage(width: 100, height: 100)))!; final ui.Image image = (await tester.runAsync(() => createTestImage(width: 100, height: 100)))!;
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter( final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(
ImageInfo( ImageInfo(
image: image, image: image,
scale: 1.0, scale: 1.0,
debugLabel: 'test.png', debugLabel: 'test.png',
), ),
); );
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
...@@ -1767,13 +1767,13 @@ void main() { ...@@ -1767,13 +1767,13 @@ void main() {
expect(image.debugGetOpenHandleStackTraces()!.length, 1); expect(image.debugGetOpenHandleStackTraces()!.length, 1);
final ImageProvider provider = TestImageProvider( final ImageProvider provider = _TestImageProvider(
streamCompleter: OneFrameImageStreamCompleter( streamCompleter: OneFrameImageStreamCompleter(
Future<ImageInfo>.value( Future<ImageInfo>.value(
ImageInfo( ImageInfo(
image: image, image: image,
scale: 1.0, scale: 1.0,
debugLabel: 'TestImage', debugLabel: '_TestImage',
), ),
), ),
), ),
...@@ -1806,7 +1806,7 @@ void main() { ...@@ -1806,7 +1806,7 @@ void main() {
testWidgets('Keeps stream alive when ticker mode is disabled', (WidgetTester tester) async { testWidgets('Keeps stream alive when ticker mode is disabled', (WidgetTester tester) async {
imageCache!.maximumSize = 0; imageCache!.maximumSize = 0;
final ui.Image image = (await tester.runAsync(() => createTestImage(width: 1, height: 1, cache: false)))!; 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); provider.complete(image);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1835,8 +1835,8 @@ void main() { ...@@ -1835,8 +1835,8 @@ void main() {
testWidgets('Load a good image after a bad image was loaded should not call errorBuilder', (WidgetTester tester) async { testWidgets('Load a good image after a bad image was loaded should not call errorBuilder', (WidgetTester tester) async {
final UniqueKey errorKey = UniqueKey(); final UniqueKey errorKey = UniqueKey();
final ui.Image image = (await tester.runAsync(() => createTestImage()))!; final ui.Image image = (await tester.runAsync(() => createTestImage()))!;
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(); final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter); final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
...@@ -1878,81 +1878,40 @@ void main() { ...@@ -1878,81 +1878,40 @@ void main() {
expect(find.byKey(errorKey), findsNothing); expect(find.byKey(errorKey), findsNothing);
}); });
testWidgets('Image at default filterQuality', (WidgetTester tester) async { testWidgets('Failed image loads in debug mode', (WidgetTester tester) async {
await testImageQuality(tester, null); final Key key = UniqueKey();
}); await tester.pumpWidget(Center(
child: RepaintBoundary(
testWidgets('Image at high filterQuality', (WidgetTester tester) async { key: key,
await testImageQuality(tester, ui.FilterQuality.high); child: Container(
}); width: 150.0,
height: 50.0,
testWidgets('Image at none filterQuality', (WidgetTester tester) async { decoration: BoxDecoration(
await testImageQuality(tester, ui.FilterQuality.none); border: Border.all(
}); width: 2.0,
} color: const Color(0xFF00FF99),
),
Future<void> testImageQuality(WidgetTester tester, ui.FilterQuality? quality) async { ),
await tester.binding.setSurfaceSize(const ui.Size(3, 3)); child: Image.asset('missing-asset'),
// 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( await expectLater(
find.byType(Image), find.byKey(key),
matchesGoldenFile('image_quality_${quality ?? 'default'}.png'), matchesGoldenFile('image_test.missing.1.png'),
); );
} expect(tester.takeException().toString(), startsWith('Unable to load asset: '));
await tester.pump();
class ImagePainter extends CustomPainter { await expectLater(
ImagePainter(this.image); find.byKey(key),
matchesGoldenFile('image_test.missing.2.png'),
@override );
void paint(ui.Canvas canvas, ui.Size size) { }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/74935 (broken assets not being reported on web)
canvas.drawImage(image, Offset.zero, Paint());
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
final ui.Image image;
} }
@immutable @immutable
class ConfigurationAwareKey { class _ConfigurationAwareKey {
const ConfigurationAwareKey(this.provider, this.configuration) const _ConfigurationAwareKey(this.provider, this.configuration)
: assert(provider != null), : assert(provider != null),
assert(configuration != null); assert(configuration != null);
...@@ -1964,7 +1923,7 @@ class ConfigurationAwareKey { ...@@ -1964,7 +1923,7 @@ class ConfigurationAwareKey {
if (other.runtimeType != runtimeType) { if (other.runtimeType != runtimeType) {
return false; return false;
} }
return other is ConfigurationAwareKey return other is _ConfigurationAwareKey
&& other.provider == provider && other.provider == provider
&& other.configuration == configuration; && other.configuration == configuration;
} }
...@@ -1973,15 +1932,15 @@ class ConfigurationAwareKey { ...@@ -1973,15 +1932,15 @@ class ConfigurationAwareKey {
int get hashCode => hashValues(provider, configuration); int get hashCode => hashValues(provider, configuration);
} }
class ConfigurationKeyedTestImageProvider extends TestImageProvider { class _ConfigurationKeyedTestImageProvider extends _TestImageProvider {
@override @override
Future<ConfigurationAwareKey> obtainKey(ImageConfiguration configuration) { Future<_ConfigurationAwareKey> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<ConfigurationAwareKey>(ConfigurationAwareKey(this, configuration)); return SynchronousFuture<_ConfigurationAwareKey>(_ConfigurationAwareKey(this, configuration));
} }
} }
class TestImageProvider extends ImageProvider<Object> { class _TestImageProvider extends ImageProvider<Object> {
TestImageProvider({ImageStreamCompleter? streamCompleter}) { _TestImageProvider({ImageStreamCompleter? streamCompleter}) {
_streamCompleter = streamCompleter _streamCompleter = streamCompleter
?? OneFrameImageStreamCompleter(_completer.future); ?? OneFrameImageStreamCompleter(_completer.future);
} }
...@@ -1996,7 +1955,7 @@ class TestImageProvider extends ImageProvider<Object> { ...@@ -1996,7 +1955,7 @@ class TestImageProvider extends ImageProvider<Object> {
@override @override
Future<Object> obtainKey(ImageConfiguration configuration) { Future<Object> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<TestImageProvider>(this); return SynchronousFuture<_TestImageProvider>(this);
} }
@override @override
...@@ -2023,14 +1982,8 @@ class TestImageProvider extends ImageProvider<Object> { ...@@ -2023,14 +1982,8 @@ class TestImageProvider extends ImageProvider<Object> {
String toString() => '${describeIdentity(this)}()'; String toString() => '${describeIdentity(this)}()';
} }
class SimpleTestImageStreamCompleter extends ImageStreamCompleter { class _TestImageStreamCompleter extends ImageStreamCompleter {
void testSetImage(ui.Image image) { _TestImageStreamCompleter([this._currentImage]);
setImage(ImageInfo(image: image, scale: 1.0));
}
}
class TestImageStreamCompleter extends ImageStreamCompleter {
TestImageStreamCompleter([this._currentImage]);
ImageInfo? _currentImage; ImageInfo? _currentImage;
final Set<ImageStreamListener> listeners = <ImageStreamListener>{}; final Set<ImageStreamListener> listeners = <ImageStreamListener>{};
...@@ -2080,8 +2033,8 @@ class TestImageStreamCompleter extends ImageStreamCompleter { ...@@ -2080,8 +2033,8 @@ class TestImageStreamCompleter extends ImageStreamCompleter {
} }
} }
class DebouncingImageProvider extends ImageProvider<Object> { class _DebouncingImageProvider extends ImageProvider<Object> {
DebouncingImageProvider(this.imageProvider, this.seenKeys); _DebouncingImageProvider(this.imageProvider, this.seenKeys);
/// A set of keys that will only get resolved the _first_ time they are seen. /// A set of keys that will only get resolved the _first_ time they are seen.
/// ///
...@@ -2107,8 +2060,8 @@ class DebouncingImageProvider extends ImageProvider<Object> { ...@@ -2107,8 +2060,8 @@ class DebouncingImageProvider extends ImageProvider<Object> {
ImageStreamCompleter load(Object key, DecoderCallback decode) => imageProvider.load(key, decode); ImageStreamCompleter load(Object key, DecoderCallback decode) => imageProvider.load(key, decode);
} }
class FailingImageProvider extends ImageProvider<int> { class _FailingImageProvider extends ImageProvider<int> {
const FailingImageProvider({ const _FailingImageProvider({
this.failOnObtainKey = false, this.failOnObtainKey = false,
this.failOnLoad = false, this.failOnLoad = false,
required this.throws, 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