fade_in_image_test.dart 17.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7
import 'dart:async';
8
import 'dart:typed_data';
9 10 11
import 'dart:ui' as ui;

import 'package:flutter/widgets.dart';
12
import 'package:flutter/painting.dart';
13
import 'package:flutter_test/flutter_test.dart';
14
import '../painting/image_data.dart';
15
import '../painting/image_test_utils.dart';
16

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
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);
32
      animatedFadeOutFadeInElement = child as StatefulElement;
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
    });
    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;

54 55
  RawImage get rawImage => rawImageElement.widget as RawImage;
  FadeTransition get fadeTransition => fadeTransitionElement?.widget as FadeTransition;
56 57 58
  double get opacity => fadeTransition == null ? 1 : fadeTransition.opacity.value;
}

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
class LoadTestImageProvider extends ImageProvider<dynamic> {
  LoadTestImageProvider(this.provider);

  final ImageProvider provider;

  ImageStreamCompleter testLoad(dynamic key, DecoderCallback decode) {
    return provider.load(key, decode);
  }

  @override
  Future<dynamic> obtainKey(ImageConfiguration configuration) {
    return null;
  }

  @override
  ImageStreamCompleter load(dynamic key, DecoderCallback decode) {
    return null;
  }
}

79 80 81 82
FadeInImageParts findFadeInImage(WidgetTester tester) {
  final List<FadeInImageElements> elements = <FadeInImageElements>[];
  final Iterable<Element> rawImageElements = tester.elementList(find.byType(RawImage));
  ComponentElement fadeInImageElement;
83
  for (final Element rawImageElement in rawImageElements) {
84 85 86 87 88 89
    Element fadeTransitionElement;
    rawImageElement.visitAncestorElements((Element ancestor) {
      if (ancestor.widget is FadeTransition) {
        fadeTransitionElement = ancestor;
      } else if (ancestor.widget is FadeInImage) {
        if (fadeInImageElement == null) {
90
          fadeInImageElement = ancestor as ComponentElement;
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
        } 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);
  }
}

109
Future<void> main() async {
110 111 112
  // These must run outside test zone to complete
  final ui.Image targetImage = await createTestImage();
  final ui.Image placeholderImage = await createTestImage();
113
  final ui.Image replacementImage = await createTestImage();
114 115

  group('FadeInImage', () {
116
    testWidgets('animates an uncached image', (WidgetTester tester) async {
117 118
      final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
      final TestImageProvider imageProvider = TestImageProvider(targetImage);
119

120
      await tester.pumpWidget(FadeInImage(
121 122
        placeholder: placeholderProvider,
        image: imageProvider,
123 124 125 126 127
        fadeOutDuration: animationDuration,
        fadeInDuration: animationDuration,
        fadeOutCurve: Curves.linear,
        fadeInCurve: Curves.linear,
        excludeFromSemantics: true,
128 129
      ));

130 131 132
      expect(findFadeInImage(tester).placeholder.rawImage.image, null);
      expect(findFadeInImage(tester).target.rawImage.image, null);

133 134
      placeholderProvider.complete();
      await tester.pump();
135 136
      expect(findFadeInImage(tester).placeholder.rawImage.image, same(placeholderImage));
      expect(findFadeInImage(tester).target.rawImage.image, null);
137

138 139 140 141 142 143 144 145
      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);
146 147
        await tester.pump(const Duration(milliseconds: 10));
      }
148 149 150 151 152 153 154

      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));
155 156 157
        await tester.pump(const Duration(milliseconds: 10));
      }

158
      await tester.pumpWidget(FadeInImage(
159 160 161
        placeholder: placeholderProvider,
        image: imageProvider,
      ));
162 163 164 165 166 167 168 169 170 171
      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();

172
      await tester.pumpWidget(FadeInImage(
173 174 175
        placeholder: placeholderProvider,
        image: imageProvider,
      ));
176

177 178 179 180
      expect(findFadeInImage(tester).target.rawImage.image, same(targetImage));
      expect(findFadeInImage(tester).placeholder, isNull);
      expect(findFadeInImage(tester).target.opacity, 1);
    });
181

