blob: c5078a8be6fe444461968dcd54a9b1e56b209eaa [file] [log] [blame]
// 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../image_data.dart';
import '../painting/image_test_utils.dart';
const Duration animationDuration = Duration(milliseconds: 50);
class FadeInImageParts {
const FadeInImageParts(this.fadeInImageElement, this.placeholder, this.target)
: assert(fadeInImageElement != null),
assert(target != null);
final ComponentElement fadeInImageElement;
final FadeInImageElements? placeholder;
final FadeInImageElements target;
State? get state {
StatefulElement? animatedFadeOutFadeInElement;
fadeInImageElement.visitChildren((Element child) {
expect(animatedFadeOutFadeInElement, isNull);
animatedFadeOutFadeInElement = child as StatefulElement;
});
expect(animatedFadeOutFadeInElement, isNotNull);
return animatedFadeOutFadeInElement!.state;
}
Element? get semanticsElement {
Element? result;
fadeInImageElement.visitChildren((Element child) {
if (child.widget is Semantics) {
result = child;
}
});
return result;
}
}
class FadeInImageElements {
const FadeInImageElements(this.rawImageElement);
final Element rawImageElement;
RawImage get rawImage => rawImageElement.widget as RawImage;
double get opacity => rawImage.opacity?.value ?? 1.0;
BoxFit? get fit => rawImage.fit;
FilterQuality? get filterQuality => rawImage.filterQuality;
}
class LoadTestImageProvider extends ImageProvider<Object> {
LoadTestImageProvider(this.provider);
final ImageProvider provider;
ImageStreamCompleter testLoad(Object key, DecoderBufferCallback decode) {
return provider.loadBuffer(key, decode);
}
@override
Future<Object> obtainKey(ImageConfiguration configuration) {
throw UnimplementedError();
}
@override
ImageStreamCompleter load(Object key, DecoderCallback decode) {
throw UnimplementedError();
}
}
FadeInImageParts findFadeInImage(WidgetTester tester) {
final List<FadeInImageElements> elements = <FadeInImageElements>[];
final Iterable<Element> rawImageElements = tester.elementList(find.byType(RawImage));
ComponentElement? fadeInImageElement;
for (final Element rawImageElement in rawImageElements) {
rawImageElement.visitAncestorElements((Element ancestor) {
if (ancestor.widget is FadeInImage) {
if (fadeInImageElement == null) {
fadeInImageElement = ancestor as ComponentElement;
} else {
expect(fadeInImageElement, same(ancestor));
}
return false;
}
return true;
});
expect(fadeInImageElement, isNotNull);
elements.add(FadeInImageElements(rawImageElement));
}
if (elements.length == 2) {
return FadeInImageParts(fadeInImageElement!, elements.last, elements.first);
} else {
expect(elements, hasLength(1));
return FadeInImageParts(fadeInImageElement!, null, elements.first);
}
}
Future<void> main() async {
// These must run outside test zone to complete
final ui.Image targetImage = await createTestImage();
final ui.Image placeholderImage = await createTestImage();
final ui.Image replacementImage = await createTestImage();
group('FadeInImage', () {
testWidgets('animates an uncached image', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
fadeOutCurve: Curves.linear,
fadeInCurve: Curves.linear,
excludeFromSemantics: true,
));
expect(findFadeInImage(tester).placeholder!.rawImage.image, null);
expect(findFadeInImage(tester).target.rawImage.image, null);
placeholderProvider.complete();
await tester.pump();
expect(findFadeInImage(tester).placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
expect(findFadeInImage(tester).target.rawImage.image, null);
imageProvider.complete();
await tester.pump();
for (int i = 0; i < 5; i += 1) {
final FadeInImageParts parts = findFadeInImage(tester);
expect(parts.placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
expect(parts.target.rawImage.image!.isCloneOf(targetImage), true);
expect(parts.placeholder!.opacity, moreOrLessEquals(1 - i / 5));
expect(parts.target.opacity, 0);
await tester.pump(const Duration(milliseconds: 10));
}
for (int i = 0; i < 5; i += 1) {
final FadeInImageParts parts = findFadeInImage(tester);
expect(parts.placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
expect(parts.target.rawImage.image!.isCloneOf(targetImage), true);
expect(parts.placeholder!.opacity, 0);
expect(parts.target.opacity, moreOrLessEquals(i / 5));
await tester.pump(const Duration(milliseconds: 10));
}
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
expect(findFadeInImage(tester).target.rawImage.image!.isCloneOf(targetImage), true);
expect(findFadeInImage(tester).target.opacity, 1);
});
testWidgets("FadeInImage's image obeys gapless playback", (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
final TestImageProvider secondImageProvider = TestImageProvider(replacementImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
));
imageProvider.complete();
placeholderProvider.complete();
await tester.pump();
await tester.pump(animationDuration * 2);
// Calls setState after the animation, which removes the placeholder image.
await tester.pump(const Duration(milliseconds: 100));
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: secondImageProvider,
));
await tester.pump();
FadeInImageParts parts = findFadeInImage(tester);
// Continually shows previously loaded image,
expect(parts.placeholder, isNull);
expect(parts.target.rawImage.image!.isCloneOf(targetImage), isTrue);
expect(parts.target.opacity, 1);
// Until the new image provider provides the image.
secondImageProvider.complete();
await tester.pump();
parts = findFadeInImage(tester);
expect(parts.target.rawImage.image!.isCloneOf(replacementImage), isTrue);
expect(parts.target.opacity, 1);
});
// Regression test for https://github.com/flutter/flutter/issues/111011
testWidgets("FadeInImage's image obeys gapless playback when first image is cached but second isn't",
(WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
final TestImageProvider secondImageProvider = TestImageProvider(replacementImage);
// Pre-cache the initial image.
imageProvider.resolve(ImageConfiguration.empty);
imageProvider.complete();
placeholderProvider.complete();
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
await tester.pumpAndSettle();
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: secondImageProvider,
));
FadeInImageParts parts = findFadeInImage(tester);
// Continually shows previously loaded image until the new image provider provides the image.
expect(parts.placeholder, isNull);
expect(parts.target.rawImage.image!.isCloneOf(targetImage), isTrue);
expect(parts.target.opacity, 1);
// Now, provide the image.
secondImageProvider.complete();
await tester.pump();
parts = findFadeInImage(tester);
expect(parts.target.rawImage.image!.isCloneOf(replacementImage), isTrue);
expect(parts.target.opacity, 1);
});
testWidgets("FadeInImage's placeholder obeys gapless playback", (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider secondPlaceholderProvider = TestImageProvider(replacementImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
placeholderProvider.complete();
await tester.pump();
FadeInImageParts parts = findFadeInImage(tester);
expect(parts.placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
expect(parts.placeholder!.opacity, 1);
await tester.pumpWidget(FadeInImage(
placeholder: secondPlaceholderProvider,
image: imageProvider,
));
parts = findFadeInImage(tester);
// continually shows previously loaded image.
expect(parts.placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
expect(parts.placeholder!.opacity, 1);
// Until the new image provider provides the image.
secondPlaceholderProvider.complete();
await tester.pump();
parts = findFadeInImage(tester);
expect(parts.placeholder!.rawImage.image!.isCloneOf(replacementImage), true);
expect(parts.placeholder!.opacity, 1);
});
testWidgets('shows a cached image immediately when skipFadeOnSynchronousLoad=true', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
imageProvider.resolve(ImageConfiguration.empty);
imageProvider.complete();
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
expect(findFadeInImage(tester).target.rawImage.image!.isCloneOf(targetImage), true);
expect(findFadeInImage(tester).placeholder, isNull);
expect(findFadeInImage(tester).target.opacity, 1);
});
testWidgets('handles updating the placeholder image', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider secondPlaceholderProvider = TestImageProvider(replacementImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
final State? state = findFadeInImage(tester).state;
placeholderProvider.complete();
await tester.pump();
expect(findFadeInImage(tester).placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
await tester.pumpWidget(FadeInImage(
placeholder: secondPlaceholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
secondPlaceholderProvider.complete();
await tester.pump();
expect(findFadeInImage(tester).placeholder!.rawImage.image!.isCloneOf(replacementImage), true);
expect(findFadeInImage(tester).state, same(state));
});
testWidgets('does not keep the placeholder in the tree if it is invisible', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
placeholderProvider.complete();
await tester.pumpAndSettle();
expect(find.byType(Image), findsNWidgets(2));
imageProvider.complete();
await tester.pumpAndSettle();
expect(find.byType(Image), findsOneWidget);
});
testWidgets("doesn't interrupt in-progress animation when animation values are updated", (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
excludeFromSemantics: true,
));
final State? state = findFadeInImage(tester).state;
placeholderProvider.complete();
imageProvider.complete();
await tester.pump();
await tester.pump(animationDuration);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration * 2,
fadeInDuration: animationDuration * 2,
excludeFromSemantics: true,
));
expect(findFadeInImage(tester).state, same(state));
expect(findFadeInImage(tester).placeholder!.opacity, moreOrLessEquals(0));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
await tester.pump(animationDuration);
expect(findFadeInImage(tester).placeholder!.opacity, moreOrLessEquals(0));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(1));
});
group('ImageProvider', () {
test('memory placeholder cacheWidth and cacheHeight is passed through', () async {
final Uint8List testBytes = Uint8List.fromList(kTransparentImage);
final FadeInImage image = FadeInImage.memoryNetwork(
placeholder: testBytes,
image: 'test.com',
placeholderCacheWidth: 20,
placeholderCacheHeight: 30,
imageCacheWidth: 40,
imageCacheHeight: 50,
);
bool called = false;
Future<ui.Codec> decode(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) {
expect(cacheWidth, 20);
expect(cacheHeight, 30);
expect(allowUpscaling, false);
called = true;
return PaintingBinding.instance.instantiateImageCodecFromBuffer(buffer, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling);
}
final ImageProvider resizeImage = image.placeholder;
expect(image.placeholder, isA<ResizeImage>());
expect(called, false);
final LoadTestImageProvider testProvider = LoadTestImageProvider(image.placeholder);
final ImageStreamCompleter streamCompleter = testProvider.testLoad(await resizeImage.obtainKey(ImageConfiguration.empty), decode);
final Completer<void> completer = Completer<void>();
streamCompleter.addListener(ImageStreamListener((ImageInfo imageInfo, bool syncCall) {
completer.complete();
}));
await completer.future;
expect(called, true);
});
test('do not resize when null cache dimensions', () async {
final Uint8List testBytes = Uint8List.fromList(kTransparentImage);
final FadeInImage image = FadeInImage.memoryNetwork(
placeholder: testBytes,
image: 'test.com',
);
bool called = false;
Future<ui.Codec> decode(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) {
expect(cacheWidth, null);
expect(cacheHeight, null);
expect(allowUpscaling, false);
called = true;
return PaintingBinding.instance.instantiateImageCodecFromBuffer(buffer, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
}
// image.placeholder should be an instance of MemoryImage instead of ResizeImage
final ImageProvider memoryImage = image.placeholder;
expect(image.placeholder, isA<MemoryImage>());
expect(called, false);
final LoadTestImageProvider testProvider = LoadTestImageProvider(image.placeholder);
final ImageStreamCompleter streamCompleter = testProvider.testLoad(await memoryImage.obtainKey(ImageConfiguration.empty), decode);
final Completer<void> completer = Completer<void>();
streamCompleter.addListener(ImageStreamListener((ImageInfo imageInfo, bool syncCall) {
completer.complete();
}));
await completer.future;
expect(called, true);
});
});
group('semantics', () {
testWidgets('only one Semantics node appears within FadeInImage', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
));
expect(find.byType(Semantics), findsOneWidget);
});
testWidgets('is excluded if excludeFromSemantics is true', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
excludeFromSemantics: true,
));
expect(find.byType(Semantics), findsNothing);
});
group('label', () {
const String imageSemanticText = 'Test image semantic label';
testWidgets('defaults to image label if placeholder label is unspecified', (WidgetTester tester) async {
Semantics semanticsWidget() => tester.widget(find.byType(Semantics));
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
imageSemanticLabel: imageSemanticText,
),
));
placeholderProvider.complete();
await tester.pump();
expect(semanticsWidget().properties.label, imageSemanticText);
imageProvider.complete();
await tester.pump();
await tester.pump(const Duration(milliseconds: 51));
expect(semanticsWidget().properties.label, imageSemanticText);
});
testWidgets('is empty without any specified semantics labels', (WidgetTester tester) async {
Semantics semanticsWidget() => tester.widget(find.byType(Semantics));
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
));
placeholderProvider.complete();
await tester.pump();
expect(semanticsWidget().properties.label, isEmpty);
imageProvider.complete();
await tester.pump();
await tester.pump(const Duration(milliseconds: 51));
expect(semanticsWidget().properties.label, isEmpty);
});
});
});
group("placeholder's BoxFit", () {
testWidgets("should be the image's BoxFit when not set", (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fit: BoxFit.cover,
));
expect(findFadeInImage(tester).placeholder!.fit, equals(findFadeInImage(tester).target.fit));
expect(findFadeInImage(tester).placeholder!.fit, equals(BoxFit.cover));
});
testWidgets('should be the given value when set', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
fit: BoxFit.cover,
placeholderFit: BoxFit.fill,
));
expect(findFadeInImage(tester).target.fit, equals(BoxFit.cover));
expect(findFadeInImage(tester).placeholder!.fit, equals(BoxFit.fill));
});
});
group("placeholder's FilterQuality", () {
testWidgets("should be the image's FilterQuality when not set", (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
filterQuality: FilterQuality.medium,
));
expect(findFadeInImage(tester).placeholder!.filterQuality, equals(findFadeInImage(tester).target.filterQuality));
expect(findFadeInImage(tester).placeholder!.filterQuality, equals(FilterQuality.medium));
});
testWidgets('should be the given value when set', (WidgetTester tester) async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage(
placeholder: placeholderProvider,
image: imageProvider,
filterQuality: FilterQuality.medium,
placeholderFilterQuality: FilterQuality.high,
));
expect(findFadeInImage(tester).target.filterQuality, equals(FilterQuality.medium));
expect(findFadeInImage(tester).placeholder!.filterQuality, equals(FilterQuality.high));
});
});
});
}