fade_in_image_test.dart 16.9 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
import 'dart:typed_data';
6 7 8 9
import 'dart:ui' as ui;

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
10 11

import '../image_data.dart';
12
import '../painting/image_test_utils.dart';
13

14 15 16 17 18 19 20 21
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;
22
  final FadeInImageElements? placeholder;
23 24
  final FadeInImageElements target;

25 26
  State? get state {
    StatefulElement? animatedFadeOutFadeInElement;
27 28
    fadeInImageElement.visitChildren((Element child) {
      expect(animatedFadeOutFadeInElement, isNull);
29
      animatedFadeOutFadeInElement = child as StatefulElement;
30 31
    });
    expect(animatedFadeOutFadeInElement, isNotNull);
32
    return animatedFadeOutFadeInElement!.state;
33 34
  }

35 36
  Element? get semanticsElement {
    Element? result;
37 38 39 40 41 42 43 44 45
    fadeInImageElement.visitChildren((Element child) {
      if (child.widget is Semantics)
        result = child;
    });
    return result;
  }
}

class FadeInImageElements {
46
  const FadeInImageElements(this.rawImageElement);
47 48 49

  final Element rawImageElement;

50
  RawImage get rawImage => rawImageElement.widget as RawImage;
51
  double get opacity => rawImage.opacity?.value ?? 1.0;
52 53
}

54
class LoadTestImageProvider extends ImageProvider<Object> {
55 56 57 58
  LoadTestImageProvider(this.provider);

  final ImageProvider provider;

59
  ImageStreamCompleter testLoad(Object key, DecoderCallback decode) {
60 61 62 63
    return provider.load(key, decode);
  }

  @override
64 65
  Future<Object> obtainKey(ImageConfiguration configuration) {
    throw UnimplementedError();
66 67 68
  }

  @override
69 70
  ImageStreamCompleter load(Object key, DecoderCallback decode) {
    throw UnimplementedError();
71 72 73
  }
}

74 75 76
FadeInImageParts findFadeInImage(WidgetTester tester) {
  final List<FadeInImageElements> elements = <FadeInImageElements>[];
  final Iterable<Element> rawImageElements = tester.elementList(find.byType(RawImage));
77
  ComponentElement? fadeInImageElement;
78
  for (final Element rawImageElement in rawImageElements) {
79
    rawImageElement.visitAncestorElements((Element ancestor) {
80
      if (ancestor.widget is FadeInImage) {
81
        if (fadeInImageElement == null) {
82
          fadeInImageElement = ancestor as ComponentElement;
83 84 85 86 87 88 89 90
        } else {
          expect(fadeInImageElement, same(ancestor));
        }
        return false;
      }
      return true;
    });
    expect(fadeInImageElement, isNotNull);
91
    elements.add(FadeInImageElements(rawImageElement));
92 93
  }
  if (elements.length == 2) {
94
    return FadeInImageParts(fadeInImageElement!, elements.last, elements.first);
95 96
  } else {
    expect(elements, hasLength(1));
97
    return FadeInImageParts(fadeInImageElement!, null, elements.first);
98 99 100
  }
}