182
    testWidgets('handles updating the placeholder image', (WidgetTester tester) async {
183
      final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
184
      final TestImageProvider secondPlaceholderProvider = TestImageProvider(replacementImage);
185
      final TestImageProvider imageProvider = TestImageProvider(targetImage);
186

187
      await tester.pumpWidget(FadeInImage(
188 189
        placeholder: placeholderProvider,
        image: imageProvider,
190 191 192
        fadeOutDuration: animationDuration,
        fadeInDuration: animationDuration,
        excludeFromSemantics: true,
193
      ));
194 195

      final State state = findFadeInImage(tester).state;
196 197
      placeholderProvider.complete();
      await tester.pump();
198
      expect(findFadeInImage(tester).placeholder.rawImage.image, same(placeholderImage));
199

200
      await tester.pumpWidget(FadeInImage(
201 202
        placeholder: secondPlaceholderProvider,
        image: imageProvider,
203 204 205
        fadeOutDuration: animationDuration,
        fadeInDuration: animationDuration,
        excludeFromSemantics: true,
206
      ));
207

208 209
      secondPlaceholderProvider.complete();
      await tester.pump();
210 211 212 213
      expect(findFadeInImage(tester).placeholder.rawImage.image, same(replacementImage));
      expect(findFadeInImage(tester).state, same(state));
    });

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
    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);
    });

236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
    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();
265

266 267 268 269 270 271 272 273 274 275
      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));
276
    });
277

278
    testWidgets("doesn't interrupt in-progress animation when animation values are updated", (WidgetTester tester) async {
279 280
      final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
      final TestImageProvider imageProvider = TestImageProvider(targetImage);
281

282 283 284 285 286 287 288 289 290 291 292 293 294
      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);
295

296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
      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));
    });

312
    group('ImageProvider', () {
313 314 315 316 317 318 319 320 321 322 323 324 325

      testWidgets('memory placeholder cacheWidth and cacheHeight is passed through', (WidgetTester tester) 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;
326
        final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
327 328
          expect(cacheWidth, 20);
          expect(cacheHeight, 30);
329
          expect(allowUpscaling, false);
330
          called = true;
331
          return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling);
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
        };
        final ImageProvider resizeImage = image.placeholder;
        expect(image.placeholder, isA<ResizeImage>());
        expect(called, false);
        final LoadTestImageProvider testProvider = LoadTestImageProvider(image.placeholder);
        testProvider.testLoad(await resizeImage.obtainKey(ImageConfiguration.empty), decode);
        expect(called, true);
      });

      testWidgets('do not resize when null cache dimensions', (WidgetTester tester) async {
        final Uint8List testBytes = Uint8List.fromList(kTransparentImage);
        final FadeInImage image = FadeInImage.memoryNetwork(
          placeholder: testBytes,
          image: 'test.com',
        );

        bool called = false;
349
        final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) {
350 351
          expect(cacheWidth, null);
          expect(cacheHeight, null);
352
          expect(allowUpscaling, null);
353 354 355 356 357 358 359 360 361 362 363 364 365
          called = true;
          return PaintingBinding.instance.instantiateImageCodec(bytes, 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);
        testProvider.testLoad(await memoryImage.obtainKey(ImageConfiguration.empty), decode);
        expect(called, true);
      });
    });

366 367
    group('semantics', () {
      testWidgets('only one Semantics node appears within FadeInImage', (WidgetTester tester) async {
368 369
        final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
        final TestImageProvider imageProvider = TestImageProvider(targetImage);
370

371
        await tester.pumpWidget(FadeInImage(
372 373
          placeholder: placeholderProvider,
          image: imageProvider,
374 375
        ));

376
        expect(find.byType(Semantics), findsOneWidget);
377 378
      });

379
      testWidgets('is excluded if excludeFromSemantics is true', (WidgetTester tester) async {
380 381
        final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
        final TestImageProvider imageProvider = TestImageProvider(targetImage);
382

383
        await tester.pumpWidget(FadeInImage(
384 385 386
          placeholder: placeholderProvider,
          image: imageProvider,
          excludeFromSemantics: true,
387 388
        ));

389
        expect(find.byType(Semantics), findsNothing);
390 391
      });

392 393 394 395 396 397 398 399 400
      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);

401 402 403 404 405 406 407 408 409
          await tester.pumpWidget(Directionality(
            textDirection: TextDirection.ltr,
            child: FadeInImage(
              placeholder: placeholderProvider,
              image: imageProvider,
              fadeOutDuration: animationDuration,
              fadeInDuration: animationDuration,
              imageSemanticLabel: imageSemanticText,
            ),
410
          ));
411

412 413 414
          placeholderProvider.complete();
          await tester.pump();
          expect(semanticsWidget().properties.label, imageSemanticText);
415

416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
          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);
        });
444 445
      });
    });
446 447
  });
}