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

5 6 7 8
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])

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

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

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

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

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

  tearDown(() {
35
    imageCache.maximumSize = originalCacheSize;
Dan Field's avatar
Dan Field committed
36 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
  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);
  });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // 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(
281
      MediaQuery(
282
        key: mediaQueryKey2,
283
        data: const MediaQueryData(
284 285
          devicePixelRatio: 5.0,
        ),
286
        child: MediaQuery(
287
          key: mediaQueryKey1,
288
          data: const MediaQueryData(
289 290
            devicePixelRatio: 10.0,
          ),
291
          child: Image(
292
            excludeFromSemantics: true,
293
            key: imageKey,
294
            image: debouncingProvider,
295
          ),
296
        ),
297
      ),
298 299
    );

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

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

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

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

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

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

370 371 372 373
  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');
374
    final _TestImageProvider imageProvider = _TestImageProvider();
375
    final Set<Object> seenKeys = <Object>{};
376
    final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
377 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

    // 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');
432
    final _TestImageProvider imageProvider = _TestImageProvider();
433
    final Set<Object> seenKeys = <Object>{};
434
    final _DebouncingImageProvider debouncingProvider = _DebouncingImageProvider(imageProvider, seenKeys);
435 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

    // 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);
  });

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

500
    final _TestImageProvider imageProvider = _TestImageProvider();
501
    await tester.pumpWidget(Image(image: imageProvider, excludeFromSemantics: true));
502
    final State<Image> image = tester.state/*State<Image>*/(find.byType(Image));
503
    expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null, loadingProgress: null, frameNumber: null, wasSynchronouslyLoaded: false)'));
504
    imageProvider.complete(image100x100);
505
    await tester.pump();
506
    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)'));
507
    await tester.pumpWidget(Container());
508
    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)'));
509 510
  });

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

523
    final Exception testException = Exception('cannot resolve host');
524
    final StackTrace testStack = StackTrace.current;
525
    final _TestImageProvider imageProvider = _TestImageProvider();
526
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
527
    late ImageConfiguration configuration;
528
    await tester.pumpWidget(
529
      Builder(
530 531
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
532
          return Container();
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
        },
      ),
    );
    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;
553
    StackTrace? capturedStackTrace;
554
    dynamic reportedException;
555 556
    StackTrace? reportedStackTrace;
    ImageInfo? capturedImage;
557
    void errorListener(dynamic exception, StackTrace? stackTrace) {
558 559
      capturedException = exception;
      capturedStackTrace = stackTrace;
560 561
    }
    void listener(ImageInfo info, bool synchronous) {
562
      capturedImage = info;
563
    }
564 565 566 567 568
    FlutterError.onError = (FlutterErrorDetails flutterError) {
      reportedException = flutterError.exception;
      reportedStackTrace = flutterError.stack;
    };

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

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

    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;
604 605
    StackTrace? capturedStackTrace;
    ImageInfo? capturedImage;
606
    void errorListener(dynamic exception, StackTrace? stackTrace) {
607 608
      capturedException = exception;
      capturedStackTrace = stackTrace;
609 610
    }
    void listener(ImageInfo info, bool synchronous) {
611
      capturedImage = info;
612
    }
613

614
    final Exception testException = Exception('cannot resolve host');
615
    final StackTrace testStack = StackTrace.current;
616
    final _TestImageProvider imageProvider = _TestImageProvider();
617
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
618
    // Add the exact same listener a second time without the errorListener.
619
    imageProvider._streamCompleter.addListener(ImageStreamListener(listener));
620
    late ImageConfiguration configuration;
621
    await tester.pumpWidget(
622
      Builder(
623 624
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
625
          return Container();
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
        },
      ),
    );
    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;
646 647
    StackTrace? capturedStackTrace;
    ImageInfo? capturedImage;
648
    int errorListenerCalled = 0;
649
    void errorListener(dynamic exception, StackTrace? stackTrace) {
650 651 652
      capturedException = exception;
      capturedStackTrace = stackTrace;
      errorListenerCalled++;
653 654
    }
    void listener(ImageInfo info, bool synchronous) {
655
      capturedImage = info;
656
    }
657

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

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

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

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

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

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

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

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

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

791 792 793 794 795 796 797 798 799 800 801 802 803
  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);
  });

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

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

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

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

839 840 841
    // 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);
