image_test.dart 77 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 7
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
8
library;
9

10
import 'dart:async';
11
import 'dart:io';
12
import 'dart:math' as math;
13
import 'dart:ui' as ui;
14

15
import 'package:flutter/foundation.dart';
16
import 'package:flutter/rendering.dart';
Dan Field's avatar
Dan Field committed
17
import 'package:flutter/scheduler.dart';
18
import 'package:flutter/widgets.dart';
19
import 'package:flutter_test/flutter_test.dart';
20

21
import '../image_data.dart';
22
import 'semantics_tester.dart';
23

24
void main() {
25 26
  late int originalCacheSize;
  late ui.Image image10x10;
Dan Field's avatar
Dan Field committed
27

28
  setUp(() async {
29 30 31
    originalCacheSize = imageCache.maximumSize;
    imageCache.clear();
    imageCache.clearLiveImages();
32
    image10x10 = await createTestImage(width: 10, height: 10);
Dan Field's avatar
Dan Field committed
33 34 35
  });

  tearDown(() {
36
    imageCache.maximumSize = originalCacheSize;
Dan Field's avatar
Dan Field committed
37 38
  });

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
  testWidgets('Verify Image does not use disposed handles', (WidgetTester tester) async {
    final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!;

    final _TestImageProvider imageProvider1 = _TestImageProvider();
    final _TestImageProvider imageProvider2 = _TestImageProvider();

    final ValueNotifier<_TestImageProvider> imageListenable = ValueNotifier<_TestImageProvider>(imageProvider1);
    final ValueNotifier<int> innerListenable = ValueNotifier<int>(0);

    bool imageLoaded = false;

    await tester.pumpWidget(ValueListenableBuilder<_TestImageProvider>(
      valueListenable: imageListenable,
      builder: (BuildContext context, _TestImageProvider image, Widget? child) => Image(
        image: image,
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
          if (frame == 0) {
            imageLoaded = true;
          }
          return LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) => ValueListenableBuilder<int>(
              valueListenable: innerListenable,
              builder: (BuildContext context, int value, Widget? valueListenableChild) => KeyedSubtree(
                key: UniqueKey(),
                child: child,
              ),
            ),
          );
        },
      ),
    ));

    imageLoaded = false;
    imageProvider1.complete(image10x10);
    await tester.idle();
    await tester.pump();
    expect(imageLoaded, true);

    imageLoaded = false;
    imageListenable.value = imageProvider2;
    innerListenable.value += 1;
    imageProvider2.complete(image100x100);
    await tester.idle();
    await tester.pump();
    expect(imageLoaded, true);
  });

86
  testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
87
    final GlobalKey key = GlobalKey();
88
    final _TestImageProvider imageProvider1 = _TestImageProvider();
89
    await tester.pumpWidget(
90
      Container(
91
        key: key,
92
        child: Image(
93 94
          image: imageProvider1,
          excludeFromSemantics: true,
95
        ),
96 97
      ),
      null,
98
      EnginePhase.layout,
99
    );
100
    RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage;
101
    expect(renderImage.image, isNull);
102

103
    imageProvider1.complete(image10x10);
104 105
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
106

107
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
108
    expect(renderImage.image, isNotNull);
109

110
    final _TestImageProvider imageProvider2 = _TestImageProvider();
111
    await tester.pumpWidget(
112
      Container(
113
        key: key,
114
        child: Image(
115 116
          image: imageProvider2,
          excludeFromSemantics: true,
117
        ),
118 119
      ),
      null,
120
      EnginePhase.layout,
121 122
    );

123
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
124
    expect(renderImage.image, isNull);
125 126
  });

127
  testWidgets("Verify Image doesn't reset its RenderImage when changing providers if it has gaplessPlayback set", (WidgetTester tester) async {
128
    final GlobalKey key = GlobalKey();
129
    final _TestImageProvider imageProvider1 = _TestImageProvider();
130
    await tester.pumpWidget(
131
      Container(
132
        key: key,
133
        child: Image(
134
          gaplessPlayback: true,
135 136
          image: imageProvider1,
          excludeFromSemantics: true,
137
        ),
138 139
      ),
      null,
140
      EnginePhase.layout,
141
    );
142
    RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage;
143
    expect(renderImage.image, isNull);
144

145
    imageProvider1.complete(image10x10);
146 147
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
148

149
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
150 151
    expect(renderImage.image, isNotNull);

152
    final _TestImageProvider imageProvider2 = _TestImageProvider();
153
    await tester.pumpWidget(
154
      Container(
155
        key: key,
156
        child: Image(
157
          gaplessPlayback: true,
158 159
          image: imageProvider2,
          excludeFromSemantics: true,
160
        ),
161 162
      ),
      null,
163
      EnginePhase.layout,
164 165
    );

166
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
167
    expect(renderImage.image, isNotNull);
168 169
  });

170
  testWidgets('Verify Image resets its RenderImage when changing providers if it has a key', (WidgetTester tester) async {
171
    final GlobalKey key = GlobalKey();
172
    final _TestImageProvider imageProvider1 = _TestImageProvider();
173
    await tester.pumpWidget(
174
      Image(
175
        key: key,
176 177
        image: imageProvider1,
        excludeFromSemantics: true,
178 179
      ),
      null,
180
      EnginePhase.layout,
181
    );
182
    RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage;
183 184
    expect(renderImage.image, isNull);

185
    imageProvider1.complete(image10x10);
186 187
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
188

189
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
190 191
    expect(renderImage.image, isNotNull);

192
    final _TestImageProvider imageProvider2 = _TestImageProvider();
193
    await tester.pumpWidget(
194
      Image(
195
        key: key,
196 197
        image: imageProvider2,
        excludeFromSemantics: true,
198 199
      ),
      null,
200
      EnginePhase.layout,
201
    );
202

203
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
204
    expect(renderImage.image, isNull);
205 206
  });

207
  testWidgets("Verify Image doesn't reset its RenderImage when changing providers if it has gaplessPlayback set", (WidgetTester tester) async {
208
    final GlobalKey key = GlobalKey();
209
    final _TestImageProvider imageProvider1 = _TestImageProvider();
210
    await tester.pumpWidget(
211
      Image(
212 213
        key: key,
        gaplessPlayback: true,
214 215
        image: imageProvider1,
        excludeFromSemantics: true,
216 217
      ),
      null,
218
      EnginePhase.layout,
219
    );
220
    RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage;
221 222
    expect(renderImage.image, isNull);

223
    imageProvider1.complete(image10x10);
224 225
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
226

227
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
228 229
    expect(renderImage.image, isNotNull);

230
    final _TestImageProvider imageProvider2 = _TestImageProvider();
231
    await tester.pumpWidget(
232
      Image(
233
        key: key,
234
        gaplessPlayback: true,
235
        excludeFromSemantics: true,
236
        image: imageProvider2,
237 238
      ),
      null,
239
      EnginePhase.layout,
240 241
    );

242
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
243
    expect(renderImage.image, isNotNull);
244 245
  });

246
  testWidgets('Verify ImageProvider configuration inheritance', (WidgetTester tester) async {
247 248 249
    final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
    final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
    final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
250
    final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider();
251
    final Set<Object> seenKeys = <Object>{};
252
    final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
253 254 255 256

    // Of the two nested MediaQuery objects, the innermost one,
    // mediaQuery2, should define the configuration of the imageProvider.
    await tester.pumpWidget(
257
      MediaQuery(
258
        key: mediaQueryKey1,
259
        data: const MediaQueryData(
260 261
          devicePixelRatio: 10.0,
        ),
262
        child: MediaQuery(
263
          key: mediaQueryKey2,
264
          data: const MediaQueryData(
265 266
            devicePixelRatio: 5.0,
          ),
267
          child: Image(
268
            excludeFromSemantics: true,
269
            key: imageKey,
270
            image: debouncingProvider,
271
          ),
272
        ),
273
      ),
274 275
    );

276
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);
277 278 279 280 281

    // This is the same widget hierarchy as before except that the
    // two MediaQuery objects have exchanged places. The imageProvider
    // should be resolved again, with the new innermost MediaQuery.
    await tester.pumpWidget(
282
      MediaQuery(
283
        key: mediaQueryKey2,
284
        data: const MediaQueryData(
285 286
          devicePixelRatio: 5.0,
        ),
287
        child: MediaQuery(
288
          key: mediaQueryKey1,
289
          data: const MediaQueryData(
290 291
            devicePixelRatio: 10.0,
          ),
292
          child: Image(
293
            excludeFromSemantics: true,
294
            key: imageKey,
295
            image: debouncingProvider,
296
          ),
297
        ),
298
      ),
299 300
    );

301
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 10.0);
302 303 304
  });

  testWidgets('Verify ImageProvider configuration inheritance again', (WidgetTester tester) async {
305 306 307
    final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
    final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
    final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
308
    final _ConfigurationKeyedTestImageProvider imageProvider = _ConfigurationKeyedTestImageProvider();
309
    final Set<Object> seenKeys = <Object>{};
310
    final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
311

312
    // This is just a variation on the previous test. In this version the location
313 314
    // of the Image changes and the MediaQuery widgets do not.
    await tester.pumpWidget(
315
      Row(
316
        textDirection: TextDirection.ltr,
317
        children: <Widget> [
318
          MediaQuery(
319
            key: mediaQueryKey2,
320
            data: const MediaQueryData(
321 322
              devicePixelRatio: 5.0,
            ),
323
            child: Image(
324
              excludeFromSemantics: true,
325
              key: imageKey,
326
              image: debouncingProvider,
327
            ),
328
          ),
329
          MediaQuery(
330
            key: mediaQueryKey1,
331
            data: const MediaQueryData(
332 333
              devicePixelRatio: 10.0,
            ),
334 335 336
            child: Container(width: 100.0),
          ),
        ],
337
      ),
338 339
    );

340
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);
341 342

    await tester.pumpWidget(
343
      Row(
344
        textDirection: TextDirection.ltr,
345
        children: <Widget> [
346
          MediaQuery(
347
            key: mediaQueryKey2,
348
            data: const MediaQueryData(
349 350
              devicePixelRatio: 5.0,
            ),
351
            child: Container(width: 100.0),
352
          ),
353
          MediaQuery(
354
            key: mediaQueryKey1,
355
            data: const MediaQueryData(
356 357
              devicePixelRatio: 10.0,
            ),
358
            child: Image(
359
              excludeFromSemantics: true,
360
              key: imageKey,
361
              image: debouncingProvider,
362 363 364
            ),
          ),
        ],
365
      ),
366 367
    );

368
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 10.0);
369 370
  });

