image_test.dart 32.5 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:typed_data';
7
import 'dart:ui' as ui show Image, ImageByteFormat;
8

9
import 'package:flutter/foundation.dart';
10 11
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
12
import 'package:flutter_test/flutter_test.dart';
13

14
import '../painting/image_data.dart';
15
import 'semantics_tester.dart';
16

17
void main() {
18
  testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
19 20
    final GlobalKey key = GlobalKey();
    final TestImageProvider imageProvider1 = TestImageProvider();
21
    await tester.pumpWidget(
22
      Container(
23
        key: key,
24
        child: Image(
25 26
          image: imageProvider1,
          excludeFromSemantics: true,
27
        ),
28 29
      ),
      null,
30
      EnginePhase.layout,
31
    );
32 33
    RenderImage renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNull);
34

35 36 37
    imageProvider1.complete();
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
38

39 40
    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);
41

42
    final TestImageProvider imageProvider2 = TestImageProvider();
43
    await tester.pumpWidget(
44
      Container(
45
        key: key,
46
        child: Image(
47 48
          image: imageProvider2,
          excludeFromSemantics: true,
49
        ),
50 51
      ),
      null,
52
      EnginePhase.layout,
53 54
    );

55 56
    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNull);
57 58
  });

59
  testWidgets('Verify Image doesn\'t reset its RenderImage when changing providers if it has gaplessPlayback set', (WidgetTester tester) async {
60 61
    final GlobalKey key = GlobalKey();
    final TestImageProvider imageProvider1 = TestImageProvider();
62
    await tester.pumpWidget(
63
      Container(
64
        key: key,
65
        child: Image(
66
          gaplessPlayback: true,
67 68
          image: imageProvider1,
          excludeFromSemantics: true,
69
        ),
70 71
      ),
      null,
72
      EnginePhase.layout,
73
    );
74 75
    RenderImage renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNull);
76

77 78 79
    imageProvider1.complete();
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
80

81 82 83
    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);

84
    final TestImageProvider imageProvider2 = TestImageProvider();
85
    await tester.pumpWidget(
86
      Container(
87
        key: key,
88
        child: Image(
89
          gaplessPlayback: true,
90 91
          image: imageProvider2,
          excludeFromSemantics: true,
92
        ),
93 94
      ),
      null,
95
      EnginePhase.layout,
96 97
    );

98 99
    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);
100 101
  });

102
  testWidgets('Verify Image resets its RenderImage when changing providers if it has a key', (WidgetTester tester) async {
103 104
    final GlobalKey key = GlobalKey();
    final TestImageProvider imageProvider1 = TestImageProvider();
105
    await tester.pumpWidget(
106
      Image(
107
        key: key,
108 109
        image: imageProvider1,
        excludeFromSemantics: true,
110 111
      ),
      null,
112
      EnginePhase.layout,
113 114 115 116 117
    );
    RenderImage renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNull);

    imageProvider1.complete();
118 119
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
120 121 122 123

    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);

124
    final TestImageProvider imageProvider2 = TestImageProvider();
125
    await tester.pumpWidget(
126
      Image(
127
        key: key,
128 129
        image: imageProvider2,
        excludeFromSemantics: true,
130 131
      ),
      null,
132
      EnginePhase.layout,
133
    );
134

135 136
    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNull);
137 138
  });

139
  testWidgets('Verify Image doesn\'t reset its RenderImage when changing providers if it has gaplessPlayback set', (WidgetTester tester) async {
140 141
    final GlobalKey key = GlobalKey();
    final TestImageProvider imageProvider1 = TestImageProvider();
142
    await tester.pumpWidget(
143
      Image(
144 145
        key: key,
        gaplessPlayback: true,
146 147
        image: imageProvider1,
        excludeFromSemantics: true,
148 149
      ),
      null,
150
      EnginePhase.layout,
151 152 153 154 155
    );
    RenderImage renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNull);

    imageProvider1.complete();
156 157
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);
158 159 160 161

    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);

162
    final TestImageProvider imageProvider2 = TestImageProvider();
163
    await tester.pumpWidget(
164
      Image(
165
        key: key,
166
        gaplessPlayback: true,
167
        excludeFromSemantics: true,
168
        image: imageProvider2,
169 170
      ),
      null,
171
      EnginePhase.layout,
172 173 174 175
    );

    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);