101
Future<void> main() async {
102 103 104
  // These must run outside test zone to complete
  final ui.Image targetImage = await createTestImage();
  final ui.Image placeholderImage = await createTestImage();
105
  final ui.Image replacementImage = await createTestImage();
106 107

  group('FadeInImage', () {
108
    testWidgets('animates an uncached image', (WidgetTester tester) async {
109 110
      final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
      final TestImageProvider imageProvider = TestImageProvider(targetImage);
111

112
      await tester.pumpWidget(FadeInImage(
113 114
        placeholder: placeholderProvider,
        image: imageProvider,
115 116 117 118 119
        fadeOutDuration: animationDuration,
        fadeInDuration: animationDuration,
        fadeOutCurve: Curves.linear,
        fadeInCurve: Curves.linear,
        excludeFromSemantics: true,
120 121
      ));

122
      expect(findFadeInImage(tester).placeholder!.rawImage.image, null);
123 124
      expect(findFadeInImage(tester).target.rawImage.image, null);

125 126
      placeholderProvider.complete();
      await tester.pump();
127
      expect(findFadeInImage(tester).placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
128
      expect(findFadeInImage(tester).target.rawImage.image, null);
129

130 131 132 133
      imageProvider.complete();
      await tester.pump();
      for (int i = 0; i < 5; i += 1) {
        final FadeInImageParts parts = findFadeInImage(tester);
134 135 136
        expect(parts.placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
        expect(parts.target.rawImage.image!.isCloneOf(targetImage), true);
        expect(parts.placeholder!.opacity, moreOrLessEquals(1 - i / 5));
137
        expect(parts.target.opacity, 0);
138 139
        await tester.pump(const Duration(milliseconds: 10));
      }
140 141 142

      for (int i = 0; i < 5; i += 1) {
        final FadeInImageParts parts = findFadeInImage(tester);
143 144 145
        expect(parts.placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
        expect(parts.target.rawImage.image!.isCloneOf(targetImage), true);
        expect(parts.placeholder!.opacity, 0);
146
        expect(parts.target.opacity, moreOrLessEquals(i / 5));
147 148 149
        await tester.pump(const Duration(milliseconds: 10));
      }

150
      await tester.pumpWidget(FadeInImage(
151 152 153
        placeholder: placeholderProvider,
        image: imageProvider,
      ));
154
      expect(findFadeInImage(tester).target.rawImage.image!.isCloneOf(targetImage), true);
155 156 157 158 159 160 161 162 163
      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();

164
      await tester.pumpWidget(FadeInImage(
165 166 167
        placeholder: placeholderProvider,
        image: imageProvider,
      ));
168

169
      expect(findFadeInImage(tester).target.rawImage.image!.isCloneOf(targetImage), true);
170 171 172
      expect(findFadeInImage(tester).placeholder, isNull);
      expect(findFadeInImage(tester).target.opacity, 1);
    });
173

174
    testWidgets('handles updating the placeholder image', (WidgetTester tester) async {
175
      final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
176
      final TestImageProvider secondPlaceholderProvider = TestImageProvider(replacementImage);
177
      final TestImageProvider imageProvider = TestImageProvider(targetImage);
178

179
      await tester.pumpWidget(FadeInImage(
180 181
        placeholder: placeholderProvider,
        image: imageProvider,
182 183 184
        fadeOutDuration: animationDuration,
        fadeInDuration: animationDuration,
        excludeFromSemantics: true,
185
      ));
186

187
      final State? state = findFadeInImage(tester).state;
188 189
      placeholderProvider.complete();
      await tester.pump();
190
      expect(findFadeInImage(tester).placeholder!.rawImage.image!.isCloneOf(placeholderImage), true);
191

192
      await tester.pumpWidget(FadeInImage(
193 194
        placeholder: secondPlaceholderProvider,
        image: imageProvider,
195 196 197
        fadeOutDuration: animationDuration,
        fadeInDuration: animationDuration,
        excludeFromSemantics: true,
198
      ));
199

200 201
      secondPlaceholderProvider.complete();
      await tester.pump();
202
      expect(findFadeInImage(tester).placeholder!.rawImage.image!.isCloneOf(replacementImage), true);
203 204 205
      expect(findFadeInImage(tester).state, same(state));
    });

206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
    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);
    });

228 229 230 231 232 233 234 235 236 237 238 239 240
    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,
      ));

241
      final State? state = findFadeInImage(tester).state;
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
      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();
257

258
      expect(findFadeInImage(tester).target.rawImage.image!.isCloneOf(replacementImage), true);
259
      expect(findFadeInImage(tester).state, same(state));
260
      expect(findFadeInImage(tester).placeholder!.opacity, moreOrLessEquals(1));
261 262
      expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
      await tester.pump(animationDuration);
263
      expect(findFadeInImage(tester).placeholder!.opacity, moreOrLessEquals(0));
264 265
      expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
      await tester.pump(animationDuration);
266
      expect(findFadeInImage(tester).placeholder!.opacity, moreOrLessEquals(0));
267
      expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(1));