371 372 373 374
  testWidgets('Verify ImageProvider does not inherit configuration when it does not key to it', (WidgetTester tester) async {
    final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
    final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
    final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
375
    final _TestImageProvider imageProvider = _TestImageProvider();
376
    final Set<Object> seenKeys = <Object>{};
377
    final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 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

    // Of the two nested MediaQuery objects, the innermost one,
    // mediaQuery2, should define the configuration of the imageProvider.
    await tester.pumpWidget(
      MediaQuery(
        key: mediaQueryKey1,
        data: const MediaQueryData(
          devicePixelRatio: 10.0,
        ),
        child: MediaQuery(
          key: mediaQueryKey2,
          data: const MediaQueryData(
            devicePixelRatio: 5.0,
          ),
          child: Image(
            excludeFromSemantics: true,
            key: imageKey,
            image: debouncingProvider,
          ),
        ),
      ),
    );

    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);

    // This is the same widget hierarchy as before except that the
    // two MediaQuery objects have exchanged places. The imageProvider
    // should not be resolved again, because it does not key to configuration.
    await tester.pumpWidget(
      MediaQuery(
        key: mediaQueryKey2,
        data: const MediaQueryData(
          devicePixelRatio: 5.0,
        ),
        child: MediaQuery(
          key: mediaQueryKey1,
          data: const MediaQueryData(
            devicePixelRatio: 10.0,
          ),
          child: Image(
            excludeFromSemantics: true,
            key: imageKey,
            image: debouncingProvider,
          ),
        ),
      ),
    );

    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);
  });

  testWidgets('Verify ImageProvider does not inherit configuration when it does not key to it again', (WidgetTester tester) async {
    final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
    final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
    final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
433
    final _TestImageProvider imageProvider = _TestImageProvider();
434
    final Set<Object> seenKeys = <Object>{};
435
    final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495

    // This is just a variation on the previous test. In this version the location
    // of the Image changes and the MediaQuery widgets do not.
    await tester.pumpWidget(
      Row(
        textDirection: TextDirection.ltr,
        children: <Widget> [
          MediaQuery(
            key: mediaQueryKey2,
            data: const MediaQueryData(
              devicePixelRatio: 5.0,
            ),
            child: Image(
              excludeFromSemantics: true,
              key: imageKey,
              image: debouncingProvider,
            ),
          ),
          MediaQuery(
            key: mediaQueryKey1,
            data: const MediaQueryData(
              devicePixelRatio: 10.0,
            ),
            child: Container(width: 100.0),
          ),
        ],
      ),
    );

    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);

    await tester.pumpWidget(
      Row(
        textDirection: TextDirection.ltr,
        children: <Widget> [
          MediaQuery(
            key: mediaQueryKey2,
            data: const MediaQueryData(
              devicePixelRatio: 5.0,
            ),
            child: Container(width: 100.0),
          ),
          MediaQuery(
            key: mediaQueryKey1,
            data: const MediaQueryData(
              devicePixelRatio: 10.0,
            ),
            child: Image(
              excludeFromSemantics: true,
              key: imageKey,
              image: debouncingProvider,
            ),
          ),
        ],
      ),
    );

    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);
  });

496
  testWidgets('Verify Image stops listening to ImageStream', (WidgetTester tester) async {
497
    final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!;
498 499 500
    // Web does not override the toString, whereas VM does
    final String imageString = image100x100.toString();

501
    final _TestImageProvider imageProvider = _TestImageProvider();
502
    await tester.pumpWidget(Image(image: imageProvider, excludeFromSemantics: true));
503
    final State<Image> image = tester.state/*State<Image>*/(find.byType(Image));
504
    expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null, loadingProgress: null, frameNumber: null, wasSynchronouslyLoaded: false)'));
505
    imageProvider.complete(image100x100);
506
    await tester.pump();
507
    expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 1 listener), pixels: $imageString @ 1.0x, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)'));
508
    await tester.pumpWidget(Container());
509
    expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(lifecycle state: defunct, not mounted, stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 0 listeners), pixels: null, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)'));
510 511
  });

512 513
  testWidgets('Stream completer errors can be listened to by attaching before resolving', (WidgetTester tester) async {
    dynamic capturedException;
514 515
    StackTrace? capturedStackTrace;
    ImageInfo? capturedImage;
516
    void errorListener(dynamic exception, StackTrace? stackTrace) {
517 518
      capturedException = exception;
      capturedStackTrace = stackTrace;
519 520
    }
    void listener(ImageInfo info, bool synchronous) {
521
      capturedImage = info;
522
    }
523

524
    final Exception testException = Exception('cannot resolve host');
525
    final StackTrace testStack = StackTrace.current;
526
    final _TestImageProvider imageProvider = _TestImageProvider();
527
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
528
    late ImageConfiguration configuration;
529
    await tester.pumpWidget(
530
      Builder(
531 532
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
533
          return Container();
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
        },
      ),
    );
    imageProvider.resolve(configuration);
    imageProvider.fail(testException, testStack);

    expect(tester.binding.microtaskCount, 1);
    await tester.idle(); // Let the failed completer's future hit the stream completer.
    expect(tester.binding.microtaskCount, 0);

    expect(capturedImage, isNull); // The image stream listeners should never be called.
    // The image stream error handler should have the original exception.
    expect(capturedException, testException);
    expect(capturedStackTrace, testStack);
    // If there is an error listener, there should be no FlutterError reported.
    expect(tester.takeException(), isNull);
  });

  testWidgets('Stream completer errors can be listened to by attaching after resolving', (WidgetTester tester) async {
    dynamic capturedException;
554
    StackTrace? capturedStackTrace;
555
    dynamic reportedException;
556 557
    StackTrace? reportedStackTrace;
    ImageInfo? capturedImage;
558
    void errorListener(dynamic exception, StackTrace? stackTrace) {
559 560
      capturedException = exception;
      capturedStackTrace = stackTrace;
561 562
    }
    void listener(ImageInfo info, bool synchronous) {
563
      capturedImage = info;
564
    }
565 566 567 568 569
    FlutterError.onError = (FlutterErrorDetails flutterError) {
      reportedException = flutterError.exception;
      reportedStackTrace = flutterError.stack;
    };

570
    final Exception testException = Exception('cannot resolve host');
571
    final StackTrace testStack = StackTrace.current;
572
    final _TestImageProvider imageProvider = _TestImageProvider();
573
    late ImageConfiguration configuration;
574
    await tester.pumpWidget(
575
      Builder(
576 577
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
578
          return Container();
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
        },
      ),
    );
    final ImageStream streamUnderTest = imageProvider.resolve(configuration);

    imageProvider.fail(testException, testStack);

    expect(tester.binding.microtaskCount, 1);
    await tester.idle(); // Let the failed completer's future hit the stream completer.
    expect(tester.binding.microtaskCount, 0);

    // Since there's no listeners attached yet, report error up via
    // FlutterError.
    expect(reportedException, testException);
    expect(reportedStackTrace, testStack);

595
    streamUnderTest.addListener(ImageStreamListener(listener, onError: errorListener));
596 597 598 599 600 601 602 603 604

    expect(capturedImage, isNull); // The image stream listeners should never be called.
    // The image stream error handler should have the original exception.
    expect(capturedException, testException);
    expect(capturedStackTrace, testStack);
  });

  testWidgets('Duplicate listener registration does not affect error listeners', (WidgetTester tester) async {
    dynamic capturedException;
605 606
    StackTrace? capturedStackTrace;
    ImageInfo? capturedImage;
607
    void errorListener(dynamic exception, StackTrace? stackTrace) {
608 609
      capturedException = exception;
      capturedStackTrace = stackTrace;
610 611
    }
    void listener(ImageInfo info, bool synchronous) {
612
      capturedImage = info;
613
    }
614

615
    final Exception testException = Exception('cannot resolve host');
616
    final StackTrace testStack = StackTrace.current;
617
    final _TestImageProvider imageProvider = _TestImageProvider();
618
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
619
    // Add the exact same listener a second time without the errorListener.
620
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener));
621
    late ImageConfiguration configuration;
