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

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

import 'package:flutter/painting.dart';
9
import 'package:flutter/scheduler.dart' show timeDilation, SchedulerBinding;
10 11
import 'package:flutter_test/flutter_test.dart';

12
class FakeFrameInfo implements FrameInfo {
13
  const FakeFrameInfo(this._duration, this._image);
14

15 16 17
  final Duration _duration;
  final Image _image;

18 19 20 21 22
  @override
  Duration get duration => _duration;

  @override
  Image get image => _image;
23

24
  int get imageHandleCount => image.debugGetOpenHandleStackTraces()!.length;
25 26 27 28 29 30 31

  FakeFrameInfo clone() {
    return FakeFrameInfo(
      _duration,
      _image.clone(),
    );
  }
32 33 34 35
}

class MockCodec implements Codec {
  @override
36
  late int frameCount;
37 38

  @override
39
  late int repetitionCount;
40 41 42

  int numFramesAsked = 0;

43
  Completer<FrameInfo> _nextFrameCompleter = Completer<FrameInfo>();
44 45 46 47 48 49 50 51 52

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

  void completeNextFrame(FrameInfo frameInfo) {
    _nextFrameCompleter.complete(frameInfo);
53
    _nextFrameCompleter = Completer<FrameInfo>();
54 55 56 57 58 59 60
  }

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

  @override
61
  void dispose() { }
62 63 64

}

65
class FakeEventReportingImageStreamCompleter extends ImageStreamCompleter {
66
  FakeEventReportingImageStreamCompleter({Stream<ImageChunkEvent>? chunkEvents}) {
67 68 69 70 71 72 73 74 75
    if (chunkEvents != null) {
      chunkEvents.listen((ImageChunkEvent event) {
          reportImageChunkEvent(event);
        },
      );
    }
  }
}

76
void main() {
77 78
  late Image image20x10;
  late Image image200x100;
79
  setUp(() async {
80 81 82 83
    image20x10 = await createTestImage(width: 20, height: 10);
    image200x100 = await createTestImage(width: 200, height: 100);
  });

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

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

104 105 106 107
    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 0);

108
    void listener(ImageInfo image, bool synchronousCall) { }
109
    imageStream.addListener(ImageStreamListener(listener));
110 111 112 113 114 115 116 117 118 119 120 121 122
    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,
    );

123
    void listener(ImageInfo image, bool synchronousCall) { }
124
    imageStream.addListener(ImageStreamListener(listener));
125 126 127
    await tester.idle();
    expect(mockCodec.numFramesAsked, 0);

128 129 130 131 132
    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 1);
  });

133 134 135 136 137 138 139 140 141 142 143 144 145
  testWidgets('Decoding does not crash when disposed', (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,
    );

    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 0);

146
    void listener(ImageInfo image, bool synchronousCall) { }
147 148 149 150 151 152 153 154 155 156 157
    final ImageStreamListener streamListener = ImageStreamListener(listener);
    imageStream.addListener(streamListener);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 1);

    final FrameInfo frame = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    mockCodec.completeNextFrame(frame);
    imageStream.removeListener(streamListener);
    await tester.idle();
  });

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 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 203 204 205
  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 {
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
    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);
  });

232
  testWidgets('Chunk events of MultiFrameImageStreamCompleter are not buffered before listener registration', (WidgetTester tester) async {
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
    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);
  });

284
  testWidgets('getNextFrame future fails', (WidgetTester tester) async {
285 286 287 288
    final MockCodec mockCodec = MockCodec();
    mockCodec.frameCount = 1;
    final Completer<Codec> codecCompleter = Completer<Codec>();

289
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
290 291 292 293
      codec: codecCompleter.future,
      scale: 1.0,
    );

294
    void listener(ImageInfo image, bool synchronousCall) { }
295
    imageStream.addListener(ImageStreamListener(listener));
296 297 298 299 300 301 302 303 304 305 306 307
    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');
  });
308 309

  testWidgets('ImageStream emits frame (static image)', (WidgetTester tester) async {
310
    final MockCodec mockCodec = MockCodec();
311
    mockCodec.frameCount = 1;
312
    final Completer<Codec> codecCompleter = Completer<Codec>();
313

314
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
315 316 317 318 319
      codec: codecCompleter.future,
      scale: 1.0,
    );

    final List<ImageInfo> emittedImages = <ImageInfo>[];
320
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
321
      emittedImages.add(image);
322
    }));