842

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

960
    final Column columnSwapped = Column(children: <Widget>[image2, image1]);
961 962 963 964 965 966 967 968 969
    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);
  });
970 971

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

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

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

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

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

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

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

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

    expect(lastFrame, isNull);
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
1050
    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
1051 1052 1053 1054
    await tester.pump();
    expect(lastFrame, 0);
    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
1055
    streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
1056 1057 1058 1059 1060 1061 1062
    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 {
1063 1064
    final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
1065 1066
    int? lastFrame;
    late bool lastFrameWasSync;
1067 1068 1069 1070

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

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

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

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

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

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

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1121
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
          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,
1134
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
          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));
  });

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

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

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

1161
    Widget buildFrame(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1162 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
      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);
  });

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

    await tester.pumpWidget(
      Image(
        image: imageProvider,
1227
        loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
1228
          chunkEvents.add(loadingProgress);
1229
          if (loadingProgress == null) {
1230
            return child;
1231
          }
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242
          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);
1243
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1244 1245 1246 1247 1248
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(chunkEvents.length, 2);
    expect(find.text('loading 10 / 100'), findsOneWidget);
    expect(find.byType(RawImage), findsNothing);
1249
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 30, expectedTotalBytes: 100));
1250 1251 1252 1253 1254
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();
    expect(chunkEvents.length, 3);
    expect(find.text('loading 30 / 100'), findsOneWidget);
    expect(find.byType(RawImage), findsNothing);
1255
    streamCompleter.setData(imageInfo: ImageInfo(image: image10x10));
1256 1257 1258 1259
    await tester.pump();
    expect(chunkEvents.length, 4);
    expect(find.byType(Text), findsNothing);
    expect(find.byType(RawImage), findsOneWidget);
1260
  });
1261

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

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

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

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

    await tester.pumpWidget(
      Image(
        image: imageProvider,
        excludeFromSemantics: true,
1292
        frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1293 1294
          return Padding(padding: const EdgeInsets.all(1), child: child);
        },
1295
        loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
1296 1297 1298 1299 1300 1301 1302 1303
          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
1304
    expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>());
1305
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1306 1307 1308 1309
    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
1310 1311
    expect(tester.widget<Center>(find.byType(Center)).child, isA<Padding>());
    expect(tester.widget<Padding>(find.byType(Padding)).child, isA<RawImage>());
1312
  });
1313 1314

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

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

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

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

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

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

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

    expect(find.byType(Center), findsOneWidget);
    expect(find.byType(RawImage), findsOneWidget);
1361
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374
    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));
1375
    streamCompleter.setData(chunkEvent: const ImageChunkEvent(cumulativeBytesLoaded: 10, expectedTotalBytes: 100));
1376
    expect(tester.binding.hasScheduledFrame, isFalse);
1377
  });
1378

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


1395
    final _TestImageProvider imageProvider2 = _TestImageProvider();
1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413
    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();
1414 1415
    final _TestImageStreamCompleter imageStreamCompleter = _TestImageStreamCompleter();
    final _TestImageProvider imageProvider1 = _TestImageProvider(streamCompleter: imageStreamCompleter);
1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428
    await tester.pumpWidget(
      Container(
        key: key,
        child: Image(
          image: imageProvider1,
          errorBuilder: (_,__,___) => Container(),
        ),
      ),
    );
    // listener from resolveStreamForKey is always added.
    expect(imageStreamCompleter.listeners.length, 2);


1429
    final _TestImageProvider imageProvider2 = _TestImageProvider();
1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445
    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);
  });

1446 1447
  testWidgets('Image defers loading while fast scrolling', (WidgetTester tester) async {
    const int gridCells = 1000;
1448
    final List<_TestImageProvider> imageProviders = <_TestImageProvider>[];
1449 1450 1451 1452 1453 1454 1455 1456
    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) {
1457
          final _TestImageProvider provider = _TestImageProvider();
1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470
          imageProviders.add(provider);
          return SizedBox(
            height: 250,
            width: 250,
            child: Image(
              image: provider,
              semanticLabel: index.toString(),
            ),
          );
        },
      ),
    ));

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

    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
1493 1494

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

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

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

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

1514 1515 1516 1517 1518 1519 1520
    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
1521 1522 1523 1524

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

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

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

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

    await tester.pumpWidget(Image(image: provider2));
    await tester.idle();