622
    await tester.pumpWidget(
623
      Builder(
624 625
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
626
          return Container();
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
        },
      ),
    );
    imageProvider.resolve(configuration);
    imageProvider.fail(testException, testStack);

    expect(tester.binding.microtaskCount, 1);
    await tester.idle(); // Let the failed completer's future hit the stream completer.
    expect(tester.binding.microtaskCount, 0);

    expect(capturedImage, isNull); // The image stream listeners should never be called.
    // The image stream error handler should have the original exception.
    expect(capturedException, testException);
    expect(capturedStackTrace, testStack);
    // If there is an error listener, there should be no FlutterError reported.
    expect(tester.takeException(), isNull);
  });

  testWidgets('Duplicate error listeners are all called', (WidgetTester tester) async {
    dynamic capturedException;
647 648
    StackTrace? capturedStackTrace;
    ImageInfo? capturedImage;
649
    int errorListenerCalled = 0;
650
    void errorListener(dynamic exception, StackTrace? stackTrace) {
651 652 653
      capturedException = exception;
      capturedStackTrace = stackTrace;
      errorListenerCalled++;
654 655
    }
    void listener(ImageInfo info, bool synchronous) {
656
      capturedImage = info;
657
    }
658

659
    final Exception testException = Exception('cannot resolve host');
660
    final StackTrace testStack = StackTrace.current;
661
    final _TestImageProvider imageProvider = _TestImageProvider();
662
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
663
    // Add the exact same errorListener a second time.
664
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
665
    late ImageConfiguration configuration;
666
    await tester.pumpWidget(
667
      Builder(
668 669
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
670
          return Container();
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
        },
      ),
    );
    imageProvider.resolve(configuration);
    imageProvider.fail(testException, testStack);

    expect(tester.binding.microtaskCount, 1);
    await tester.idle(); // Let the failed completer's future hit the stream completer.
    expect(tester.binding.microtaskCount, 0);

    expect(capturedImage, isNull); // The image stream listeners should never be called.
    // The image stream error handler should have the original exception.
    expect(capturedException, testException);
    expect(capturedStackTrace, testStack);
    expect(errorListenerCalled, 2);
    // If there is an error listener, there should be no FlutterError reported.
    expect(tester.takeException(), isNull);
  });

690
  testWidgets('Listeners are only removed if callback tuple matches', (WidgetTester tester) async {
691 692
    bool errorListenerCalled = false;
    dynamic reportedException;
693 694
    StackTrace? reportedStackTrace;
    ImageInfo? capturedImage;
695
    void errorListener(dynamic exception, StackTrace? stackTrace) {
696
      errorListenerCalled = true;
697 698
      reportedException = exception;
      reportedStackTrace = stackTrace;
699 700
    }
    void listener(ImageInfo info, bool synchronous) {
701
      capturedImage = info;
702
    }
703

704
    final Exception testException = Exception('cannot resolve host');
705
    final StackTrace testStack = StackTrace.current;
706
    final _TestImageProvider imageProvider = _TestImageProvider();
707
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
708 709
    // Now remove the listener the error listener is attached to.
    // Don't explicitly remove the error listener.
710
    imageProvider._streamCompleter.removeListener(ImageStreamListener(listener));
711
    late ImageConfiguration configuration;
712
    await tester.pumpWidget(
713
      Builder(
714 715
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
716
          return Container();
717 718 719 720 721 722 723 724 725 726 727
        },
      ),
    );
    imageProvider.resolve(configuration);

    imageProvider.fail(testException, testStack);

    expect(tester.binding.microtaskCount, 1);
    await tester.idle(); // Let the failed completer's future hit the stream completer.
    expect(tester.binding.microtaskCount, 0);

728
    expect(errorListenerCalled, true);
729 730 731 732 733
    expect(reportedException, testException);
    expect(reportedStackTrace, testStack);
    expect(capturedImage, isNull); // The image stream listeners should never be called.
  });

734 735
  testWidgets('Removing listener removes one listener and error listener', (WidgetTester tester) async {
    int errorListenerCalled = 0;
736
    ImageInfo? capturedImage;
737
    void errorListener(dynamic exception, StackTrace? stackTrace) {
738
      errorListenerCalled++;
739 740
    }
    void listener(ImageInfo info, bool synchronous) {
741
      capturedImage = info;
742
    }
743

744
    final Exception testException = Exception('cannot resolve host');
745
    final StackTrace testStack = StackTrace.current;
746
    final _TestImageProvider imageProvider = _TestImageProvider();
747
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
748
    // Duplicates the same set of listener and errorListener.
749
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
750
    // Now remove one entry of the specified listener and associated error listener.
751
    // Don't explicitly remove the error listener.
752
    imageProvider._streamCompleter.removeListener(ImageStreamListener(listener, onError: errorListener));
753
    late ImageConfiguration configuration;
754
    await tester.pumpWidget(
755
      Builder(
756 757
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
758
          return Container();
759 760 761 762 763 764 765 766 767 768 769
        },
      ),
    );
    imageProvider.resolve(configuration);

    imageProvider.fail(testException, testStack);

    expect(tester.binding.microtaskCount, 1);
    await tester.idle(); // Let the failed completer's future hit the stream completer.
    expect(tester.binding.microtaskCount, 0);

770
    expect(errorListenerCalled, 1);
771 772 773
    expect(capturedImage, isNull); // The image stream listeners should never be called.
  });

774
  testWidgets('Image.memory control test', (WidgetTester tester) async {
775
    await tester.pumpWidget(Image.memory(Uint8List.fromList(kTransparentImage), excludeFromSemantics: true));
776
  });
777 778 779

  testWidgets('Image color and colorBlend parameters', (WidgetTester tester) async {
    await tester.pumpWidget(
780
      Image(
781
        excludeFromSemantics: true,
782
        image: _TestImageProvider(),
783
        color: const Color(0xFF00FF00),
784
        colorBlendMode: BlendMode.clear,
785
      ),
786 787 788 789 790
    );
    final RenderImage renderer = tester.renderObject<RenderImage>(find.byType(Image));
    expect(renderer.color, const Color(0xFF00FF00));
    expect(renderer.colorBlendMode, BlendMode.clear);
  });
791

792 793 794 795 796 797 798 799 800 801 802 803 804
  testWidgets('Image opacity parameter', (WidgetTester tester) async {
    const Animation<double> opacity = AlwaysStoppedAnimation<double>(0.5);
    await tester.pumpWidget(
      Image(
        excludeFromSemantics: true,
        image: _TestImageProvider(),
        opacity: opacity,
      ),
    );
    final RenderImage renderer = tester.renderObject<RenderImage>(find.byType(Image));
    expect(renderer.opacity, opacity);
  });

805
  testWidgets('Precache', (WidgetTester tester) async {
806
    final _TestImageProvider provider = _TestImageProvider();
807
    late Future<void> precache;
808
    await tester.pumpWidget(
809
      Builder(
810 811
        builder: (BuildContext context) {
          precache = precacheImage(provider, context);
812
          return Container();
813 814
        },
      ),
815
    );
816
    provider.complete(image10x10);
817 818 819 820 821
    await precache;
    expect(provider._lastResolvedConfiguration, isNotNull);

    // Check that a second resolve of the same image is synchronous.
    final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration);
822
    late bool isSync;
823
    stream.addListener(ImageStreamListener((ImageInfo image, bool sync) { isSync = sync; }));
824 825
    expect(isSync, isTrue);
  });
826

Dan Field's avatar
Dan Field committed
827
  testWidgets('Precache removes original listener immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async {
828 829
    final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider provider = _TestImageProvider(streamCompleter: imageStreamCompleter);
830 831 832 833 834 835

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          precacheImage(provider, context);
          return Container();
836 837
        },
      ),
838 839
    );

840 841 842
    // Two listeners - one is the listener added by precacheImage, the other by the ImageCache.
    final List<ImageStreamListener> listeners = imageStreamCompleter.listeners.toList();
    expect(listeners.length, 2);
843

844
    // Make sure the first listener can be called re-entrantly
845 846 847
    final ImageInfo imageInfo = ImageInfo(image: image10x10);
    listeners[1].onImage(imageInfo.clone(), false);
    listeners[1].onImage(imageInfo.clone(), false);
848

