image_test.dart 29.4 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 52
        )
      ),
      null,
      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 72
        )
      ),
      null,
      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 95
        )
      ),
      null,
      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 112 113 114 115 116 117
      ),
      null,
      EnginePhase.layout
    );
    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 132 133
      ),
      null,
      EnginePhase.layout
    );
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 150 151 152 153 154 155
      ),
      null,
      EnginePhase.layout
    );
    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 171 172 173 174 175
      ),
      null,
      EnginePhase.layout
    );

    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 202 203 204 205 206 207
            key: imageKey,
            image: imageProvider
          ),
        )
      )
    );

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 229 230 231 232 233 234
            key: imageKey,
            image: imageProvider
          ),
        )
      )
    );

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 259 260 261
              key: imageKey,
              image: imageProvider
            )
          ),
262
          MediaQuery(
263
            key: mediaQueryKey1,
264
            data: const MediaQueryData(
265 266 267
              devicePixelRatio: 10.0,
              padding: EdgeInsets.zero,
            ),
268
            child: Container(width: 100.0)
269 270 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 297 298 299 300 301 302 303
              key: imageKey,
              image: imageProvider
            )
          )
        ]
      )
    );

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
  testWidgets('Image.memory control test', (WidgetTester tester) async {
585
    await tester.pumpWidget(Image.memory(Uint8List.fromList(kTransparentImage), excludeFromSemantics: true,));
586
  });
587 588 589

  testWidgets('Image color and colorBlend parameters', (WidgetTester tester) async {
    await tester.pumpWidget(
590
      Image(
591
        excludeFromSemantics: true,
592
        image: TestImageProvider(),
593 594 595 596 597 598 599 600
        color: const Color(0xFF00FF00),
        colorBlendMode: BlendMode.clear
      )
    );
    final RenderImage renderer = tester.renderObject<RenderImage>(find.byType(Image));
    expect(renderer.color, const Color(0xFF00FF00));
    expect(renderer.colorBlendMode, BlendMode.clear);
  });
601 602

  testWidgets('Precache', (WidgetTester tester) async {
603
    final TestImageProvider provider = TestImageProvider();
604 605
    Future<Null> precache;
    await tester.pumpWidget(
606
      Builder(
607 608
        builder: (BuildContext context) {
          precache = precacheImage(provider, context);
609
          return Container();
610 611 612 613 614 615 616 617 618 619 620 621 622
        }
      )
    );
    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);
  });
623

624 625 626 627 628 629 630 631
  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;
    };

632
    final Exception testException = Exception('cannot resolve host');
633
    final StackTrace testStack = StackTrace.current;
634
    final TestImageProvider imageProvider = TestImageProvider();
635 636
    Future<Null> precache;
    await tester.pumpWidget(
637
      Builder(
638 639
        builder: (BuildContext context) {
          precache = precacheImage(imageProvider, context, onError: errorListener);
640
          return Container();
641 642 643 644 645 646 647 648 649 650 651 652 653
        }
      )
    );
    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);
  });

654
  testWidgets('TickerMode controls stream registration', (WidgetTester tester) async {
655 656
    final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
    final Image image = Image(
657
      excludeFromSemantics: true,
658
      image: TestImageProvider(streamCompleter: imageStreamCompleter),
659 660
    );
    await tester.pumpWidget(
661
      TickerMode(
662 663 664 665
        enabled: true,
        child: image,
      ),
    );
666
    expect(imageStreamCompleter.listeners.length, 2);
667
    await tester.pumpWidget(
668
      TickerMode(
669 670 671 672
        enabled: false,
        child: image,
      ),
    );
673
    expect(imageStreamCompleter.listeners.length, 1);
674 675
  });

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

679 680
    final TestImageProvider imageProvider1 = TestImageProvider();
    final TestImageProvider imageProvider2 = TestImageProvider();
