// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.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; }); 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, this.fadeTransitionElement); final Element rawImageElement; final Element fadeTransitionElement; RawImage get rawImage => rawImageElement.widget; FadeTransition get fadeTransition => fadeTransitionElement?.widget; double get opacity => fadeTransition == null ? 1 : fadeTransition.opacity.value; } FadeInImageParts findFadeInImage(WidgetTester tester) { final List<FadeInImageElements> elements = <FadeInImageElements>[]; final Iterable<Element> rawImageElements = tester.elementList(find.byType(RawImage)); ComponentElement fadeInImageElement; for (Element rawImageElement in rawImageElements) { Element fadeTransitionElement; rawImageElement.visitAncestorElements((Element ancestor) { if (ancestor.widget is FadeTransition) { fadeTransitionElement = ancestor; } else if (ancestor.widget is FadeInImage) { if (fadeInImageElement == null) { fadeInImageElement = ancestor; } else { expect(fadeInImageElement, same(ancestor)); } return false; } return true; }); expect(fadeInImageElement, isNotNull); elements.add(FadeInImageElements(rawImageElement, fadeTransitionElement)); } 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, same(placeholderImage)); 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, same(placeholderImage)); expect(parts.target.rawImage.image, same(targetImage)); 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, same(placeholderImage)); expect(parts.target.rawImage.image, same(targetImage)); 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, same(targetImage)); expect(findFadeInImage(tester).target.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(FakeImageConfiguration()); imageProvider.complete(); await tester.pumpWidget(FadeInImage( placeholder: placeholderProvider, image: imageProvider, )); expect(findFadeInImage(tester).target.rawImage.image, same(targetImage)); 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, same(placeholderImage)); 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, same(replacementImage)); expect(findFadeInImage(tester).state, same(state)); }); testWidgets('re-fades in the image when the target image is updated', (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, excludeFromSemantics: true, )); final State state = findFadeInImage(tester).state; placeholderProvider.complete(); imageProvider.complete(); await tester.pump(); await tester.pump(animationDuration * 2); await tester.pumpWidget(FadeInImage( placeholder: placeholderProvider, image: secondImageProvider, fadeOutDuration: animationDuration, fadeInDuration: animationDuration, excludeFromSemantics: true, )); secondImageProvider.complete(); await tester.pump(); expect(findFadeInImage(tester).target.rawImage.image, same(replacementImage)); expect(findFadeInImage(tester).state, same(state)); expect(findFadeInImage(tester).placeholder.opacity, moreOrLessEquals(1)); expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0)); await tester.pump(animationDuration); 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)); }); 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('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); }); }); }); }); }