323 324 325 326

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

327
    final FrameInfo frame = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
328 329 330
    mockCodec.completeNextFrame(frame);
    await tester.idle();

331
    expect(emittedImages.every((ImageInfo info) => info.image.isCloneOf(frame.image)), true);
332 333
  });

334
  testWidgets('ImageStream emits frames (animated images)', (WidgetTester tester) async {
335
    final MockCodec mockCodec = MockCodec();
336 337
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
338
    final Completer<Codec> codecCompleter = Completer<Codec>();
339

340
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
341 342 343
      codec: codecCompleter.future,
      scale: 1.0,
    );
344

345
    final List<ImageInfo> emittedImages = <ImageInfo>[];
346
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
347
      emittedImages.add(image);
348
    }));
349

350 351
    codecCompleter.complete(mockCodec);
    await tester.idle();
352

353
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
354 355 356 357 358 359 360
    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();
361
    expect(emittedImages.single.image.isCloneOf(frame1.image), true);
362

363
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
364 365 366 367 368 369 370 371
    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));
372 373
    expect(emittedImages[0].image.isCloneOf(frame1.image), true);
    expect(emittedImages[1].image.isCloneOf(frame2.image), true);
374 375 376 377 378

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

380
  testWidgets('animation wraps back', (WidgetTester tester) async {
381
    final MockCodec mockCodec = MockCodec();
382 383
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
384
    final Completer<Codec> codecCompleter = Completer<Codec>();
385

386
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
387 388 389
      codec: codecCompleter.future,
      scale: 1.0,
    );
390

391
    final List<ImageInfo> emittedImages = <ImageInfo>[];
392
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
393
      emittedImages.add(image);
394
    }));
395

396 397
    codecCompleter.complete(mockCodec);
    await tester.idle();
398

399 400
    final FakeFrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FakeFrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
401

402
    mockCodec.completeNextFrame(frame1.clone());
403 404
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.
405
    mockCodec.completeNextFrame(frame2.clone());
406 407
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
408
    mockCodec.completeNextFrame(frame1.clone());
409 410 411
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(const Duration(milliseconds: 400)); // emit 3rd frame

412 413 414
    expect(emittedImages[0].image.isCloneOf(frame1.image), true);
    expect(emittedImages[1].image.isCloneOf(frame2.image), true);
    expect(emittedImages[2].image.isCloneOf(frame1.image), true);
415 416 417 418 419

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

421
  testWidgets('animation doesnt repeat more than specified', (WidgetTester tester) async {
422
    final MockCodec mockCodec = MockCodec();
423 424
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = 0;
425
    final Completer<Codec> codecCompleter = Completer<Codec>();
426

427
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
428 429 430
      codec: codecCompleter.future,
      scale: 1.0,
    );
431

432
    final List<ImageInfo> emittedImages = <ImageInfo>[];
433
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
434
      emittedImages.add(image);
435
    }));
436

437 438
    codecCompleter.complete(mockCodec);
    await tester.idle();
439

440 441
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
442 443 444 445 446 447 448 449 450 451 452 453

    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));
454

455 456
    expect(emittedImages[0].image.isCloneOf(frame1.image), true);
    expect(emittedImages[1].image.isCloneOf(frame2.image), true);
457
  });
458

459
  testWidgets('frames are only decoded when there are listeners', (WidgetTester tester) async {
460
    final MockCodec mockCodec = MockCodec();
461 462
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
463
    final Completer<Codec> codecCompleter = Completer<Codec>();
464

465
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
466 467 468
      codec: codecCompleter.future,
      scale: 1.0,
    );
469

470
    void listener(ImageInfo image, bool synchronousCall) { }
471
    imageStream.addListener(ImageStreamListener(listener));
472
    final ImageStreamCompleterHandle handle = imageStream.keepAlive();
473

474 475
    codecCompleter.complete(mockCodec);
    await tester.idle();
476

477 478
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
479

480 481 482 483
    mockCodec.completeNextFrame(frame1);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(); // first animation frame shows on first app frame.
    mockCodec.completeNextFrame(frame2);
484
    imageStream.removeListener(ImageStreamListener(listener));
485 486
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
487

488 489 490
    // Decoding of the 3rd frame should not start as there are no registered
    // listeners to the stream
    expect(mockCodec.numFramesAsked, 2);
491

492
    imageStream.addListener(ImageStreamListener(listener));
493 494
    await tester.idle(); // let nextFrameFuture complete
    expect(mockCodec.numFramesAsked, 3);
495 496

    handle.dispose();
497
  });