849
    // Make sure the second listener can be called re-entrantly.
850 851
    listeners[0].onImage(imageInfo.clone(), false);
    listeners[0].onImage(imageInfo.clone(), false);
852 853
  });

854 855
  testWidgets('Precache completes with onError on error', (WidgetTester tester) async {
    dynamic capturedException;
856
    StackTrace? capturedStackTrace;
857
    void errorListener(dynamic exception, StackTrace? stackTrace) {
858 859
      capturedException = exception;
      capturedStackTrace = stackTrace;
860
    }
861

862
    final Exception testException = Exception('cannot resolve host');
863
    final StackTrace testStack = StackTrace.current;
864
    final _TestImageProvider imageProvider = _TestImageProvider();
865
    late Future<void> precache;
866
    await tester.pumpWidget(
867
      Builder(
868 869
        builder: (BuildContext context) {
          precache = precacheImage(imageProvider, context, onError: errorListener);
870
          return Container();
871 872
        },
      ),
873 874 875 876 877 878 879 880 881 882 883
    );
    imageProvider.fail(testException, testStack);
    await precache;

    // The image stream error handler should have the original exception.
    expect(capturedException, testException);
    expect(capturedStackTrace, testStack);
    // If there is an error listener, there should be no FlutterError reported.
    expect(tester.takeException(), isNull);
  });

884
  testWidgets('TickerMode controls stream registration', (WidgetTester tester) async {
885
    final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
886
    final Image image = Image(
887
      excludeFromSemantics: true,
888
      image: _TestImageProvider(streamCompleter: imageStreamCompleter),
889 890
    );
    await tester.pumpWidget(
891
      TickerMode(
892 893 894 895
        enabled: true,
        child: image,
      ),
    );
896
    expect(imageStreamCompleter.listeners.length, 2);
897
    await tester.pumpWidget(
898
      TickerMode(
899 900 901 902
        enabled: false,
        child: image,
      ),
    );
903
    expect(imageStreamCompleter.listeners.length, 1);
904 905
  });

906
  testWidgets('Verify Image shows correct RenderImage when changing to an already completed provider', (WidgetTester tester) async {
907
    final GlobalKey key = GlobalKey();
908

909 910
    final _TestImageProvider imageProvider1 = _TestImageProvider();
    final _TestImageProvider imageProvider2 = _TestImageProvider();
911
    final ui.Image image100x100 = (await tester.runAsync(() async => createTestImage(width: 100, height: 100)))!;
912 913

    await tester.pumpWidget(
914
        Container(
915
            key: key,
916
            child: Image(
917
                excludeFromSemantics: true,
918 919
                image: imageProvider1,
            ),
920 921
        ),
        null,
922
        EnginePhase.layout,
923
    );
924
    RenderImage renderImage = key.currentContext!.findRenderObject()! as RenderImage;
925 926
    expect(renderImage.image, isNull);

927 928
    imageProvider1.complete(image10x10);
    imageProvider2.complete(image100x100);
929 930 931
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);

932
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
933 934
    expect(renderImage.image, isNotNull);

935
    final ui.Image oldImage = renderImage.image!;
936 937

    await tester.pumpWidget(
938
        Container(
939
            key: key,
940
            child: Image(
941
              excludeFromSemantics: true,
942 943
              image: imageProvider2,
            ),
944 945
        ),
        null,
946
        EnginePhase.layout,
947 948
    );

949
    renderImage = key.currentContext!.findRenderObject()! as RenderImage;
950 951 952 953 954
    expect(renderImage.image, isNotNull);
    expect(renderImage.image, isNot(equals(oldImage)));
  });

  testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async {
955 956
    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);
957

958
    final Column column = Column(children: <Widget>[image1, image2]);
959 960
    await tester.pumpWidget(column, null, EnginePhase.layout);

961
    final Column columnSwapped = Column(children: <Widget>[image2, image1]);
962 963 964 965 966 967 968 969 970
    await tester.pumpWidget(columnSwapped, null, EnginePhase.layout);

    final List<RenderImage> renderObjects = tester.renderObjectList<RenderImage>(find.byType(Image)).toList();
    expect(renderObjects, hasLength(2));
    expect(renderObjects[0].image, isNotNull);
    expect(renderObjects[0].width, 20.0);
    expect(renderObjects[1].image, isNotNull);
    expect(renderObjects[1].width, 10.0);
  });
971 972

  testWidgets('Image contributes semantics', (WidgetTester tester) async {
973
    final SemanticsTester semantics = SemanticsTester(tester);
974
    await tester.pumpWidget(
975
      Directionality(
976
        textDirection: TextDirection.ltr,
977
        child: Row(
978
          children: <Widget>[
979
            Image(
980
              image: _TestImageProvider(),
981 982 983 984 985 986 987 988 989
              width: 100.0,
              height: 100.0,
              semanticLabel: 'test',
            ),
          ],
        ),
      ),
    );

990
    expect(semantics, hasSemantics(TestSemantics.root(
991
      children: <TestSemantics>[
992
        TestSemantics.rootChild(
993 994
          id: 1,
          label: 'test',
Dan Field's avatar
Dan Field committed
995
          rect: const Rect.fromLTWH(0.0, 0.0, 100.0, 100.0),
996 997
          textDirection: TextDirection.ltr,
          flags: <SemanticsFlag>[SemanticsFlag.isImage],
998
        ),
999
      ],
1000 1001 1002 1003 1004
    ), ignoreTransform: true));
    semantics.dispose();
  });

  testWidgets('Image can exclude semantics', (WidgetTester tester) async {
1005
    final SemanticsTester semantics = SemanticsTester(tester);
1006
    await tester.pumpWidget(
1007
      Directionality(
1008
        textDirection: TextDirection.ltr,
1009
        child: Image(
1010
          image: _TestImageProvider(),
1011 1012 1013 1014 1015 1016 1017
          width: 100.0,
          height: 100.0,
          excludeFromSemantics: true,
        ),
      ),
    );

1018
    expect(semantics, hasSemantics(TestSemantics.root(
1019
      children: <TestSemantics>[],
1020 1021 1022
    )));
    semantics.dispose();
  });
1023 1024

  testWidgets('Image invokes frameBuilder with correct frameNumber argument', (WidgetTester tester) async {
1025
    final ui.Codec codec = (await tester.runAsync(() {
1026
      return ui.instantiateImageCodec(Uint8List.fromList(kAnimatedGif));
1027
    }))!;
1028 1029

    Future<ui.Image> nextFrame() async {
1030
      final ui.FrameInfo frameInfo = (await tester.runAsync(codec.getNextFrame))!;
1031 1032 1033
      return frameInfo.image;
    }

1034 1035
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1036
    int? lastFrame;
1037 1038 1039 1040

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1041
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1042 1043 1044 1045 1046 1047 1048 1049 1050
          lastFrame = frame;
          return Center(child: child);
        },
      ),
    );

    expect(lastFrame, isNull);
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
1051
    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
1052 1053 1054 1055
    await tester.pump();
    expect(lastFrame, 0);
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
1056
    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
1057 1058 1059 1060 1061 1062 1063
    await tester.pump();
    expect(lastFrame, 1);
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
  });

  testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=false', (WidgetTester tester) async {
1064 1065
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1066 1067
    int? lastFrame;
    late bool lastFrameWasSync;
1068 1069 1070 1071

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1072
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1073 1074 1075 1076 1077 1078 1079 1080 1081 1082
          lastFrame = frame;
          lastFrameWasSync = wasSynchronouslyLoaded;
          return child;
        },
      ),
    );

    expect(lastFrame, isNull);
    expect(lastFrameWasSync, isFalse);
    expect(find.byType(RawImage), findsOneWidget);
1083
    streamCompleter.setData(imageInfo: ImageInfo(image: image10x10));
1084 1085 1086 1087 1088 1089
    await tester.pump();
    expect(lastFrame, 0);
    expect(lastFrameWasSync, isFalse);
  });

  testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=true', (WidgetTester tester) async {
1090 1091
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(ImageInfo(image: image10x10.clone()));
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1092 1093
    int? lastFrame;
    late bool lastFrameWasSync;
1094 1095 1096 1097

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1098
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1099 1100 1101 1102 1103 1104 1105 1106 1107 1108
          lastFrame = frame;
          lastFrameWasSync = wasSynchronouslyLoaded;
          return child;
        },
      ),
    );

    expect(lastFrame, 0);
    expect(lastFrameWasSync, isTrue);
    expect(find.byType(RawImage), findsOneWidget);
1109
    streamCompleter.setData(imageInfo: ImageInfo(image: image10x10.clone()));
1110 1111 1112 1113 1114 1115
    await tester.pump();
    expect(lastFrame, 1);
    expect(lastFrameWasSync, isTrue);
  });

  testWidgets('Image state handles frameBuilder update', (WidgetTester tester) async {
1116 1117
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1118 1119 1120 1121

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1122
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134
          return Center(child: child);
        },
      ),
    );

    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
    final State<Image> state = tester.state(find.byType(Image));

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1135
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146
          return Padding(padding: const EdgeInsets.all(1), child: child);
        },
      ),
    );

    expect(find.byType(Center), findsNothing);
    expect(find.byType(Padding), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
    expect(tester.state(find.byType(Image)), same(state));
  });

