image_stream_test.dart 27.4 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
// @dart = 2.8

7 8
import 'dart:async';
import 'dart:ui';
9 10

import 'package:flutter/painting.dart';
11 12
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter_test/flutter_test.dart';
13
import 'package:meta/meta.dart';
14

15
class FakeFrameInfo implements FrameInfo {
16 17
  FakeFrameInfo(this._duration, this._image);

18

19 20 21
  final Duration _duration;
  final Image _image;

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
  @override
  Duration get duration => _duration;

  @override
  Image get image => _image;
}

class MockCodec implements Codec {

  @override
  int frameCount;

  @override
  int repetitionCount;

  int numFramesAsked = 0;

39
  Completer<FrameInfo> _nextFrameCompleter = Completer<FrameInfo>();
40 41 42 43 44 45 46 47 48

  @override
  Future<FrameInfo> getNextFrame() {
    numFramesAsked += 1;
    return _nextFrameCompleter.future;
  }

  void completeNextFrame(FrameInfo frameInfo) {
    _nextFrameCompleter.complete(frameInfo);
49
    _nextFrameCompleter = Completer<FrameInfo>();
50 51 52 53 54 55 56
  }

  void failNextFrame(String err) {
    _nextFrameCompleter.completeError(err);
  }

  @override
57
  void dispose() { }
58 59 60

}

61 62 63 64 65 66 67 68 69 70 71
class FakeEventReportingImageStreamCompleter extends ImageStreamCompleter {
  FakeEventReportingImageStreamCompleter({Stream<ImageChunkEvent> chunkEvents,}) {
    if (chunkEvents != null) {
      chunkEvents.listen((ImageChunkEvent event) {
          reportImageChunkEvent(event);
        },
      );
    }
  }
}

72
void main() {
73 74 75 76 77 78 79
  Image image20x10;
  Image image200x100;
  setUpAll(() async {
    image20x10 = await createTestImage(width: 20, height: 10);
    image200x100 = await createTestImage(width: 200, height: 100);
  });

80
  testWidgets('Codec future fails', (WidgetTester tester) async {
81 82
    final Completer<Codec> completer = Completer<Codec>();
    MultiFrameImageStreamCompleter(
83 84 85 86 87 88 89 90
      codec: completer.future,
      scale: 1.0,
    );
    completer.completeError('failure message');
    await tester.idle();
    expect(tester.takeException(), 'failure message');
  });

91
  testWidgets('Decoding starts when a listener is added after codec is ready', (WidgetTester tester) async {
92 93
    final Completer<Codec> completer = Completer<Codec>();
    final MockCodec mockCodec = MockCodec();
94
    mockCodec.frameCount = 1;
95
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
96 97 98 99
      codec: completer.future,
      scale: 1.0,
    );

100 101 102 103 104
    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 0);

    final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
105
    imageStream.addListener(ImageStreamListener(listener));
106 107 108 109 110 111 112 113 114 115 116 117 118 119
    await tester.idle();
    expect(mockCodec.numFramesAsked, 1);
  });

  testWidgets('Decoding starts when a codec is ready after a listener is added', (WidgetTester tester) async {
    final Completer<Codec> completer = Completer<Codec>();
    final MockCodec mockCodec = MockCodec();
    mockCodec.frameCount = 1;
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
      codec: completer.future,
      scale: 1.0,
    );

    final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
120
    imageStream.addListener(ImageStreamListener(listener));
121 122 123
    await tester.idle();
    expect(mockCodec.numFramesAsked, 0);

124 125 126 127 128
    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 1);
  });