176 177
  });

178
  testWidgets('Verify ImageProvider configuration inheritance', (WidgetTester tester) async {
179 180 181 182
    final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
    final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
    final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
    final TestImageProvider imageProvider = TestImageProvider();
183 184 185 186

    // Of the two nested MediaQuery objects, the innermost one,
    // mediaQuery2, should define the configuration of the imageProvider.
    await tester.pumpWidget(
187
      MediaQuery(
188
        key: mediaQueryKey1,
189
        data: const MediaQueryData(
190 191 192
          devicePixelRatio: 10.0,
          padding: EdgeInsets.zero,
        ),
193
        child: MediaQuery(
194
          key: mediaQueryKey2,
195
          data: const MediaQueryData(
196 197 198
            devicePixelRatio: 5.0,
            padding: EdgeInsets.zero,
          ),
199
          child: Image(
200
            excludeFromSemantics: true,
201
            key: imageKey,
202
            image: imageProvider,
203
          ),
204
        ),
205 206 207
      )
    );

208
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);
209 210 211 212 213

    // 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(
214
      MediaQuery(
215
        key: mediaQueryKey2,
216
        data: const MediaQueryData(
217 218 219
          devicePixelRatio: 5.0,
          padding: EdgeInsets.zero,
        ),
220
        child: MediaQuery(
221
          key: mediaQueryKey1,
222
          data: const MediaQueryData(
223 224 225
            devicePixelRatio: 10.0,
            padding: EdgeInsets.zero,
          ),
226
          child: Image(
227
            excludeFromSemantics: true,
228
            key: imageKey,
229
            image: imageProvider,
230
          ),
231
        ),
232 233 234
      )
    );

235
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 10.0);
236 237 238
  });

  testWidgets('Verify ImageProvider configuration inheritance again', (WidgetTester tester) async {
239 240 241 242
    final GlobalKey mediaQueryKey1 = GlobalKey(debugLabel: 'mediaQueryKey1');
    final GlobalKey mediaQueryKey2 = GlobalKey(debugLabel: 'mediaQueryKey2');
    final GlobalKey imageKey = GlobalKey(debugLabel: 'image');
    final TestImageProvider imageProvider = TestImageProvider();
243

244
    // This is just a variation on the previous test. In this version the location
245 246
    // of the Image changes and the MediaQuery widgets do not.
    await tester.pumpWidget(
247
      Row(
248
        textDirection: TextDirection.ltr,
249
        children: <Widget> [
250
          MediaQuery(
251
            key: mediaQueryKey2,
252
            data: const MediaQueryData(
253 254 255
              devicePixelRatio: 5.0,
              padding: EdgeInsets.zero,
            ),
256
            child: Image(
257
              excludeFromSemantics: true,
258
              key: imageKey,
259 260
              image: imageProvider,
            ),
261
          ),
262
          MediaQuery(
263
            key: mediaQueryKey1,
264
            data: const MediaQueryData(
265 266 267
              devicePixelRatio: 10.0,
              padding: EdgeInsets.zero,
            ),
268 269 270
            child: Container(width: 100.0),
          ),
        ],
271 272 273
      )
    );

274
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 5.0);
275 276

    await tester.pumpWidget(
277
      Row(
278
        textDirection: TextDirection.ltr,
279
        children: <Widget> [
280
          MediaQuery(
281
            key: mediaQueryKey2,
282
            data: const MediaQueryData(
283 284 285
              devicePixelRatio: 5.0,
              padding: EdgeInsets.zero,
            ),
286
            child: Container(width: 100.0),
287
          ),
288
          MediaQuery(
289
            key: mediaQueryKey1,
290
            data: const MediaQueryData(
291 292 293
              devicePixelRatio: 10.0,
              padding: EdgeInsets.zero,
            ),
294
            child: Image(
295
              excludeFromSemantics: true,
296
              key: imageKey,
297 298 299 300
              image: imageProvider,
            ),
          ),
        ],
301 302 303
      )
    );

304
    expect(imageProvider._lastResolvedConfiguration.devicePixelRatio, 10.0);
305 306
  });