1147
  testWidgets('Image state handles enabling and disabling of tickers', (WidgetTester tester) async {
1148
    final ui.Codec codec = (await tester.runAsync(() {
1149
      return ui.instantiateImageCodec(Uint8List.fromList(kAnimatedGif));
1150
    }))!;
1151 1152

    Future<ui.Image> nextFrame() async {
1153
      final ui.FrameInfo frameInfo = (await tester.runAsync(codec.getNextFrame))!;
1154 1155 1156
      return frameInfo.image;
    }

1157 1158
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1159
    int? lastFrame;
1160 1161
    int buildCount = 0;

1162
    Widget buildFrame(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219
      lastFrame = frame;
      buildCount++;
      return child;
    }

    await tester.pumpWidget(
      TickerMode(
        enabled: true,
        child: Image(
          image: imageProvider,
          frameBuilder: buildFrame,
        ),
      ),
    );

    final State<Image> state = tester.state(find.byType(Image));
    expect(lastFrame, isNull);
    expect(buildCount, 1);
    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
    await tester.pump();
    expect(lastFrame, 0);
    expect(buildCount, 2);

    await tester.pumpWidget(
      TickerMode(
        enabled: false,
        child: Image(
          image: imageProvider,
          frameBuilder: buildFrame,
        ),
      ),
    );

    expect(tester.state(find.byType(Image)), same(state));
    expect(lastFrame, 0);
    expect(buildCount, 3);
    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
    await tester.pump();
    expect(lastFrame, 0);
    expect(buildCount, 3);

    await tester.pumpWidget(
      TickerMode(
        enabled: true,
        child: Image(
          image: imageProvider,
          frameBuilder: buildFrame,
        ),
      ),
    );

    expect(tester.state(find.byType(Image)), same(state));
    expect(lastFrame, 1); // missed a frame because we weren't animating at the time
    expect(buildCount, 4);
  });

1220
  testWidgets('Image invokes loadingBuilder on chunk event notification', (WidgetTester tester) async {
1221 1222
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1223
    final List<ImageChunkEvent?> chunkEvents = <ImageChunkEvent?>[];
1224 1225 1226 1227

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1228
        loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
1229
          chunkEvents.add(loadingProgress);
1230
          if (loadingProgress == null) {
1231
            return child;
1232
          }
1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243
          return Directionality(
            textDirection: TextDirection.ltr,
            child: Text('loading ${loadingProgress.cumulativeBytesLoaded} / ${loadingProgress.expectedTotalBytes}'),
          );
        },
      ),
    );

    expect(chunkEvents.length, 1);
    expect(chunkEvents.first, isNull);
    expect(tester.binding.hasScheduledFrame, isFalse);
1244
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1245 1246 1247 1248 1249
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(chunkEvents.length, 2);
    expect(find.text('loading 10 / 100'), findsOneWidget);
    expect(find.byType(RawImage), findsNothing);
1250
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 30, expectedTotalBytes: 100));
1251 1252 1253 1254 1255
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(chunkEvents.length, 3);
    expect(find.text('loading 30 / 100'), findsOneWidget);
    expect(find.byType(RawImage), findsNothing);
1256
    streamCompleter.setData(imageInfo: ImageInfo(image: image10x10));
1257 1258 1259 1260
    await tester.pump();
    expect(chunkEvents.length, 4);
    expect(find.byType(Text), findsNothing);
    expect(find.byType(RawImage), findsOneWidget);
1261
  });
1262

1263
  testWidgets("Image doesn't rebuild on chunk events if loadingBuilder is null", (WidgetTester tester) async {
1264 1265
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1266 1267 1268 1269 1270 1271 1272 1273 1274

    await tester.pumpWidget(
      Image(
        image: imageProvider,
        excludeFromSemantics: true,
      ),
    );

    expect(tester.binding.hasScheduledFrame, isFalse);
1275
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1276
    expect(tester.binding.hasScheduledFrame, isFalse);
1277
    streamCompleter.setData(imageInfo: ImageInfo(image: image10x10));
1278 1279
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
1280
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1281 1282 1283 1284 1285
    expect(tester.binding.hasScheduledFrame, isFalse);
    expect(find.byType(RawImage), findsOneWidget);
  });

  testWidgets('Image chains the results of frameBuilder and loadingBuilder', (WidgetTester tester) async {
1286 1287
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1288 1289 1290 1291 1292

    await tester.pumpWidget(
      Image(
        image: imageProvider,
        excludeFromSemantics: true,
1293
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1294 1295
          return Padding(padding: const EdgeInsets.all(1), child: child);
        },
1296
        loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
1297 1298 1299 1300 1301 1302 1303 1304
          return Center(child: child);
        },
      ),
    );

    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(Padding), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
Dan Field's avatar
Dan Field committed
1305
    expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>());
1306
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1307 1308 1309 1310
    await tester.pump();
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(Padding), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
Dan Field's avatar
Dan Field committed
1311 1312
    expect(tester.widget<Center>(find.byType(Center)).child, isA<Padding>());
    expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>());
1313
  });
1314 1315

  testWidgets('Image state handles loadingBuilder update from null to non-null', (WidgetTester tester) async {
1316 1317
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1318 1319 1320 1321 1322 1323

    await tester.pumpWidget(
      Image(image: imageProvider),
    );

    expect(find.byType(RawImage), findsOneWidget);
1324
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1325 1326 1327 1328 1329 1330
    expect(tester.binding.hasScheduledFrame, isFalse);
    final State<Image> state = tester.state(find.byType(Image));

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1331
        loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
1332 1333 1334 1335 1336 1337 1338 1339
          return Center(child: child);
        },
      ),
    );

    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
    expect(tester.state(find.byType(Image)), same(state));
1340
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1341 1342 1343 1344
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
1345
  });
1346 1347

  testWidgets('Image state handles loadingBuilder update from non-null to null', (WidgetTester tester) async {
1348 1349
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1350 1351 1352 1353

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1354
        loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
1355 1356 1357 1358 1359 1360 1361
          return Center(child: child);
        },
      ),
    );

    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
1362
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
    final State<Image> state = tester.state(find.byType(Image));

    await tester.pumpWidget(
      Image(image: imageProvider),
    );

    expect(find.byType(Center), findsNothing);
    expect(find.byType(RawImage), findsOneWidget);
    expect(tester.state(find.byType(Image)), same(state));
1376
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1377
    expect(tester.binding.hasScheduledFrame, isFalse);
1378
  });
1379

1380 1381
  testWidgets('Verify Image resets its ImageListeners', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
1382 1383
    final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter);
1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395
    await tester.pumpWidget(
      Container(
        key: key,
        child: Image(
          image: imageProvider1,
        ),
      ),
    );
    // listener from resolveStreamForKey is always added.
    expect(imageStreamCompleter.listeners.length, 2);


1396
    final _TestImageProvider imageProvider2 = _TestImageProvider();
1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414
    await tester.pumpWidget(
      Container(
        key: key,
        child: Image(
          image: imageProvider2,
          excludeFromSemantics: true,
        ),
      ),
      null,
      EnginePhase.layout,
    );

    // only listener from resolveStreamForKey is left.
    expect(imageStreamCompleter.listeners.length, 1);
  });

  testWidgets('Verify Image resets its ErrorListeners', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
1415 1416
    final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter);
1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429
    await tester.pumpWidget(
      Container(
        key: key,
        child: Image(
          image: imageProvider1,
          errorBuilder: (_,__,___) => Container(),
        ),
      ),
    );
    // listener from resolveStreamForKey is always added.
    expect(imageStreamCompleter.listeners.length, 2);


1430
    final _TestImageProvider imageProvider2 = _TestImageProvider();
1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446
    await tester.pumpWidget(
      Container(
        key: key,
        child: Image(
          image: imageProvider2,
          excludeFromSemantics: true,
        ),
      ),
      null,
      EnginePhase.layout,
    );

    // only listener from resolveStreamForKey is left.
    expect(imageStreamCompleter.listeners.length, 1);
  });

1447 1448
  testWidgets('Image defers loading while fast scrolling', (WidgetTester tester) async {
    const int gridCells = 1000;
1449
    final List<_TestImageProvider> imageProviders = <_TestImageProvider>[];
1450 1451 1452 1453 1454 1455 1456 1457
    final ScrollController controller = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: GridView.builder(
        controller: controller,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
        itemCount: gridCells,
        itemBuilder: (_, int index) {
1458
          final _TestImageProvider provider = _TestImageProvider();
1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471
          imageProviders.add(provider);
          return SizedBox(
            height: 250,
            width: 250,
            child: Image(
              image: provider,
              semanticLabel: index.toString(),
            ),
          );
        },
      ),
    ));