129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
  testWidgets('Chunk events of base ImageStreamCompleter are delivered', (WidgetTester tester) async {
    final List<ImageChunkEvent> chunkEvents = <ImageChunkEvent>[];
    final StreamController<ImageChunkEvent> streamController = StreamController<ImageChunkEvent>();
    final ImageStreamCompleter imageStream = FakeEventReportingImageStreamCompleter(
      chunkEvents: streamController.stream,
    );

    imageStream.addListener(ImageStreamListener(
      (ImageInfo image, bool synchronousCall) { },
      onChunk: (ImageChunkEvent event) {
        chunkEvents.add(event);
      },
    ));
    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3));
    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3));
    await tester.idle();

    expect(chunkEvents.length, 2);
    expect(chunkEvents[0].cumulativeBytesLoaded, 1);
    expect(chunkEvents[0].expectedTotalBytes, 3);
    expect(chunkEvents[1].cumulativeBytesLoaded, 2);
    expect(chunkEvents[1].expectedTotalBytes, 3);
  });

  testWidgets('Chunk events of base ImageStreamCompleter are not buffered before listener registration', (WidgetTester tester) async {
    final List<ImageChunkEvent> chunkEvents = <ImageChunkEvent>[];
    final StreamController<ImageChunkEvent> streamController = StreamController<ImageChunkEvent>();
    final ImageStreamCompleter imageStream = FakeEventReportingImageStreamCompleter(
      chunkEvents: streamController.stream,
    );

    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3));
    await tester.idle();
    imageStream.addListener(ImageStreamListener(
      (ImageInfo image, bool synchronousCall) { },
      onChunk: (ImageChunkEvent event) {
        chunkEvents.add(event);
      },
    ));
    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3));
    await tester.idle();

    expect(chunkEvents.length, 1);
    expect(chunkEvents[0].cumulativeBytesLoaded, 2);
    expect(chunkEvents[0].expectedTotalBytes, 3);
  });

  testWidgets('Chunk events of MultiFrameImageStreamCompleter are delivered', (WidgetTester tester) async {
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    final List<ImageChunkEvent> chunkEvents = <ImageChunkEvent>[];
    final Completer<Codec> completer = Completer<Codec>();
    final StreamController<ImageChunkEvent> streamController = StreamController<ImageChunkEvent>();
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
      codec: completer.future,
      chunkEvents: streamController.stream,
      scale: 1.0,
    );

    imageStream.addListener(ImageStreamListener(
      (ImageInfo image, bool synchronousCall) { },
      onChunk: (ImageChunkEvent event) {
        chunkEvents.add(event);
      },
    ));
    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3));
    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3));
    await tester.idle();

    expect(chunkEvents.length, 2);
    expect(chunkEvents[0].cumulativeBytesLoaded, 1);
    expect(chunkEvents[0].expectedTotalBytes, 3);
    expect(chunkEvents[1].cumulativeBytesLoaded, 2);
    expect(chunkEvents[1].expectedTotalBytes, 3);
  });

203
  testWidgets('Chunk events of MultiFrameImageStreamCompleter are not buffered before listener registration', (WidgetTester tester) async {
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
    final List<ImageChunkEvent> chunkEvents = <ImageChunkEvent>[];
    final Completer<Codec> completer = Completer<Codec>();
    final StreamController<ImageChunkEvent> streamController = StreamController<ImageChunkEvent>();
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
      codec: completer.future,
      chunkEvents: streamController.stream,
      scale: 1.0,
    );

    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3));
    await tester.idle();
    imageStream.addListener(ImageStreamListener(
      (ImageInfo image, bool synchronousCall) { },
      onChunk: (ImageChunkEvent event) {
        chunkEvents.add(event);
      },
    ));
    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3));
    await tester.idle();

    expect(chunkEvents.length, 1);
    expect(chunkEvents[0].cumulativeBytesLoaded, 2);
    expect(chunkEvents[0].expectedTotalBytes, 3);
  });

  testWidgets('Chunk errors are reported', (WidgetTester tester) async {
    final List<ImageChunkEvent> chunkEvents = <ImageChunkEvent>[];
    final Completer<Codec> completer = Completer<Codec>();
    final StreamController<ImageChunkEvent> streamController = StreamController<ImageChunkEvent>();
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
      codec: completer.future,
      chunkEvents: streamController.stream,
      scale: 1.0,
    );

    imageStream.addListener(ImageStreamListener(
      (ImageInfo image, bool synchronousCall) { },
      onChunk: (ImageChunkEvent event) {
        chunkEvents.add(event);
      },
    ));
    streamController.addError(Error());
    streamController.add(const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3));
    await tester.idle();

    expect(tester.takeException(), isNotNull);
    expect(chunkEvents.length, 1);
    expect(chunkEvents[0].cumulativeBytesLoaded, 2);
    expect(chunkEvents[0].expectedTotalBytes, 3);
  });