307
  testWidgets('Verify Image stops listening to ImageStream', (WidgetTester tester) async {
308 309
    final TestImageProvider imageProvider = TestImageProvider();
    await tester.pumpWidget(Image(image: imageProvider, excludeFromSemantics: true));
310
    final State<Image> image = tester.state/*State<Image>*/(find.byType(Image));
311
    expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null)'));
312 313
    imageProvider.complete();
    await tester.pump();
314
    expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, [100×100] @ 1.0x, 1 listener), pixels: [100×100] @ 1.0x)'));
315
    await tester.pumpWidget(Container());
316
    expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(lifecycle state: defunct, not mounted, stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, [100×100] @ 1.0x, 0 listeners), pixels: [100×100] @ 1.0x)'));
317 318
  });

319 320 321 322 323 324 325 326 327 328 329 330
  testWidgets('Stream completer errors can be listened to by attaching before resolving', (WidgetTester tester) async {
    dynamic capturedException;
    StackTrace capturedStackTrace;
    ImageInfo capturedImage;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
      capturedException = exception;
      capturedStackTrace = stackTrace;
    };
    final ImageListener listener = (ImageInfo info, bool synchronous) {
      capturedImage = info;
    };

331
    final Exception testException = Exception('cannot resolve host');
332
    final StackTrace testStack = StackTrace.current;
333
    final TestImageProvider imageProvider = TestImageProvider();
334 335 336
    imageProvider._streamCompleter.addListener(listener, onError: errorListener);
    ImageConfiguration configuration;
    await tester.pumpWidget(
337
      Builder(
338 339
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
340
          return Container();
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
        },
      ),
    );
    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;
    StackTrace capturedStackTrace;
    dynamic reportedException;
    StackTrace reportedStackTrace;
    ImageInfo capturedImage;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
      capturedException = exception;
      capturedStackTrace = stackTrace;
    };
    final ImageListener listener = (ImageInfo info, bool synchronous) {
      capturedImage = info;
    };
    FlutterError.onError = (FlutterErrorDetails flutterError) {
      reportedException = flutterError.exception;
      reportedStackTrace = flutterError.stack;
    };

377
    final Exception testException = Exception('cannot resolve host');
378
    final StackTrace testStack = StackTrace.current;
379
    final TestImageProvider imageProvider = TestImageProvider();
380 381
    ImageConfiguration configuration;
    await tester.pumpWidget(
382
      Builder(
383 384
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
385
          return Container();
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
        },
      ),
    );
    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);

    streamUnderTest.addListener(listener, onError: errorListener);

    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;
    StackTrace capturedStackTrace;
    ImageInfo capturedImage;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
      capturedException = exception;
      capturedStackTrace = stackTrace;
    };
    final ImageListener listener = (ImageInfo info, bool synchronous) {
      capturedImage = info;
    };

422
    final Exception testException = Exception('cannot resolve host');
423
    final StackTrace testStack = StackTrace.current;
424
    final TestImageProvider imageProvider = TestImageProvider();
425 426 427 428 429
    imageProvider._streamCompleter.addListener(listener, onError: errorListener);
    // Add the exact same listener a second time without the errorListener.
    imageProvider._streamCompleter.addListener(listener);
    ImageConfiguration configuration;
    await tester.pumpWidget(
430
      Builder(
431 432
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
433
          return Container();
434 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
        },
      ),
    );
    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;
    StackTrace capturedStackTrace;
    ImageInfo capturedImage;
    int errorListenerCalled = 0;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
      capturedException = exception;
      capturedStackTrace = stackTrace;
      errorListenerCalled++;
    };
    final ImageListener listener = (ImageInfo info, bool synchronous) {
      capturedImage = info;
    };

466
    final Exception testException = Exception('cannot resolve host');
467
    final StackTrace testStack = StackTrace.current;
468
    final TestImageProvider imageProvider = TestImageProvider();