268
    });
269

270
    testWidgets("doesn't interrupt in-progress animation when animation values are updated", (WidgetTester tester) async {
271 272
      final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
      final TestImageProvider imageProvider = TestImageProvider(targetImage);
273

274 275 276 277 278 279 280 281
      await tester.pumpWidget(FadeInImage(
        placeholder: placeholderProvider,
        image: imageProvider,
        fadeOutDuration: animationDuration,
        fadeInDuration: animationDuration,
        excludeFromSemantics: true,
      ));

282
      final State? state = findFadeInImage(tester).state;
283 284 285 286
      placeholderProvider.complete();
      imageProvider.complete();
      await tester.pump();
      await tester.pump(animationDuration);
287

288 289 290 291 292 293 294 295 296
      await tester.pumpWidget(FadeInImage(
        placeholder: placeholderProvider,
        image: imageProvider,
        fadeOutDuration: animationDuration * 2,
        fadeInDuration: animationDuration * 2,
        excludeFromSemantics: true,
      ));

      expect(findFadeInImage(tester).state, same(state));
297
      expect(findFadeInImage(tester).placeholder!.opacity, moreOrLessEquals(0));
298 299
      expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
      await tester.pump(animationDuration);
300
      expect(findFadeInImage(tester).placeholder!.opacity, moreOrLessEquals(0));
301 302 303
      expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(1));
    });

304
    group('ImageProvider', () {
305 306 307 308 309 310 311 312 313 314 315 316 317

      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;
318
        Future<ui.Codec> decode(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) {
319 320
          expect(cacheWidth, 20);
          expect(cacheHeight, 30);
321
          expect(allowUpscaling, false);
322
          called = true;
323
          return PaintingBinding.instance!.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling);
324
        }
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
        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;
341
        Future<ui.Codec> decode(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool allowUpscaling = false}) {
342 343
          expect(cacheWidth, null);
          expect(cacheHeight, null);
344
          expect(allowUpscaling, false);
345
          called = true;
346
          return PaintingBinding.instance!.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
347
        }
348 349 350 351 352 353 354 355 356 357
        // 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);
      });
    });

358 359
    group('semantics', () {
      testWidgets('only one Semantics node appears within FadeInImage', (WidgetTester tester) async {
360 361
        final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
        final TestImageProvider imageProvider = TestImageProvider(targetImage);
362

363
        await tester.pumpWidget(FadeInImage(
364 365
          placeholder: placeholderProvider,
          image: imageProvider,
366 367
        ));

368
        expect(find.byType(Semantics), findsOneWidget);
369 370
      });

371
      testWidgets('is excluded if excludeFromSemantics is true', (WidgetTester tester) async {
372 373
        final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
        final TestImageProvider imageProvider = TestImageProvider(targetImage);
374

375
        await tester.pumpWidget(FadeInImage(
376 377 378
          placeholder: placeholderProvider,
          image: imageProvider,
          excludeFromSemantics: true,
379 380
        ));

381
        expect(find.byType(Semantics), findsNothing);
382 383
      });

384 385 386 387 388 389 390 391 392
      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);

393 394 395 396 397 398 399 400 401
          await tester.pumpWidget(Directionality(
            textDirection: TextDirection.ltr,
            child: FadeInImage(
              placeholder: placeholderProvider,
              image: imageProvider,
              fadeOutDuration: animationDuration,
              fadeInDuration: animationDuration,
              imageSemanticLabel: imageSemanticText,
            ),
402
          ));
403

404 405 406
          placeholderProvider.complete();
          await tester.pump();
          expect(semanticsWidget().properties.label, imageSemanticText);
407

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
          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);
        });
436 437
      });
    });
438 439
  });
}