498

499
  testWidgets('multiple stream listeners', (WidgetTester tester) async {
500
    final MockCodec mockCodec = MockCodec();
501 502
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
503
    final Completer<Codec> codecCompleter = Completer<Codec>();
504

505
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
506 507 508
      codec: codecCompleter.future,
      scale: 1.0,
    );
509

510
    final List<ImageInfo> emittedImages1 = <ImageInfo>[];
511
    void listener1(ImageInfo image, bool synchronousCall) {
512
      emittedImages1.add(image);
513
    }
514
    final List<ImageInfo> emittedImages2 = <ImageInfo>[];
515
    void listener2(ImageInfo image, bool synchronousCall) {
516
      emittedImages2.add(image);
517
    }
518 519
    imageStream.addListener(ImageStreamListener(listener1));
    imageStream.addListener(ImageStreamListener(listener2));
520

521 522
    codecCompleter.complete(mockCodec);
    await tester.idle();
523

524 525
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
526 527 528 529

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

    expect(emittedImages1.single.image.isCloneOf(frame1.image), true);
    expect(emittedImages2.single.image.isCloneOf(frame1.image), true);
533 534 535 536

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

    await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
540 541 542 543
    expect(emittedImages1.single.image.isCloneOf(frame1.image), true);
    expect(emittedImages2[0].image.isCloneOf(frame1.image), true);
    expect(emittedImages2[1].image.isCloneOf(frame2.image), true);

544
  });
545

546
  testWidgets('timer is canceled when listeners are removed', (WidgetTester tester) async {
547
    final MockCodec mockCodec = MockCodec();
548 549
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
550
    final Completer<Codec> codecCompleter = Completer<Codec>();
551

552
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
553 554 555
      codec: codecCompleter.future,
      scale: 1.0,
    );
556

557
    void listener(ImageInfo image, bool synchronousCall) { }
558
    imageStream.addListener(ImageStreamListener(listener));
559

560 561
    codecCompleter.complete(mockCodec);
    await tester.idle();
562

563 564
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
565

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

570 571 572
    mockCodec.completeNextFrame(frame2);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump();
573

574
    imageStream.removeListener(ImageStreamListener(listener));
575 576 577
    // The test framework will fail this if there are pending timers at this
    // point.
  });
578

579
  testWidgets('timeDilation affects animation frame timers', (WidgetTester tester) async {
580
    final MockCodec mockCodec = MockCodec();
581 582
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
583
    final Completer<Codec> codecCompleter = Completer<Codec>();
584

585
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
586 587 588
      codec: codecCompleter.future,
      scale: 1.0,
    );
589

590
    void listener(ImageInfo image, bool synchronousCall) { }
591
    imageStream.addListener(ImageStreamListener(listener));
592

593 594
    codecCompleter.complete(mockCodec);
    await tester.idle();
595

596 597
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613

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

615
  testWidgets('error handlers can intercept errors', (WidgetTester tester) async {
616
    final MockCodec mockCodec = MockCodec();
617
    mockCodec.frameCount = 1;
618
    final Completer<Codec> codecCompleter = Completer<Codec>();
619

620
    final ImageStreamCompleter streamUnderTest = MultiFrameImageStreamCompleter(
621 622 623
      codec: codecCompleter.future,
      scale: 1.0,
    );
624

625
    dynamic capturedException;
626
    void errorListener(dynamic exception, StackTrace? stackTrace) {
627
      capturedException = exception;
628
    }
629

630
    streamUnderTest.addListener(ImageStreamListener(
631
      (ImageInfo image, bool synchronousCall) { },
632
      onError: errorListener,
633
    ));
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648

    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');
  });
649 650 651 652 653 654 655 656 657 658 659 660

  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,
    );