1472 1473
    bool loadCalled(_TestImageProvider provider) => provider.loadCalled;
    bool loadNotCalled(_TestImageProvider provider) => !provider.loadCalled;
1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493

    expect(find.bySemanticsLabel('5'), findsOneWidget);
    expect(imageProviders.length, 12);
    expect(imageProviders.every(loadCalled), true);

    imageProviders.clear();

    // Simulate a very fast fling.
    controller.animateTo(
      30000,
      duration: const Duration(seconds: 2),
      curve: Curves.linear,
    );
    await tester.pumpAndSettle();
    // The last 15 images on screen have loaded because the scrolling settled there.
    // The rest have not loaded.
    expect(imageProviders.length, 309);
    expect(imageProviders.skip(309 - 15).every(loadCalled), true);
    expect(imageProviders.take(309 - 15).every(loadNotCalled), true);
  });
Dan Field's avatar
Dan Field committed
1494 1495

  testWidgets('Same image provider in multiple parts of the tree, no cache room left', (WidgetTester tester) async {
1496
    imageCache.maximumSize = 0;
Dan Field's avatar
Dan Field committed
1497

1498 1499
    final _TestImageProvider provider1 = _TestImageProvider();
    final _TestImageProvider provider2 = _TestImageProvider();
Dan Field's avatar
Dan Field committed
1500 1501 1502

    expect(provider1.loadCallCount, 0);
    expect(provider2.loadCallCount, 0);
1503
    expect(imageCache.liveImageCount, 0);
Dan Field's avatar
Dan Field committed
1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514

    await tester.pumpWidget(Column(
      children: <Widget>[
        Image(image: provider1),
        Image(image: provider2),
        Image(image: provider1),
        Image(image: provider1),
        Image(image: provider2),
      ],
    ));

1515 1516 1517 1518 1519 1520 1521
    expect(imageCache.liveImageCount, 2);
    expect(imageCache.statusForKey(provider1).live, true);
    expect(imageCache.statusForKey(provider1).pending, false);
    expect(imageCache.statusForKey(provider1).keepAlive, false);
    expect(imageCache.statusForKey(provider2).live, true);
    expect(imageCache.statusForKey(provider2).pending, false);
    expect(imageCache.statusForKey(provider2).keepAlive, false);
Dan Field's avatar
Dan Field committed
1522 1523 1524 1525

    expect(provider1.loadCallCount, 1);
    expect(provider2.loadCallCount, 1);

1526
    provider1.complete(image10x10.clone());
Dan Field's avatar
Dan Field committed
1527 1528
    await tester.idle();

1529
    provider2.complete(image10x10.clone());
Dan Field's avatar
Dan Field committed
1530 1531
    await tester.idle();

1532 1533
    expect(imageCache.liveImageCount, 2);
    expect(imageCache.currentSize, 0);
Dan Field's avatar
Dan Field committed
1534 1535 1536

    await tester.pumpWidget(Image(image: provider2));
    await tester.idle();
1537 1538 1539 1540 1541
    expect(imageCache.statusForKey(provider1).untracked, true);
    expect(imageCache.statusForKey(provider2).live, true);
    expect(imageCache.statusForKey(provider2).pending, false);
    expect(imageCache.statusForKey(provider2).keepAlive, false);
    expect(imageCache.liveImageCount, 1);
Dan Field's avatar
Dan Field committed
1542 1543 1544 1545 1546

    await tester.pumpWidget(const SizedBox());
    await tester.idle();
    expect(provider1.loadCallCount, 1);
    expect(provider2.loadCallCount, 1);
1547
    expect(imageCache.liveImageCount, 0);
Dan Field's avatar
Dan Field committed
1548 1549 1550
  });

  testWidgets('precacheImage does not hold weak ref for more than a frame', (WidgetTester tester) async {
1551
    imageCache.maximumSize = 0;
1552
    final _TestImageProvider provider = _TestImageProvider();
1553
    late Future<void> precache;
Dan Field's avatar
Dan Field committed
1554 1555 1556 1557 1558
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          precache = precacheImage(provider, context);
          return Container();
1559 1560
        },
      ),
Dan Field's avatar
Dan Field committed
1561
    );
1562
    provider.complete(image10x10);
Dan Field's avatar
Dan Field committed
1563 1564 1565
    await precache;

    // Should have ended up with only a weak ref, not in cache because cache size is 0
1566 1567
    expect(imageCache.liveImageCount, 1);
    expect(imageCache.containsKey(provider), false);
Dan Field's avatar
Dan Field committed
1568

1569
    final ImageCacheStatus providerLocation = (await provider.obtainCacheStatus(configuration: ImageConfiguration.empty))!;
Dan Field's avatar
Dan Field committed
1570 1571 1572 1573 1574 1575 1576 1577 1578

    expect(providerLocation, isNotNull);
    expect(providerLocation.live, true);
    expect(providerLocation.keepAlive, false);
    expect(providerLocation.pending, false);

    // Check that a second resolve of the same image is synchronous.
    expect(provider._lastResolvedConfiguration, isNotNull);
    final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration);
1579
    late bool isSync;
Dan Field's avatar
Dan Field committed
1580 1581 1582 1583
    final ImageStreamListener listener = ImageStreamListener((ImageInfo image, bool syncCall) { isSync = syncCall; });

    // Still have live ref because frame has not pumped yet.
    await tester.pump();
1584
    expect(imageCache.liveImageCount, 1);
Dan Field's avatar
Dan Field committed
1585

1586
    SchedulerBinding.instance.scheduleFrame();
Dan Field's avatar
Dan Field committed
1587 1588
    await tester.pump();
    // Live ref should be gone - we didn't listen to the stream.
1589 1590
    expect(imageCache.liveImageCount, 0);
    expect(imageCache.currentSize, 0);
Dan Field's avatar
Dan Field committed
1591 1592 1593 1594

    stream.addListener(listener);
    expect(isSync, true); // because the stream still has the image.

1595 1596
    expect(imageCache.liveImageCount, 0);
    expect(imageCache.currentSize, 0);
Dan Field's avatar
Dan Field committed
1597 1598 1599 1600

    expect(provider.loadCallCount, 1);
  });

1601
  testWidgets('precacheImage allows time to take over weak reference', (WidgetTester tester) async {
1602
    final _TestImageProvider provider = _TestImageProvider();
1603
    late Future<void> precache;
Dan Field's avatar
Dan Field committed
1604 1605 1606 1607 1608
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          precache = precacheImage(provider, context);
          return Container();
1609 1610
        },
      ),
Dan Field's avatar
Dan Field committed
1611
    );
1612
    provider.complete(image10x10);
Dan Field's avatar
Dan Field committed
1613 1614 1615
    await precache;

    // Should have ended up in the cache and have a weak reference.
1616 1617 1618
    expect(imageCache.liveImageCount, 1);
    expect(imageCache.currentSize, 1);
    expect(imageCache.containsKey(provider), true);
Dan Field's avatar
Dan Field committed
1619 1620 1621 1622

    // Check that a second resolve of the same image is synchronous.
    expect(provider._lastResolvedConfiguration, isNotNull);
    final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration);
1623
    late bool isSync;
Dan Field's avatar
Dan Field committed
1624 1625 1626
    final ImageStreamListener listener = ImageStreamListener((ImageInfo image, bool syncCall) { isSync = syncCall; });

    // Should have ended up in the cache and still have a weak reference.
1627 1628 1629
    expect(imageCache.liveImageCount, 1);
    expect(imageCache.currentSize, 1);
    expect(imageCache.containsKey(provider), true);
Dan Field's avatar
Dan Field committed
1630 1631 1632 1633

    stream.addListener(listener);
    expect(isSync, true);

1634 1635 1636
    expect(imageCache.liveImageCount, 1);
    expect(imageCache.currentSize, 1);
    expect(imageCache.containsKey(provider), true);
Dan Field's avatar
Dan Field committed
1637

1638
    SchedulerBinding.instance.scheduleFrame();
Dan Field's avatar
Dan Field committed
1639 1640
    await tester.pump();

1641 1642 1643
    expect(imageCache.liveImageCount, 1);
    expect(imageCache.currentSize, 1);
    expect(imageCache.containsKey(provider), true);
Dan Field's avatar
Dan Field committed
1644 1645
    stream.removeListener(listener);

1646 1647 1648
    expect(imageCache.liveImageCount, 0);
    expect(imageCache.currentSize, 1);
    expect(imageCache.containsKey(provider), true);