469 470 471 472 473
    imageProvider._streamCompleter.addListener(listener, onError: errorListener);
    // Add the exact same errorListener a second time.
    imageProvider._streamCompleter.addListener(null, onError: errorListener);
    ImageConfiguration configuration;
    await tester.pumpWidget(
474
      Builder(
475 476
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
477
          return Container();
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
        },
      ),
    );
    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);
  });

  testWidgets('Error listeners are removed along with listeners', (WidgetTester tester) async {
    bool errorListenerCalled = false;
    dynamic reportedException;
    StackTrace reportedStackTrace;
    ImageInfo capturedImage;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
      errorListenerCalled = true;
    };
    final ImageListener listener = (ImageInfo info, bool synchronous) {
      capturedImage = info;
    };
    FlutterError.onError = (FlutterErrorDetails flutterError) {
      reportedException = flutterError.exception;
      reportedStackTrace = flutterError.stack;
    };

513
    final Exception testException = Exception('cannot resolve host');
514
    final StackTrace testStack = StackTrace.current;
515
    final TestImageProvider imageProvider = TestImageProvider();
516 517 518 519 520 521
    imageProvider._streamCompleter.addListener(listener, onError: errorListener);
    // Now remove the listener the error listener is attached to.
    // Don't explicitly remove the error listener.
    imageProvider._streamCompleter.removeListener(listener);
    ImageConfiguration configuration;
    await tester.pumpWidget(
522
      Builder(
523 524
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
525
          return Container();
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
        },
      ),
    );
    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(errorListenerCalled, false);
    // Since the error listener is removed, bubble up to FlutterError.
    expect(reportedException, testException);
    expect(reportedStackTrace, testStack);
    expect(capturedImage, isNull); // The image stream listeners should never be called.
  });

544 545
  testWidgets('Removing listener removes one listener and error listener', (WidgetTester tester) async {
    int errorListenerCalled = 0;
546 547
    ImageInfo capturedImage;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
548
      errorListenerCalled++;
549 550 551 552 553
    };
    final ImageListener listener = (ImageInfo info, bool synchronous) {
      capturedImage = info;
    };

554
    final Exception testException = Exception('cannot resolve host');
555
    final StackTrace testStack = StackTrace.current;
556
    final TestImageProvider imageProvider = TestImageProvider();
557 558 559
    imageProvider._streamCompleter.addListener(listener, onError: errorListener);
    // Duplicates the same set of listener and errorListener.
    imageProvider._streamCompleter.addListener(listener, onError: errorListener);
560
    // Now remove one entry of the specified listener and associated error listener.
561 562 563 564
    // Don't explicitly remove the error listener.
    imageProvider._streamCompleter.removeListener(listener);
    ImageConfiguration configuration;
    await tester.pumpWidget(
565
      Builder(
566 567
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
568
          return Container();
569 570 571 572 573 574 575 576 577 578 579
        },
      ),
    );
    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);

580
    expect(errorListenerCalled, 1);
581 582 583
    expect(capturedImage, isNull); // The image stream listeners should never be called.
  });

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
  testWidgets('Removing listener FIFO removes exactly one listener and error listener', (WidgetTester tester) async {
    // To make sure that a single listener removal doesn't only happen
    // accidentally as described in https://github.com/flutter/flutter/pull/25865#discussion_r244851565.
    int errorListener1Called = 0;
    int errorListener2Called = 0;
    int errorListener3Called = 0;
    ImageInfo capturedImage;
    final ImageErrorListener errorListener1 = (dynamic exception, StackTrace stackTrace) {
      errorListener1Called++;
    };
    final ImageErrorListener errorListener2 = (dynamic exception, StackTrace stackTrace) {
      errorListener2Called++;
    };
    final ImageErrorListener errorListener3 = (dynamic exception, StackTrace stackTrace) {
      errorListener3Called++;
    };
    final ImageListener listener = (ImageInfo info, bool synchronous) {
      capturedImage = info;
    };

    final Exception testException = Exception('cannot resolve host');
    final StackTrace testStack = StackTrace.current;
    final TestImageProvider imageProvider = TestImageProvider();
    imageProvider._streamCompleter.addListener(listener, onError: errorListener1);
    imageProvider._streamCompleter.addListener(listener, onError: errorListener2);
    imageProvider._streamCompleter.addListener(listener, onError: errorListener3);
    // Remove listener. It should remove exactly the first one and the associated
    // errorListener1.
    imageProvider._streamCompleter.removeListener(listener);
    ImageConfiguration configuration;
    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          configuration = createLocalImageConfiguration(context);
          return Container();
        },
      ),
    );
    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(errorListener1Called, 0);
    expect(errorListener2Called, 1);
    expect(errorListener3Called, 1);
    expect(capturedImage, isNull); // The image stream listeners should never be called.
  });