1536 1537 1538 1539 1540
    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
1541 1542 1543 1544 1545

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

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

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

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

    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);
1578
    late bool isSync;
Dan Field's avatar
Dan Field committed
1579 1580 1581 1582
    final ImageStreamListener listener = ImageStreamListener((ImageInfo image, bool syncCall) { isSync = syncCall; });

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

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

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

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

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

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

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

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

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

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

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

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

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

1645 1646 1647
    expect(imageCache.liveImageCount, 0);
    expect(imageCache.currentSize, 1);
    expect(imageCache.containsKey(provider), true);
Dan Field's avatar
Dan Field committed
1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667
    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));
1668
        imageCache.evict(provider);
Dan Field's avatar
Dan Field committed
1669 1670 1671 1672
        futures.add(precacheImage(provider, context));
        return const SizedBox.expand();
      }));
      await Future.wait<void>(futures);
1673 1674
      expect(imageCache.statusForKey(provider).keepAlive, true);
      expect(imageCache.statusForKey(provider).live, true);
Dan Field's avatar
Dan Field committed
1675 1676

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

  testWidgets('errorBuilder - fails on key', (WidgetTester tester) async {
    final UniqueKey errorKey = UniqueKey();
1686
    late Object caughtException;
1687 1688
    await tester.pumpWidget(
      Image(
1689
        image: _FailingImageProvider(failOnObtainKey: true, throws: 'threw', image: image10x10),
1690
        errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705
          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();
1706
    late Object caughtException;
1707 1708
    await tester.pumpWidget(
      Image(
1709
        image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
1710
        errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725
          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(
1726
      Image(
1727
        image: _FailingImageProvider(failOnLoad: true, throws: 'threw', image: image10x10),
1728 1729 1730 1731 1732 1733 1734
      ),
    );

    await tester.pump();

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

1736
  Future<void> testRotatedImage(WidgetTester tester, bool isAntiAlias) async {
1737 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
    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 {
1767 1768
      await testRotatedImage(tester, true);
      await testRotatedImage(tester, false);
1769
    },
1770
    skip: kIsWeb, // https://github.com/flutter/flutter/issues/87933.
1771
  );
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
  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'),
      );
    },
1820
    skip: kIsWeb, // https://github.com/flutter/flutter/issues/87933.
1821 1822
  );

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

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

    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),
1856
        displaySize: Size(150, 150),
1857 1858 1859 1860 1861
      ),
    );

    debugOnPaintImage = null;
  });
1862

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

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

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

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

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

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

    await tester.pumpWidget(const SizedBox());

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

    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.
1900
    expect(image.debugGetOpenHandleStackTraces()!.length, 0);
1901
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
1902 1903

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

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

    await tester.pumpWidget(
      Center(
        child: SizedBox(
          height: 50,
          width: 50,
          child: Image(
            image: imageProvider,
            excludeFromSemantics: true,
1946
            errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) {
1947 1948
              return Container(key: errorKey);
            },
1949
            frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
1950 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
              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);
  });
1977

1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992
  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'),
1993
        ),
1994 1995 1996 1997 1998 1999
      ),
    ));
    await expectLater(
      find.byKey(key),
      matchesGoldenFile('image_test.missing.1.png'),
    );
2000 2001 2002 2003 2004 2005 2006
    expect(
      tester.takeException().toString(),
      equals(
        'Unable to load asset: "missing-asset".\n'
        'The asset does not exist or has empty data.',
      ),
    );
2007 2008 2009 2010 2011 2012
    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)
2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028

  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)),
    );
  });
2029 2030
}

2031
@immutable
2032 2033
class _ConfigurationAwareKey {
  const _ConfigurationAwareKey(this.provider, this.configuration)
2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044
    : assert(provider != null),
      assert(configuration != null);

  final ImageProvider provider;
  final ImageConfiguration configuration;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  /// 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
2177
  ImageStreamCompleter loadBuffer(Object key, DecoderBufferCallback decode) => imageProvider.loadBuffer(key, decode);
2178
}
2179

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

  final bool failOnObtainKey;
  final bool failOnLoad;
  final Object throws;
2195
  final ui.Image image;
2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212

  @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(
2213
          image: image,
2214 2215 2216 2217 2218 2219
          scale: 0,
        ),
      ),
    );
  }
}