image_stream_test.dart 31.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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';
8 9

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

13 14 15
import '../image_data.dart';
import 'fake_codec.dart';

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

19 20 21
  final Duration _duration;
  final Image _image;

22 23 24 25 26
  @override
  Duration get duration => _duration;

  @override
  Image get image => _image;
27

28
  int get imageHandleCount => image.debugGetOpenHandleStackTraces()!.length;
29 30 31 32 33 34 35

  FakeFrameInfo clone() {
    return FakeFrameInfo(
      _duration,
      _image.clone(),
    );
  }
36 37 38 39
}

class MockCodec implements Codec {
  @override
40
  late int frameCount;
41 42

  @override
43
  late int repetitionCount;
44 45 46

  int numFramesAsked = 0;

47
  Completer<FrameInfo> _nextFrameCompleter = Completer<FrameInfo>();
48 49 50 51 52 53 54 55 56

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

  void completeNextFrame(FrameInfo frameInfo) {
    _nextFrameCompleter.complete(frameInfo);
57
    _nextFrameCompleter = Completer<FrameInfo>();
58 59 60 61 62 63 64
  }

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

  @override
65
  void dispose() { }
66 67 68

}

69
class FakeEventReportingImageStreamCompleter extends ImageStreamCompleter {
70
  FakeEventReportingImageStreamCompleter({Stream<ImageChunkEvent>? chunkEvents}) {
71 72 73 74 75 76 77 78 79
    if (chunkEvents != null) {
      chunkEvents.listen((ImageChunkEvent event) {
          reportImageChunkEvent(event);
        },
      );
    }
  }
}

80
void main() {
81 82
  late Image image20x10;
  late Image image200x100;
83
  setUp(() async {
84 85 86 87
    image20x10 = await createTestImage(width: 20, height: 10);
    image200x100 = await createTestImage(width: 200, height: 100);
  });

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

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

108 109 110 111
    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 0);

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

127
    void listener(ImageInfo image, bool synchronousCall) { }
128
    imageStream.addListener(ImageStreamListener(listener));
129 130 131
    await tester.idle();
    expect(mockCodec.numFramesAsked, 0);

132 133 134 135 136
    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 1);
  });

137 138 139 140 141 142 143 144 145 146 147 148 149
  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);

150
    void listener(ImageInfo image, bool synchronousCall) { }
151 152 153 154 155 156 157 158 159 160 161
    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();
  });

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 206 207 208 209
  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 {
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
    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);
  });

236
  testWidgets('Chunk events of MultiFrameImageStreamCompleter are not buffered before listener registration', (WidgetTester tester) async {
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 284 285 286 287
    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);
  });

288
  testWidgets('getNextFrame future fails', (WidgetTester tester) async {
289 290 291 292
    final MockCodec mockCodec = MockCodec();
    mockCodec.frameCount = 1;
    final Completer<Codec> codecCompleter = Completer<Codec>();

293
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
294 295 296 297
      codec: codecCompleter.future,
      scale: 1.0,
    );

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

  testWidgets('ImageStream emits frame (static image)', (WidgetTester tester) async {
314
    final MockCodec mockCodec = MockCodec();
315
    mockCodec.frameCount = 1;
316
    final Completer<Codec> codecCompleter = Completer<Codec>();
317

318
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
319 320 321 322 323
      codec: codecCompleter.future,
      scale: 1.0,
    );

    final List<ImageInfo> emittedImages = <ImageInfo>[];
324
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
325
      emittedImages.add(image);
326
    }));
327 328 329 330

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

331
    final FrameInfo frame = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
332 333 334
    mockCodec.completeNextFrame(frame);
    await tester.idle();

335
    expect(emittedImages.every((ImageInfo info) => info.image.isCloneOf(frame.image)), true);
336 337
  });

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

344
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
345 346 347
      codec: codecCompleter.future,
      scale: 1.0,
    );
348

349
    final List<ImageInfo> emittedImages = <ImageInfo>[];
350
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
351
      emittedImages.add(image);
352
    }));
353

354 355
    codecCompleter.complete(mockCodec);
    await tester.idle();
356

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

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

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

384
  testWidgets('animation wraps back', (WidgetTester tester) async {
385
    final MockCodec mockCodec = MockCodec();
386 387
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
388
    final Completer<Codec> codecCompleter = Completer<Codec>();
389

390
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
391 392 393
      codec: codecCompleter.future,
      scale: 1.0,
    );
394

395
    final List<ImageInfo> emittedImages = <ImageInfo>[];
396
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
397
      emittedImages.add(image);
398
    }));