636
  testWidgets('Image.memory control test', (WidgetTester tester) async {
637
    await tester.pumpWidget(Image.memory(Uint8List.fromList(kTransparentImage), excludeFromSemantics: true,));
638
  });
639 640 641

  testWidgets('Image color and colorBlend parameters', (WidgetTester tester) async {
    await tester.pumpWidget(
642
      Image(
643
        excludeFromSemantics: true,
644
        image: TestImageProvider(),
645
        color: const Color(0xFF00FF00),
646
        colorBlendMode: BlendMode.clear,
647 648 649 650 651 652
      )
    );
    final RenderImage renderer = tester.renderObject<RenderImage>(find.byType(Image));
    expect(renderer.color, const Color(0xFF00FF00));
    expect(renderer.colorBlendMode, BlendMode.clear);
  });
653 654

  testWidgets('Precache', (WidgetTester tester) async {
655
    final TestImageProvider provider = TestImageProvider();
656
    Future<void> precache;
657
    await tester.pumpWidget(
658
      Builder(
659 660
        builder: (BuildContext context) {
          precache = precacheImage(provider, context);
661
          return Container();
662 663 664 665 666 667 668 669 670 671 672 673 674
        }
      )
    );
    provider.complete();
    await precache;
    expect(provider._lastResolvedConfiguration, isNotNull);

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

676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
  testWidgets('Precache remove listeners immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async {
    final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
    final TestImageProvider provider = TestImageProvider(streamCompleter: imageStreamCompleter);

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

    expect(imageStreamCompleter.listeners.length, 2);
    imageStreamCompleter.listeners.keys.toList()[1](null, null);

    expect(imageStreamCompleter.listeners.length, 1);
    imageStreamCompleter.listeners.keys.toList()[0](null, null);

    expect(imageStreamCompleter.listeners.length, 0);
  });

698 699 700 701 702 703 704 705
  testWidgets('Precache completes with onError on error', (WidgetTester tester) async {
    dynamic capturedException;
    StackTrace capturedStackTrace;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
      capturedException = exception;
      capturedStackTrace = stackTrace;
    };

706
    final Exception testException = Exception('cannot resolve host');
707
    final StackTrace testStack = StackTrace.current;
708
    final TestImageProvider imageProvider = TestImageProvider();
709
    Future<void> precache;
710
    await tester.pumpWidget(
711
      Builder(
712 713
        builder: (BuildContext context) {
          precache = precacheImage(imageProvider, context, onError: errorListener);
714
          return Container();
715 716 717 718 719 720 721 722 723 724 725 726 727
        }
      )
    );
    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);
  });

728
  testWidgets('TickerMode controls stream registration', (WidgetTester tester) async {
729 730
    final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
    final Image image = Image(
731
      excludeFromSemantics: true,
732
      image: TestImageProvider(streamCompleter: imageStreamCompleter),
733 734
    );
    await tester.pumpWidget(
735
      TickerMode(
736 737 738 739
        enabled: true,
        child: image,
      ),
    );
740
    expect(imageStreamCompleter.listeners.length, 2);
741
    await tester.pumpWidget(
742
      TickerMode(
743 744 745 746
        enabled: false,
        child: image,
      ),
    );
747
    expect(imageStreamCompleter.listeners.length, 1);
748 749
  });

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

753 754
    final TestImageProvider imageProvider1 = TestImageProvider();
    final TestImageProvider imageProvider2 = TestImageProvider();
755 756

    await tester.pumpWidget(
757
        Container(
758
            key: key,
759
            child: Image(
760
                excludeFromSemantics: true,
761 762
                image: imageProvider1,
            ),
763 764
        ),
        null,
765
        EnginePhase.layout,
766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
    );
    RenderImage renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNull);

    imageProvider1.complete();
    imageProvider2.complete();
    await tester.idle(); // resolve the future from the image provider
    await tester.pump(null, EnginePhase.layout);

    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);

    final ui.Image oldImage = renderImage.image;

    await tester.pumpWidget(
781
        Container(
782
            key: key,
783
            child: Image(
784
              excludeFromSemantics: true,
785 786
              image: imageProvider2,
            ),
787 788
        ),
        null,
789
        EnginePhase.layout,
790 791 792 793 794 795 796 797
    );

    renderImage = key.currentContext.findRenderObject();
    expect(renderImage.image, isNotNull);
    expect(renderImage.image, isNot(equals(oldImage)));
  });

  testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async {
798 799
    final Image image1 = Image(image: TestImageProvider()..complete(), width: 10.0, excludeFromSemantics: true);
    final Image image2 = Image(image: TestImageProvider()..complete(), width: 20.0, excludeFromSemantics: true);
800

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

804
    final Column columnSwapped = Column(children: <Widget>[image2, image1]);
805 806 807 808 809 810 811 812 813
    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);
  });