255
  testWidgets('getNextFrame future fails', (WidgetTester tester) async {
256 257 258 259
    final MockCodec mockCodec = MockCodec();
    mockCodec.frameCount = 1;
    final Completer<Codec> codecCompleter = Completer<Codec>();

260
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
261 262 263 264
      codec: codecCompleter.future,
      scale: 1.0,
    );

265
    final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
266
    imageStream.addListener(ImageStreamListener(listener));
267 268 269 270 271 272 273 274 275 276 277 278
    codecCompleter.complete(mockCodec);
    // MultiFrameImageStreamCompleter only sets an error handler for the next
    // frame future after the codec future has completed.
    // Idling here lets the MultiFrameImageStreamCompleter advance and set the
    // error handler for the nextFrame future.
    await tester.idle();

    mockCodec.failNextFrame('frame completion error');
    await tester.idle();

    expect(tester.takeException(), 'frame completion error');
  });
279 280

  testWidgets('ImageStream emits frame (static image)', (WidgetTester tester) async {
281
    final MockCodec mockCodec = MockCodec();
282
    mockCodec.frameCount = 1;
283
    final Completer<Codec> codecCompleter = Completer<Codec>();
284

285
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
286 287 288 289 290
      codec: codecCompleter.future,
      scale: 1.0,
    );

    final List<ImageInfo> emittedImages = <ImageInfo>[];
291
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
292
      emittedImages.add(image);
293
    }));
294 295 296 297

    codecCompleter.complete(mockCodec);
    await tester.idle();

298
    final FrameInfo frame = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
299 300 301
    mockCodec.completeNextFrame(frame);
    await tester.idle();

302
    expect(emittedImages, equals(<ImageInfo>[ImageInfo(image: frame.image)]));
303 304
  });

305
  testWidgets('ImageStream emits frames (animated images)', (WidgetTester tester) async {
306
    final MockCodec mockCodec = MockCodec();
307 308
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
309
    final Completer<Codec> codecCompleter = Completer<Codec>();
310

311
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
312 313 314
      codec: codecCompleter.future,
      scale: 1.0,
    );
315

316
    final List<ImageInfo> emittedImages = <ImageInfo>[];
317
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
318
      emittedImages.add(image);
319
    }));
320

321 322
    codecCompleter.complete(mockCodec);
    await tester.idle();
323

324
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
325 326 327 328 329 330 331
    mockCodec.completeNextFrame(frame1);
    await tester.idle();
    // We are waiting for the next animation tick, so at this point no frames
    // should have been emitted.
    expect(emittedImages.length, 0);

    await tester.pump();
332
    expect(emittedImages, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
333

334
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
335 336 337 338 339 340 341 342 343
    mockCodec.completeNextFrame(frame2);

    await tester.pump(const Duration(milliseconds: 100));
    // The duration for the current frame was 200ms, so we don't emit the next
    // frame yet even though it is ready.
    expect(emittedImages.length, 1);

    await tester.pump(const Duration(milliseconds: 100));
    expect(emittedImages, equals(<ImageInfo>[
344 345
      ImageInfo(image: frame1.image),
      ImageInfo(image: frame2.image),
346 347 348 349 350 351
    ]));

    // Let the pending timer for the next frame to complete so we can cleanly
    // quit the test without pending timers.
    await tester.pump(const Duration(milliseconds: 400));
  });
352

353
  testWidgets('animation wraps back', (WidgetTester tester) async {
354
    final MockCodec mockCodec = MockCodec();
355 356
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
357
    final Completer<Codec> codecCompleter = Completer<Codec>();
358

359
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
360 361 362
      codec: codecCompleter.future,
      scale: 1.0,
    );
363

364
    final List<ImageInfo> emittedImages = <ImageInfo>[];
365
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
366
      emittedImages.add(image);
367
    }));