399

400 401
    codecCompleter.complete(mockCodec);
    await tester.idle();
402

403 404
    final FakeFrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FakeFrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
405

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

416 417 418
    expect(emittedImages[0].image.isCloneOf(frame1.image), true);
    expect(emittedImages[1].image.isCloneOf(frame2.image), true);
    expect(emittedImages[2].image.isCloneOf(frame1.image), true);
419 420 421 422 423

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

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

431
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
432 433 434
      codec: codecCompleter.future,
      scale: 1.0,
    );
435

436
    final List<ImageInfo> emittedImages = <ImageInfo>[];
437
    imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
438
      emittedImages.add(image);
439
    }));
440

441 442
    codecCompleter.complete(mockCodec);
    await tester.idle();
443

444 445
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
446 447 448 449 450 451 452 453 454 455 456 457

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

459 460
    expect(emittedImages[0].image.isCloneOf(frame1.image), true);
    expect(emittedImages[1].image.isCloneOf(frame2.image), true);
461
  });
462

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

469
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
470 471 472
      codec: codecCompleter.future,
      scale: 1.0,
    );
473

474
    void listener(ImageInfo image, bool synchronousCall) { }
475
    imageStream.addListener(ImageStreamListener(listener));
476
    final ImageStreamCompleterHandle handle = imageStream.keepAlive();
477

478 479
    codecCompleter.complete(mockCodec);
    await tester.idle();
480

481 482
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
483

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

492 493 494
    // Decoding of the 3rd frame should not start as there are no registered
    // listeners to the stream
    expect(mockCodec.numFramesAsked, 2);
495

496
    imageStream.addListener(ImageStreamListener(listener));
497 498
    await tester.idle(); // let nextFrameFuture complete
    expect(mockCodec.numFramesAsked, 3);
499 500

    handle.dispose();
501
  });
502

503
  testWidgets('multiple stream listeners', (WidgetTester tester) async {
504
    final MockCodec mockCodec = MockCodec();
505 506
    mockCodec.frameCount = 2;
    mockCodec.repetitionCount = -1;
507
    final Completer<Codec> codecCompleter = Completer<Codec>();
508

509
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
510 511 512
      codec: codecCompleter.future,
      scale: 1.0,
    );
513

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

525 526
    codecCompleter.complete(mockCodec);
    await tester.idle();
527

528 529
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
530 531 532 533

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

    expect(emittedImages1.single.image.isCloneOf(frame1.image), true);
    expect(emittedImages2.single.image.isCloneOf(frame1.image), true);
537 538 539 540

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

    await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
544 545 546 547
    expect(emittedImages1.single.image.isCloneOf(frame1.image), true);
    expect(emittedImages2[0].image.isCloneOf(frame1.image), true);
    expect(emittedImages2[1].image.isCloneOf(frame2.image), true);

548
  });
549

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

556
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
557 558 559
      codec: codecCompleter.future,
      scale: 1.0,
    );
560

561
    void listener(ImageInfo image, bool synchronousCall) { }
562
    imageStream.addListener(ImageStreamListener(listener));
563

564 565
    codecCompleter.complete(mockCodec);
    await tester.idle();
566

567 568
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
    final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
569

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

574 575 576
    mockCodec.completeNextFrame(frame2);
    await tester.idle(); // let nextFrameFuture complete
    await tester.pump();
577

578
    imageStream.removeListener(ImageStreamListener(listener));
579 580 581
    // The test framework will fail this if there are pending timers at this
    // point.
  });
582

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

589
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
590 591 592
      codec: codecCompleter.future,
      scale: 1.0,
    );
593

594
    void listener(ImageInfo image, bool synchronousCall) { }
595
    imageStream.addListener(ImageStreamListener(listener));
596

597 598
    codecCompleter.complete(mockCodec);
    await tester.idle();
599

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

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