661
    void listener(ImageInfo image, bool synchronousCall) { }
662
    imageStream.addListener(ImageStreamListener(listener));
663 664 665 666 667

    codecCompleter.complete(mockCodec);

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

668
    imageStream.addListener(ImageStreamListener(listener));
669
    imageStream.removeListener(ImageStreamListener(listener));
670 671


672
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
673 674 675 676 677 678 679 680

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

681
  testWidgets('ImageStreamListener hashCode and equals', (WidgetTester tester) async {
682 683
    void handleImage(ImageInfo image, bool synchronousCall) { }
    void handleImageDifferently(ImageInfo image, bool synchronousCall) { }
684
    void handleError(dynamic error, StackTrace? stackTrace) { }
685 686 687
    void handleChunk(ImageChunkEvent event) { }

    void compare({
688 689 690 691 692 693
      required ImageListener onImage1,
      required ImageListener onImage2,
      ImageChunkListener? onChunk1,
      ImageChunkListener? onChunk2,
      ImageErrorListener? onError1,
      ImageErrorListener? onError2,
694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
      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);
  });

713
  testWidgets('Keep alive handles do not drive frames or prevent last listener callbacks', (WidgetTester tester) async {
714
    final Image image10x10 = (await tester.runAsync(() => createTestImage(width: 10, height: 10)))!;
715 716 717 718 719 720 721 722 723 724 725
    final MockCodec mockCodec = MockCodec();
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
    final Completer<Codec> codecCompleter = Completer<Codec>();

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

    int onImageCount = 0;
726
    void activeListener(ImageInfo image, bool synchronousCall) {
727
      onImageCount += 1;
728
    }
729 730 731 732 733 734 735 736
    bool lastListenerDropped = false;
    imageStream.addOnLastListenerRemovedCallback(() {
      lastListenerDropped = true;
    });

    expect(lastListenerDropped, false);
    final ImageStreamCompleterHandle handle = imageStream.keepAlive();
    expect(lastListenerDropped, false);
737
    SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Only passive listeners');
738 739 740 741 742 743 744 745 746

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

    expect(onImageCount, 0);

    final FakeFrameInfo frame1 = FakeFrameInfo(Duration.zero, image20x10);
    mockCodec.completeNextFrame(frame1);
    await tester.idle();
747
    SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Only passive listeners');
748 749 750 751 752 753 754 755
    await tester.pump();
    expect(onImageCount, 0);

    imageStream.addListener(ImageStreamListener(activeListener));

    final FakeFrameInfo frame2 = FakeFrameInfo(Duration.zero, image10x10);
    mockCodec.completeNextFrame(frame2);
    await tester.idle();
756
    expect(SchedulerBinding.instance!.transientCallbackCount, 1);
757 758 759 760 761 762 763 764 765
    await tester.pump();

    expect(onImageCount, 1);

    imageStream.removeListener(ImageStreamListener(activeListener));
    expect(lastListenerDropped, true);

    mockCodec.completeNextFrame(frame1);
    await tester.idle();
766
    expect(SchedulerBinding.instance!.transientCallbackCount, 1);
767 768 769 770
    await tester.pump();

    expect(onImageCount, 1);

771
    SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Only passive listeners');
772 773 774

    mockCodec.completeNextFrame(frame2);
    await tester.idle();
775
    SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Only passive listeners');
776 777 778 779 780 781 782
    await tester.pump();

    expect(onImageCount, 1);

    handle.dispose();
  });

783 784 785 786 787 788 789 790 791 792 793 794 795 796
  // 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) { };
797
  //   imageStream.addListener(ImageLoadingListener(listener));
798 799 800 801 802 803
  //
  //   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));
804
  //   final FrameInfo frame3 = FakeFrameInfo(200, 100, Duration.zero);
805 806 807 808 809 810 811 812 813 814 815
  //
  //   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);
816
  //   imageStream.addListener(ImageLoadingListener(listener));
817 818 819 820 821 822
  //
  //   mockCodec.completeNextFrame(frame3);
  //   await tester.idle(); // let nextFrameFuture complete
  //
  //   await tester.pump();
  // });
823
}