368

369 370
    codecCompleter.complete(mockCodec);
    await tester.idle();
371

372 373
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
374 375 376 377 378 379 380 381 382 383 384 385

    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.
    mockCodec.completeNextFrame(frame2);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(const Duration(milliseconds: 400)); // emit 3rd frame

    expect(emittedImages, equals(<ImageInfo>[
386 387 388
      ImageInfo(image: frame1.image),
      ImageInfo(image: frame2.image),
      ImageInfo(image: frame1.image),
389 390 391 392 393 394
    ]));

    // Let the pending timer for the next frame to complete so we can cleanly
    // quit the test without pending timers.
    await tester.pump(const Duration(milliseconds: 200));
  });
395

396
  testWidgets('animation doesnt repeat more than specified', (WidgetTester tester) async {
397
    final MockCodec mockCodec = MockCodec();
398 399
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = 0;
400
    final Completer<Codec> codecCompleter = Completer<Codec>();
401

402
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
403 404 405
      codec: codecCompleter.future,
      scale: 1.0,
    );
406

407
    final List<ImageInfo> emittedImages = <ImageInfo>[];
408
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
409
      emittedImages.add(image);
410
    }));
411

412 413
    codecCompleter.complete(mockCodec);
    await tester.idle();
414

415 416
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
417 418 419 420 421 422 423 424 425 426 427 428

    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.
    mockCodec.completeNextFrame(frame2);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
    mockCodec.completeNextFrame(frame1);
    // allow another frame to complete (but we shouldn't be asking for it as
    // this animation should not repeat.
    await tester.idle();
    await tester.pump(const Duration(milliseconds: 400));
429

430
    expect(emittedImages, equals(<ImageInfo>[
431 432
      ImageInfo(image: frame1.image),
      ImageInfo(image: frame2.image),
433 434
    ]));
  });
435

436
  testWidgets('frames are only decoded when there are active listeners', (WidgetTester tester) async {
437
    final MockCodec mockCodec = MockCodec();
438 439
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
440
    final Completer<Codec> codecCompleter = Completer<Codec>();
441

442
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
443 444 445
      codec: codecCompleter.future,
      scale: 1.0,
    );
446

447
    final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
448
    imageStream.addListener(ImageStreamListener(listener));
449

450 451
    codecCompleter.complete(mockCodec);
    await tester.idle();
452

453 454
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
455

456 457 458 459
    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.
    mockCodec.completeNextFrame(frame2);
460
    imageStream.removeListener(ImageStreamListener(listener));
461 462
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
463

464 465 466
    // Decoding of the 3rd frame should not start as there are no registered
    // listeners to the stream
    expect(mockCodec.numFramesAsked, 2);
467

468
    imageStream.addListener(ImageStreamListener(listener));
469 470 471
    await tester.idle(); // let nextFrameFuture complete
    expect(mockCodec.numFramesAsked, 3);
  });
472

473
  testWidgets('multiple stream listeners', (WidgetTester tester) async {
474
    final MockCodec mockCodec = MockCodec();
475 476
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
477
    final Completer<Codec> codecCompleter = Completer<Codec>();
478

479
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
480 481 482
      codec: codecCompleter.future,
      scale: 1.0,
    );
483

484 485 486 487 488 489 490 491
    final List<ImageInfo> emittedImages1 = <ImageInfo>[];
    final ImageListener listener1 = (ImageInfo image, bool synchronousCall) {
      emittedImages1.add(image);
    };
    final List<ImageInfo> emittedImages2 = <ImageInfo>[];
    final ImageListener listener2 = (ImageInfo image, bool synchronousCall) {
      emittedImages2.add(image);
    };
492 493
    imageStream.addListener(ImageStreamListener(listener1));
    imageStream.addListener(ImageStreamListener(listener2));
494

495 496
    codecCompleter.complete(mockCodec);
    await tester.idle();
497

498 499
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
500 501 502 503

    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.
504 505
    expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
    expect(emittedImages2, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
506 507 508 509

    mockCodec.completeNextFrame(frame2);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // next app frame will schedule a timer.
510
    imageStream.removeListener(ImageStreamListener(listener1));
511 512

    await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
513
    expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
514
    expect(emittedImages2, equals(<ImageInfo>[
515 516
      ImageInfo(image: frame1.image),
      ImageInfo(image: frame2.image),
517 518
    ]));
  });