619
  testWidgets('error handlers can intercept errors', (WidgetTester tester) async {
620
    final MockCodec mockCodec = MockCodec();
621
    mockCodec.frameCount = 1;
622
    final Completer<Codec> codecCompleter = Completer<Codec>();
623

624
    final ImageStreamCompleter streamUnderTest = MultiFrameImageStreamCompleter(
625 626 627
      codec: codecCompleter.future,
      scale: 1.0,
    );
628

629
    dynamic capturedException;
630
    void errorListener(dynamic exception, StackTrace? stackTrace) {
631
      capturedException = exception;
632
    }
633

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

    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');
  });
653 654 655 656 657 658 659 660 661 662 663 664

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

665
    void listener(ImageInfo image, bool synchronousCall) { }
666
    imageStream.addListener(ImageStreamListener(listener));
667 668 669 670 671

    codecCompleter.complete(mockCodec);

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

672
    imageStream.addListener(ImageStreamListener(listener));
673
    imageStream.removeListener(ImageStreamListener(listener));
674 675


676
    final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
677 678 679 680 681 682 683 684

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

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

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

717
  testWidgets('Keep alive handles do not drive frames or prevent last listener callbacks', (WidgetTester tester) async {
718
    final Image image10x10 = (await tester.runAsync(() => createTestImage(width: 10, height: 10)))!;
719 720 721 722 723 724 725 726 727 728 729
    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;
730
    void activeListener(ImageInfo image, bool synchronousCall) {
731
      onImageCount += 1;
732
    }
733 734 735 736 737 738 739 740
    bool lastListenerDropped = false;
    imageStream.addOnLastListenerRemovedCallback(() {
      lastListenerDropped = true;
    });

    expect(lastListenerDropped, false);
    final ImageStreamCompleterHandle handle = imageStream.keepAlive();
    expect(lastListenerDropped, false);
741
    SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Only passive listeners');
742 743 744 745 746 747 748 749 750

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

    expect(onImageCount, 0);

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

    imageStream.addListener(ImageStreamListener(activeListener));

    final FakeFrameInfo frame2 = FakeFrameInfo(Duration.zero, image10x10);
    mockCodec.completeNextFrame(frame2);
    await tester.idle();
760
    expect(SchedulerBinding.instance!.transientCallbackCount, 1);
761 762 763 764 765 766 767 768 769
    await tester.pump();

    expect(onImageCount, 1);

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

    mockCodec.completeNextFrame(frame1);
    await tester.idle();
770
    expect(SchedulerBinding.instance!.transientCallbackCount, 1);
771 772 773 774
    await tester.pump();

    expect(onImageCount, 1);

775
    SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Only passive listeners');
776 777 778

    mockCodec.completeNextFrame(frame2);
    await tester.idle();
779
    SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Only passive listeners');
780 781 782 783 784 785 786
    await tester.pump();

    expect(onImageCount, 1);

    handle.dispose();
  });

787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
  test('MultiFrameImageStreamCompleter - one frame image should only be decoded once', () async {
    final FakeCodec oneFrameCodec = await FakeCodec.fromData(Uint8List.fromList(kTransparentImage));
    final Completer<Codec> codecCompleter = Completer<Codec>();
    final Completer<void> decodeCompleter = Completer<void>();
    final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
      codec: codecCompleter.future,
      scale: 1.0,
    );
    final ImageStreamListener imageListener = ImageStreamListener((ImageInfo info, bool syncCall) {
      decodeCompleter.complete();
    });

    imageStream.keepAlive();  // do not dispose
    imageStream.addListener(imageListener);
    codecCompleter.complete(oneFrameCodec);
    await decodeCompleter.future;

    imageStream.removeListener(imageListener);
    expect(oneFrameCodec.numFramesAsked, 1);

    // Adding a new listener for decoded imageSteam, the one frame image should
    // not be decoded again.
    imageStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
    expect(oneFrameCodec.numFramesAsked, 1);
  });  // https://github.com/flutter/flutter/issues/82532

813 814 815 816 817 818 819 820 821 822 823 824 825 826
  // 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) { };
827
  //   imageStream.addListener(ImageLoadingListener(listener));
828 829 830 831 832 833
  //
  //   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));
834
  //   final FrameInfo frame3 = FakeFrameInfo(200, 100, Duration.zero);
835 836 837 838 839 840 841 842 843 844 845
  //
  //   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);
846
  //   imageStream.addListener(ImageLoadingListener(listener));
847 848 849 850 851 852
  //
  //   mockCodec.completeNextFrame(frame3);
  //   await tester.idle(); // let nextFrameFuture complete
  //
  //   await tester.pump();
  // });
853
}