Dan Field's avatar
Dan Field committed
1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668
    expect(provider.loadCallCount, 1);
  });

  testWidgets('evict an image during precache', (WidgetTester tester) async {
    // This test checks that the live image tracking does not hold on to a
    // pending image that will never complete because it has been evicted from
    // the cache.
    // The scenario may arise in a test harness that is trying to load real
    // images using `tester.runAsync()`, and wants to make sure that widgets
    // under test have not also tried to resolve the image in a FakeAsync zone.
    // The image loaded in the FakeAsync zone will never complete, and the
    // runAsync call wants to make sure it gets a load attempt from the correct
    // zone.
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final MemoryImage provider = MemoryImage(bytes);

    await tester.runAsync(() async {
      final List<Future<void>> futures = <Future<void>>[];
      await tester.pumpWidget(Builder(builder: (BuildContext context) {
        futures.add(precacheImage(provider, context));
1669
        imageCache.evict(provider);
Dan Field's avatar
Dan Field committed
1670 1671 1672 1673
        futures.add(precacheImage(provider, context));
        return const SizedBox.expand();
      }));
      await Future.wait<void>(futures);
1674 1675
      expect(imageCache.statusForKey(provider).keepAlive, true);
      expect(imageCache.statusForKey(provider).live, true);
Dan Field's avatar
Dan Field committed
1676 1677

      // Schedule a frame to get precacheImage to stop listening.
1678
      SchedulerBinding.instance.scheduleFrame();
Dan Field's avatar
Dan Field committed
1679
      await tester.pump();
1680 1681
      expect(imageCache.statusForKey(provider).keepAlive, true);
      expect(imageCache.statusForKey(provider).live, false);
Dan Field's avatar
Dan Field committed
1682 1683
    });
  });
1684 1685 1686

  testWidgets('errorBuilder - fails on key', (WidgetTester tester) async {
    final UniqueKey errorKey = UniqueKey();
1687
    late Object caughtException;
1688 1689
    await tester.pumpWidget(
      Image(
1690
        image: _FailingImageProvider(failOnObtainKey: true, throws: 'threw', image: image10x10),
1691
        errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706
          caughtException = error;
          return SizedBox.expand(key: errorKey);
        },
      ),
    );

    await tester.pump();

    expect(find.byKey(errorKey), findsOneWidget);
    expect(caughtException.toString(), 'threw');
    expect(tester.takeException(), isNull);
  });

  testWidgets('errorBuilder - fails on load', (WidgetTester tester) async {
    final UniqueKey errorKey = UniqueKey();
1707
    late Object caughtException;
1708 1709
    await tester.pumpWidget(
      Image(
1710
        image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
1711
        errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726
          caughtException = error;
          return SizedBox.expand(key: errorKey);
        },
      ),
    );

    await tester.pump();

    expect(find.byKey(errorKey), findsOneWidget);
    expect(caughtException.toString(), 'threw');
    expect(tester.takeException(), isNull);
  });

  testWidgets('no errorBuilder - failure reported to FlutterError', (WidgetTester tester) async {
    await tester.pumpWidget(
1727
      Image(
1728
        image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
1729 1730 1731 1732 1733 1734 1735
      ),
    );

    await tester.pump();

    expect(tester.takeException(), 'threw');
  });
1736

1737
  Future<void> testRotatedImage(WidgetTester tester, bool isAntiAlias) async {
1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767
    final Key key = UniqueKey();
    await tester.pumpWidget(RepaintBoundary(
      key: key,
      child: Transform.rotate(
        angle: math.pi / 180,
        child: Image.memory(Uint8List.fromList(kBlueRectPng), isAntiAlias: isAntiAlias),
      ),
    ));

    // precacheImage is needed, or the image in the golden file will be empty.
    if (!kIsWeb) {
      final Finder allImages = find.byType(Image);
      for (final Element e in allImages.evaluate()) {
        await tester.runAsync(() async {
          final Image image = e.widget as Image;
          await precacheImage(image.image, e);
        });
      }
      await tester.pumpAndSettle();
    }

    await expectLater(
      find.byKey(key),
      matchesGoldenFile('rotated_image_${isAntiAlias ? 'aa' : 'noaa'}.png'),
    );
  }

  testWidgets(
    'Rotated images',
    (WidgetTester tester) async {
1768 1769
      await testRotatedImage(tester, true);
      await testRotatedImage(tester, false);
1770
    },
1771
    skip: kIsWeb, // https://github.com/flutter/flutter/issues/87933.
1772
  );
1773

1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820
  testWidgets(
    'Image opacity',
    (WidgetTester tester) async {
      final Key key = UniqueKey();
      await tester.pumpWidget(RepaintBoundary(
        key: key,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          textDirection: TextDirection.ltr,
          children: <Widget>[
            Image.memory(
              Uint8List.fromList(kBlueRectPng),
              opacity: const AlwaysStoppedAnimation<double>(0.25),
            ),
            Image.memory(
              Uint8List.fromList(kBlueRectPng),
              opacity: const AlwaysStoppedAnimation<double>(0.5),
            ),
            Image.memory(
              Uint8List.fromList(kBlueRectPng),
              opacity: const AlwaysStoppedAnimation<double>(0.75),
            ),
            Image.memory(
              Uint8List.fromList(kBlueRectPng),
              opacity: const AlwaysStoppedAnimation<double>(1.0),
            ),
          ],
        ),
      ));

      // precacheImage is needed, or the image in the golden file will be empty.
      if (!kIsWeb) {
        final Finder allImages = find.byType(Image);
        for (final Element e in allImages.evaluate()) {
          await tester.runAsync(() async {
            final Image image = e.widget as Image;
            await precacheImage(image.image, e);
          });
        }
        await tester.pumpAndSettle();
      }

      await expectLater(
        find.byKey(key),
        matchesGoldenFile('transparent_image.png'),
      );
    },
1821
    skip: kIsWeb, // https://github.com/flutter/flutter/issues/87933.
1822 1823
  );

1824
  testWidgets('Reports image size when painted', (WidgetTester tester) async {
1825
    late ImageSizeInfo imageSizeInfo;
1826 1827 1828 1829 1830 1831
    int count = 0;
    debugOnPaintImage = (ImageSizeInfo info) {
      count += 1;
      imageSizeInfo = info;
    };

1832
    final ui.Image image = (await tester.runAsync(() => createTestImage(width: 100, height: 100)))!;
1833
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter(
1834 1835 1836 1837 1838
      ImageInfo(
        image: image,
        debugLabel: 'test.png',
      ),
    );
1839
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856

    await tester.pumpWidget(
      Center(
        child: SizedBox(
          height: 50,
          width: 50,
          child: Image(image: imageProvider),
        ),
      ),
    );

    expect(count, 1);
    expect(
      imageSizeInfo,
      const ImageSizeInfo(
        source: 'test.png',
        imageSize: Size(100, 100),
1857
        displaySize: Size(150, 150),
1858 1859 1860 1861 1862
      ),
    );

    debugOnPaintImage = null;
  });
1863

1864
  testWidgets('Disposes image handle when disposed', (WidgetTester tester) async {
1865
    final ui.Image image = (await tester.runAsync(() => createTestImage(cache: false)))!;
1866

1867
    expect(image.debugGetOpenHandleStackTraces()!.length, 1);
1868

1869
    final ImageProvider provider = _TestImageProvider(
1870 1871 1872 1873
      streamCompleter: OneFrameImageStreamCompleter(
        Future<ImageInfo>.value(
          ImageInfo(
            image: image,
1874
            debugLabel: '_TestImage',
1875 1876 1877 1878 1879 1880 1881
          ),
        ),
      ),
    );

    // creating the provider should not have changed anything, and the provider
    // now owns the handle.
1882
    expect(image.debugGetOpenHandleStackTraces()!.length, 1);
1883 1884 1885 1886

    await tester.pumpWidget(Image(image: provider));

    // Image widget + 1, render object + 1
1887
    expect(image.debugGetOpenHandleStackTraces()!.length, 3);
1888 1889 1890 1891

    await tester.pumpWidget(const SizedBox());

    // Image widget and render object go away
1892
    expect(image.debugGetOpenHandleStackTraces()!.length, 1);
1893 1894 1895 1896 1897 1898 1899 1900

    await provider.evict();

    tester.binding.scheduleFrame();
    await tester.pump();

    // Image cache listener go away and Image stream listeners go away.
    // Image is now at zero.
1901
    expect(image.debugGetOpenHandleStackTraces()!.length, 0);
1902
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
1903 1904

  testWidgets('Keeps stream alive when ticker mode is disabled',  (WidgetTester tester) async {
1905
    imageCache.maximumSize = 0;
1906
    final ui.Image image = (await tester.runAsync(() => createTestImage(cache: false)))!;
1907
    final _TestImageProvider provider = _TestImageProvider();
1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932
    provider.complete(image);

    await tester.pumpWidget(
      TickerMode(
        enabled: true,
        child: Image(image: provider),
      ),
    );
    expect(find.byType(Image), findsOneWidget);

    await tester.pumpWidget(TickerMode(
        enabled: false,
        child: Image(image: provider),
      ),
    );
    expect(find.byType(Image), findsOneWidget);

    await tester.pumpWidget(TickerMode(
        enabled: true,
        child: Image(image: provider),
      ),
    );
    expect(find.byType(Image), findsOneWidget);
  });

1933 1934
  testWidgets('Load a good image after a bad image was loaded should not call errorBuilder', (WidgetTester tester) async {
    final UniqueKey errorKey = UniqueKey();
1935
    final ui.Image image = (await tester.runAsync(() => createTestImage()))!;
1936 1937
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1938 1939 1940 1941 1942 1943 1944 1945 1946

    await tester.pumpWidget(
      Center(
        child: SizedBox(
          height: 50,
          width: 50,
          child: Image(
            image: imageProvider,
            excludeFromSemantics: true,
1947
            errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
1948 1949
              return Container(key: errorKey);
            },
1950
            frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977
              return Padding(padding: const EdgeInsets.all(1), child: child);
            },
          ),
        ),
      ),
    );

    // No error widget before loading a invalid image.
    expect(find.byKey(errorKey), findsNothing);

    // Loading good image succeed
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
    await tester.pump();
    expect(find.byType(Padding), findsOneWidget);

    // Loading bad image shows the error widget.
    streamCompleter.setError(exception: 'thrown');
    await tester.pump();
    expect(find.byKey(errorKey), findsOneWidget);

    // Loading good image shows the image widget instead of the error widget.
    streamCompleter.setData(imageInfo: ImageInfo(image: image));
    await tester.pump();
    expect(find.byType(Padding), findsOneWidget);
    expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>());
    expect(find.byKey(errorKey), findsNothing);
  });