519

520
  testWidgets('timer is canceled when listeners are removed', (WidgetTester tester) async {
521
    final MockCodec mockCodec = MockCodec();
522 523
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
524
    final Completer<Codec> codecCompleter = Completer<Codec>();
525

526
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
527 528 529
      codec: codecCompleter.future,
      scale: 1.0,
    );
530

531
    final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
532
    imageStream.addListener(ImageStreamListener(listener));
533

534 535
    codecCompleter.complete(mockCodec);
    await tester.idle();
536

537 538
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
539

540 541 542
    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.
543

544 545 546
    mockCodec.completeNextFrame(frame2);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump();
547

548
    imageStream.removeListener(ImageStreamListener(listener));
549 550 551
    // The test framework will fail this if there are pending timers at this
    // point.
  });
552

553
  testWidgets('timeDilation affects animation frame timers', (WidgetTester tester) async {
554
    final MockCodec mockCodec = MockCodec();
555 556
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
557
    final Completer<Codec> codecCompleter = Completer<Codec>();
558

559
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
560 561 562
      codec: codecCompleter.future,
      scale: 1.0,
    );
563

564
    final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
565
    imageStream.addListener(ImageStreamListener(listener));
566

567 568
    codecCompleter.complete(mockCodec);
    await tester.idle();
569

570 571
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587

    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.

    timeDilation = 2.0;
    mockCodec.completeNextFrame(frame2);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // schedule next app frame
    await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
    // Decoding of the 3rd frame should not start after 200 ms, as time is
    // dilated by a factor of 2.
    expect(mockCodec.numFramesAsked, 2);
    await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
    expect(mockCodec.numFramesAsked, 3);
  });
588

589
  testWidgets('error handlers can intercept errors', (WidgetTester tester) async {
590
    final MockCodec mockCodec = MockCodec();
591
    mockCodec.frameCount = 1;
592
    final Completer<Codec> codecCompleter = Completer<Codec>();
593

594
    final ImageStreamCompleter streamUnderTest = MultiFrameImageStreamCompleter(
595 596 597
      codec: codecCompleter.future,
      scale: 1.0,
    );
598

599 600 601 602 603
    dynamic capturedException;
    final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
      capturedException = exception;
    };

604
    streamUnderTest.addListener(ImageStreamListener(
605
      (ImageInfo image, bool synchronousCall) { },
606
      onError: errorListener,
607
    ));
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622

    codecCompleter.complete(mockCodec);
    // MultiFrameImageStreamCompleter only sets an error handler for the next
    // frame future after the codec future has completed.
    // Idling here lets the MultiFrameImageStreamCompleter advance and set the
    // error handler for the nextFrame future.
    await tester.idle();

    mockCodec.failNextFrame('frame completion error');
    await tester.idle();

    // No exception is passed up.
    expect(tester.takeException(), isNull);
    expect(capturedException, 'frame completion error');
  });
623 624 625 626 627 628 629 630 631 632 633 634 635

  testWidgets('remove and add listener ', (WidgetTester tester) async {
    final MockCodec mockCodec = MockCodec();
    mockCodec.frameCount = 3;
    mockCodec.repetitionCount = 0;
    final Completer<Codec> codecCompleter = Completer<Codec>();

    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
      codec: codecCompleter.future,
      scale: 1.0,
    );

    final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
636
    imageStream.addListener(ImageStreamListener(listener));
637 638 639 640 641

    codecCompleter.complete(mockCodec);

    await tester.idle(); // let nextFrameFuture complete

642 643
    imageStream.removeListener(ImageStreamListener(listener));
    imageStream.addListener(ImageStreamListener(listener));
644 645


646
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
647 648 649 650 651 652 653 654

    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.

    await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
  });