681 682

    await tester.pumpWidget(
683
        Container(
684
            key: key,
685
            child: Image(
686
                excludeFromSemantics: true,
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
                image: imageProvider1
            )
        ),
        null,
        EnginePhase.layout
    );
    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(
707
        Container(
708
            key: key,
709
            child: Image(
710 711
              excludeFromSemantics: true,
              image: imageProvider2
712 713 714 715 716 717 718 719 720 721 722 723
            )
        ),
        null,
        EnginePhase.layout
    );

    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 {
724 725
    final Image image1 = Image(image: TestImageProvider()..complete(), width: 10.0, excludeFromSemantics: true);
    final Image image2 = Image(image: TestImageProvider()..complete(), width: 20.0, excludeFromSemantics: true);
726

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

730
    final Column columnSwapped = Column(children: <Widget>[image2, image1]);
731 732 733 734 735 736 737 738 739
    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);
  });
740 741

  testWidgets('Image contributes semantics', (WidgetTester tester) async {
742
    final SemanticsTester semantics = SemanticsTester(tester);
743
    await tester.pumpWidget(
744
      Directionality(
745
        textDirection: TextDirection.ltr,
746
        child: Row(
747
          children: <Widget>[
748 749
            Image(
              image: TestImageProvider(),
750 751 752 753 754 755 756 757 758
              width: 100.0,
              height: 100.0,
              semanticLabel: 'test',
            ),
          ],
        ),
      ),
    );

759
    expect(semantics, hasSemantics(TestSemantics.root(
760
      children: <TestSemantics>[
761
        TestSemantics.rootChild(
762 763
          id: 1,
          label: 'test',
764
          rect: Rect.fromLTWH(0.0, 0.0, 100.0, 100.0),
765 766 767 768 769 770 771 772 773
          textDirection: TextDirection.ltr,
          flags: <SemanticsFlag>[SemanticsFlag.isImage],
        )
      ]
    ), ignoreTransform: true));
    semantics.dispose();
  });

  testWidgets('Image can exclude semantics', (WidgetTester tester) async {
774
    final SemanticsTester semantics = SemanticsTester(tester);
775
    await tester.pumpWidget(
776
      Directionality(
777
        textDirection: TextDirection.ltr,
778 779
        child: Image(
          image: TestImageProvider(),
780 781 782 783 784 785 786
          width: 100.0,
          height: 100.0,
          excludeFromSemantics: true,
        ),
      ),
    );

787
    expect(semantics, hasSemantics(TestSemantics.root(
788 789 790 791
      children: <TestSemantics>[]
    )));
    semantics.dispose();
  });
792 793
}

794
class TestImageProvider extends ImageProvider<TestImageProvider> {
795 796
  TestImageProvider({ImageStreamCompleter streamCompleter}) {
    _streamCompleter = streamCompleter
797
      ?? OneFrameImageStreamCompleter(_completer.future);
798 799
  }

800 801 802 803
  final Completer<ImageInfo> _completer = Completer<ImageInfo>();
  ImageStreamCompleter _streamCompleter;
  ImageConfiguration _lastResolvedConfiguration;

804
  @override
805
  Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
806
    return SynchronousFuture<TestImageProvider>(this);
807 808
  }

809 810
  @override
  ImageStream resolve(ImageConfiguration configuration) {
811
    _lastResolvedConfiguration = configuration;
812 813 814
    return super.resolve(configuration);
  }

815
  @override
816
  ImageStreamCompleter load(TestImageProvider key) => _streamCompleter;
817

818
  void complete() {
819
    _completer.complete(ImageInfo(image: TestImage()));
820
  }
821

822 823 824 825
  void fail(dynamic exception, StackTrace stackTrace) {
    _completer.completeError(exception, stackTrace);
  }

826
  @override
827
  String toString() => '${describeIdentity(this)}()';
828 829
}

830
class TestImageStreamCompleter extends ImageStreamCompleter {
831
  final Map<ImageListener, ImageErrorListener> listeners = <ImageListener, ImageErrorListener> {};
832

833
  @override
834 835
  void addListener(ImageListener listener, { ImageErrorListener onError }) {
    listeners[listener] = onError;
836 837 838 839 840 841 842 843
  }

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

844
class TestImage implements ui.Image {
845 846 847 848 849 850 851
  @override
  int get width => 100;

  @override
  int get height => 100;

  @override
852
  void dispose() { }
853

854
  @override
855
  Future<ByteData> toByteData({ui.ImageByteFormat format}) async {
856
    throw UnsupportedError('Cannot encode test image');
857 858
  }

859 860
  @override
  String toString() => '[$width\u00D7$height]';
861
}