1978

1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993
  testWidgets('Failed image loads in debug mode', (WidgetTester tester) async {
    final Key key = UniqueKey();
    await tester.pumpWidget(Center(
      child: RepaintBoundary(
        key: key,
        child: Container(
          width: 150.0,
          height: 50.0,
          decoration: BoxDecoration(
            border: Border.all(
              width: 2.0,
              color: const Color(0xFF00FF99),
            ),
          ),
          child: Image.asset('missing-asset'),
1994
        ),
1995 1996 1997 1998 1999 2000
      ),
    ));
    await expectLater(
      find.byKey(key),
      matchesGoldenFile('image_test.missing.1.png'),
    );
2001 2002 2003 2004 2005 2006 2007
    expect(
      tester.takeException().toString(),
      equals(
        'Unable to load asset: "missing-asset".\n'
        'The asset does not exist or has empty data.',
      ),
    );
2008 2009 2010 2011 2012 2013
    await tester.pump();
    await expectLater(
      find.byKey(key),
      matchesGoldenFile('image_test.missing.2.png'),
    );
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/74935 (broken assets not being reported on web)
2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029

  testWidgets('Image.file throws a non-implemented error on web', (WidgetTester tester) async {
    const String expectedError =
      'Image.file is not supported on Flutter Web. '
      'Consider using either Image.asset or Image.network instead.';
    final Uri uri = Uri.parse('/home/flutter/dash.png');
    final File file = File.fromUri(uri);
    expect(
      () => Image.file(file),
      kIsWeb
        // Web does not support file access, expect AssertionError
        ? throwsA(predicate((AssertionError e) => e.message == expectedError))
        // AOT supports file access, expect constructor to succeed
        : isNot(throwsA(anything)),
    );
  });
2030 2031
}

2032
@immutable
2033
class _ConfigurationAwareKey {
2034
  const _ConfigurationAwareKey(this.provider, this.configuration);
2035 2036 2037 2038 2039 2040 2041 2042 2043

  final ImageProvider provider;
  final ImageConfiguration configuration;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
2044
    return other is _ConfigurationAwareKey
2045 2046 2047 2048 2049
        && other.provider == provider
        && other.configuration == configuration;
  }

  @override
2050
  int get hashCode => Object.hash(provider, configuration);
2051 2052
}

2053
class _ConfigurationKeyedTestImageProvider extends _TestImageProvider {
2054
  @override
2055 2056
  Future<_ConfigurationAwareKey> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<_ConfigurationAwareKey>(_ConfigurationAwareKey(this, configuration));
2057 2058 2059
  }
}

2060 2061
class _TestImageProvider extends ImageProvider<Object> {
  _TestImageProvider({ImageStreamCompleter? streamCompleter}) {
2062
    _streamCompleter = streamCompleter
2063
      ?? OneFrameImageStreamCompleter(_completer.future);
2064 2065
  }

2066
  final Completer<ImageInfo> _completer = Completer<ImageInfo>();
2067 2068
  late ImageStreamCompleter _streamCompleter;
  late ImageConfiguration _lastResolvedConfiguration;
2069

Dan Field's avatar
Dan Field committed
2070 2071 2072
  bool get loadCalled => _loadCallCount > 0;
  int get loadCallCount => _loadCallCount;
  int _loadCallCount = 0;
2073

2074
  @override
2075
  Future<Object> obtainKey(ImageConfiguration configuration) {
2076
    return SynchronousFuture<_TestImageProvider>(this);
2077 2078
  }

2079
  @override
2080
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, Object key, ImageErrorListener handleError) {
2081
    _lastResolvedConfiguration = configuration;
2082
    super.resolveStreamForKey(configuration, stream, key, handleError);
2083 2084
  }

2085
  @override
2086
  ImageStreamCompleter load(Object key, DecoderCallback decode) {
Dan Field's avatar
Dan Field committed
2087
    _loadCallCount += 1;
2088 2089
    return _streamCompleter;
  }
2090

2091
  void complete(ui.Image image) {
Dan Field's avatar
Dan Field committed
2092
    _completer.complete(ImageInfo(image: image));
2093
  }
2094

2095
  void fail(Object exception, StackTrace? stackTrace) {
2096 2097 2098
    _completer.completeError(exception, stackTrace);
  }

2099
  @override
2100
  String toString() => '${describeIdentity(this)}()';
2101 2102
}

2103 2104
class _TestImageStreamCompleter extends ImageStreamCompleter {
  _TestImageStreamCompleter([this._currentImage]);
2105

2106
  ImageInfo? _currentImage;
2107
  final Set<ImageStreamListener> listeners = <ImageStreamListener>{};
2108

2109
  @override
2110 2111
  void addListener(ImageStreamListener listener) {
    listeners.add(listener);
2112
    if (_currentImage != null) {
2113
      listener.onImage(_currentImage!.clone(), true);
2114
    }
2115 2116 2117
  }

  @override
2118
  void removeListener(ImageStreamListener listener) {
2119 2120
    listeners.remove(listener);
  }
2121

2122
  void setData({
2123 2124
    ImageInfo? imageInfo,
    ImageChunkEvent? chunkEvent,
2125
  }) {
2126
    if (imageInfo != null) {
2127
      _currentImage?.dispose();
2128 2129
      _currentImage = imageInfo;
    }
2130
    final List<ImageStreamListener> localListeners = listeners.toList();
2131
    for (final ImageStreamListener listener in localListeners) {
2132
      if (imageInfo != null) {
2133
        listener.onImage(imageInfo.clone(), false);
2134 2135
      }
      if (chunkEvent != null && listener.onChunk != null) {
2136
        listener.onChunk!(chunkEvent);
2137 2138 2139
      }
    }
  }
2140 2141

  void setError({
2142 2143
    required Object exception,
    StackTrace? stackTrace,
2144 2145 2146
  }) {
    final List<ImageStreamListener> localListeners = listeners.toList();
    for (final ImageStreamListener listener in localListeners) {
2147
      listener.onError?.call(exception, stackTrace);
2148 2149
    }
  }
2150 2151
}

2152 2153
class _DebouncingImageProvider extends ImageProvider<Object> {
  _DebouncingImageProvider(this.imageProvider, this.seenKeys);
2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175

  /// A set of keys that will only get resolved the _first_ time they are seen.
  ///
  /// If an ImageProvider produces the same key for two different image
  /// configurations, it should only actually resolve once using this provider.
  /// However, if it does care about image configuration, it should make the
  /// property or properties it cares about part of the key material it
  /// produces.
  final Set<Object> seenKeys;
  final ImageProvider<Object> imageProvider;

  @override
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, Object key, ImageErrorListener handleError) {
    if (seenKeys.add(key)) {
      imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
    }
  }

  @override
  Future<Object> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);

  @override
2176
  ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) => imageProvider.loadImage(key, decode);
2177
}
2178

2179 2180
class _FailingImageProvider extends ImageProvider<int> {
  const _FailingImageProvider({
2181 2182
    this.failOnObtainKey = false,
    this.failOnLoad = false,
2183 2184
    required this.throws,
    required this.image,
2185
  }) : assert(failOnLoad == true || failOnObtainKey == true);
2186 2187 2188 2189

  final bool failOnObtainKey;
  final bool failOnLoad;
  final Object throws;
2190
  final ui.Image image;
2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207

  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
    if (failOnObtainKey) {
      throw throws;
    }
    return SynchronousFuture<int>(hashCode);
  }

  @override
  ImageStreamCompleter load(int key, DecoderCallback decode) {
    if (failOnLoad) {
      throw throws;
    }
    return OneFrameImageStreamCompleter(
      Future<ImageInfo>.value(
        ImageInfo(
2208
          image: image,
2209 2210 2211 2212 2213 2214
          scale: 0,
        ),
      ),
    );
  }
}