655
  testWidgets('ImageStreamListener hashCode and equals', (WidgetTester tester) async {
656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
    void handleImage(ImageInfo image, bool synchronousCall) { }
    void handleImageDifferently(ImageInfo image, bool synchronousCall) { }
    void handleError(dynamic error, StackTrace stackTrace) { }
    void handleChunk(ImageChunkEvent event) { }

    void compare({
      @required ImageListener onImage1,
      @required ImageListener onImage2,
      ImageChunkListener onChunk1,
      ImageChunkListener onChunk2,
      ImageErrorListener onError1,
      ImageErrorListener onError2,
      bool areEqual = true,
    }) {
      final ImageStreamListener l1 = ImageStreamListener(onImage1, onChunk: onChunk1, onError: onError1);
      final ImageStreamListener l2 = ImageStreamListener(onImage2, onChunk: onChunk2, onError: onError2);
      Matcher comparison(dynamic expected) => areEqual ? equals(expected) : isNot(equals(expected));
      expect(l1, comparison(l2));
      expect(l1.hashCode, comparison(l2.hashCode));
    }

    compare(onImage1: handleImage, onImage2: handleImage);
    compare(onImage1: handleImage, onImage2: handleImageDifferently, areEqual: false);
    compare(onImage1: handleImage, onChunk1: handleChunk, onImage2: handleImage, onChunk2: handleChunk);
    compare(onImage1: handleImage, onChunk1: handleChunk, onError1: handleError, onImage2: handleImage, onChunk2: handleChunk, onError2: handleError);
    compare(onImage1: handleImage, onChunk1: handleChunk, onImage2: handleImage, areEqual: false);
    compare(onImage1: handleImage, onChunk1: handleChunk, onError1: handleError, onImage2: handleImage, areEqual: false);
    compare(onImage1: handleImage, onChunk1: handleChunk, onError1: handleError, onImage2: handleImage, onChunk2: handleChunk, areEqual: false);
    compare(onImage1: handleImage, onChunk1: handleChunk, onError1: handleError, onImage2: handleImage, onError2: handleError, areEqual: false);
  });

687 688 689 690 691 692 693 694 695 696 697 698 699 700
  // TODO(amirh): enable this once WidgetTester supports flushTimers.
  // https://github.com/flutter/flutter/issues/30344
  // testWidgets('remove and add listener before a delayed frame is scheduled', (WidgetTester tester) async {
  //   final MockCodec mockCodec = MockCodec();
  //   mockCodec.frameCount = 3;
  //   mockCodec.repetitionCount = 0;
  //   final Completer<Codec> codecCompleter = Completer<Codec>();
  //
  //   final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
  //     codec: codecCompleter.future,
  //     scale: 1.0,
  //   );
  //
  //   final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
701
  //   imageStream.addListener(ImageLoadingListener(listener));
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
  //
  //   codecCompleter.complete(mockCodec);
  //   await tester.idle();
  //
  //   final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
  //   final FrameInfo frame2 = FakeFrameInfo(200, 100, const Duration(milliseconds: 400));
  //   final FrameInfo frame3 = FakeFrameInfo(200, 100, const Duration(milliseconds: 0));
  //
  //   mockCodec.completeNextFrame(frame1);
  //   await tester.idle(); // let nextFrameFuture complete
  //   await tester.pump(); // first animation frame shows on first app frame.
  //
  //   mockCodec.completeNextFrame(frame2);
  //   await tester.pump(const Duration(milliseconds: 100)); // emit 2nd frame.
  //
  //   tester.flushTimers();
  //
  //   imageStream.removeListener(listener);
720
  //   imageStream.addListener(ImageLoadingListener(listener));
721 722 723 724 725 726
  //
  //   mockCodec.completeNextFrame(frame3);
  //   await tester.idle(); // let nextFrameFuture complete
  //
  //   await tester.pump();
  // });
727
}