814 815

  testWidgets('Image contributes semantics', (WidgetTester tester) async {
816
    final SemanticsTester semantics = SemanticsTester(tester);
817
    await tester.pumpWidget(
818
      Directionality(
819
        textDirection: TextDirection.ltr,
820
        child: Row(
821
          children: <Widget>[
822 823
            Image(
              image: TestImageProvider(),
824 825 826 827 828 829 830 831 832
              width: 100.0,
              height: 100.0,
              semanticLabel: 'test',
            ),
          ],
        ),
      ),
    );

833
    expect(semantics, hasSemantics(TestSemantics.root(
834
      children: <TestSemantics>[
835
        TestSemantics.rootChild(
836 837
          id: 1,
          label: 'test',
838
          rect: Rect.fromLTWH(0.0, 0.0, 100.0, 100.0),
839 840
          textDirection: TextDirection.ltr,
          flags: <SemanticsFlag>[SemanticsFlag.isImage],
841
        ),
842 843 844 845 846 847
      ]
    ), ignoreTransform: true));
    semantics.dispose();
  });

  testWidgets('Image can exclude semantics', (WidgetTester tester) async {
848
    final SemanticsTester semantics = SemanticsTester(tester);
849
    await tester.pumpWidget(
850
      Directionality(
851
        textDirection: TextDirection.ltr,
852 853
        child: Image(
          image: TestImageProvider(),
854 855 856 857 858 859 860
          width: 100.0,
          height: 100.0,
          excludeFromSemantics: true,
        ),
      ),
    );

861
    expect(semantics, hasSemantics(TestSemantics.root(
862 863 864 865
      children: <TestSemantics>[]
    )));
    semantics.dispose();
  });
866 867
}

868
class TestImageProvider extends ImageProvider<TestImageProvider> {
869 870
  TestImageProvider({ImageStreamCompleter streamCompleter}) {
    _streamCompleter = streamCompleter
871
      ?? OneFrameImageStreamCompleter(_completer.future);
872 873
  }

874 875 876 877
  final Completer<ImageInfo> _completer = Completer<ImageInfo>();
  ImageStreamCompleter _streamCompleter;
  ImageConfiguration _lastResolvedConfiguration;

878
  @override
879
  Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
880
    return SynchronousFuture<TestImageProvider>(this);
881 882
  }

883 884
  @override
  ImageStream resolve(ImageConfiguration configuration) {
885
    _lastResolvedConfiguration = configuration;
886 887 888
    return super.resolve(configuration);
  }

889
  @override
890
  ImageStreamCompleter load(TestImageProvider key) => _streamCompleter;
891

892
  void complete() {
893
    _completer.complete(ImageInfo(image: TestImage()));
894
  }
895

896 897 898 899
  void fail(dynamic exception, StackTrace stackTrace) {
    _completer.completeError(exception, stackTrace);
  }

900
  @override
901
  String toString() => '${describeIdentity(this)}()';
902 903
}

904
class TestImageStreamCompleter extends ImageStreamCompleter {
905
  final Map<ImageListener, ImageErrorListener> listeners = <ImageListener, ImageErrorListener>{};
906

907
  @override
908 909
  void addListener(ImageListener listener, { ImageErrorListener onError }) {
    listeners[listener] = onError;
910 911 912 913 914 915 916 917
  }

  @override
  void removeListener(ImageListener listener) {
    listeners.remove(listener);
  }
}

918
class TestImage implements ui.Image {
919 920 921 922 923 924 925
  @override
  int get width => 100;

  @override
  int get height => 100;

  @override
926
  void dispose() { }
927

928
  @override
929
  Future<ByteData> toByteData({ ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba }) async {
930
    throw UnsupportedError('Cannot encode test image');
931 932
  }

933 934
  @override
  String toString() => '[$width\u00D7$height]';
935
}