| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:ui'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/scheduler.dart' show SchedulerBinding, timeDilation; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../image_data.dart'; |
| import 'fake_codec.dart'; |
| import 'mocks_for_image_cache.dart'; |
| |
| class FakeFrameInfo implements FrameInfo { |
| const FakeFrameInfo(this._duration, this._image); |
| |
| final Duration _duration; |
| final Image _image; |
| |
| @override |
| Duration get duration => _duration; |
| |
| @override |
| Image get image => _image; |
| |
| int get imageHandleCount => image.debugGetOpenHandleStackTraces()!.length; |
| |
| FakeFrameInfo clone() { |
| return FakeFrameInfo( |
| _duration, |
| _image.clone(), |
| ); |
| } |
| } |
| |
| class MockCodec implements Codec { |
| @override |
| late int frameCount; |
| |
| @override |
| late int repetitionCount; |
| |
| int numFramesAsked = 0; |
| |
| Completer<FrameInfo> _nextFrameCompleter = Completer<FrameInfo>(); |
| |
| @override |
| Future<FrameInfo> getNextFrame() { |
| numFramesAsked += 1; |
| return _nextFrameCompleter.future; |
| } |
| |
| void completeNextFrame(FrameInfo frameInfo) { |
| _nextFrameCompleter.complete(frameInfo); |
| _nextFrameCompleter = Completer<FrameInfo>(); |
| } |
| |
| void failNextFrame(String err) { |
| _nextFrameCompleter.completeError(err); |
| } |
| |
| @override |
| void dispose() { } |
| |
| } |
| |
| class FakeEventReportingImageStreamCompleter extends ImageStreamCompleter { |
| FakeEventReportingImageStreamCompleter({Stream<ImageChunkEvent>? chunkEvents}) { |
| if (chunkEvents != null) { |
| chunkEvents.listen((ImageChunkEvent event) { |
| reportImageChunkEvent(event); |
| }, |
| ); |
| } |
| } |
| } |
| |
| void main() { |
| late Image image20x10; |
| late Image image200x100; |
| setUp(() async { |
| image20x10 = await createTestImage(width: 20, height: 10); |
| image200x100 = await createTestImage(width: 200, height: 100); |
| }); |
| |
| testWidgets('Codec future fails', (WidgetTester tester) async { |
| final Completer<Codec> completer = Completer<Codec>(); |
| MultiFrameImageStreamCompleter( |
| codec: completer.future, |
| scale: 1.0, |
| ); |
| completer.completeError('failure message'); |
| await tester.idle(); |
| expect(tester.takeException(), 'failure message'); |
| }); |
| |
| testWidgets('Decoding starts when a listener is added after codec is ready', (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); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| imageStream.addListener(ImageStreamListener(listener)); |
| 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, |
| ); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| imageStream.addListener(ImageStreamListener(listener)); |
| await tester.idle(); |
| expect(mockCodec.numFramesAsked, 0); |
| |
| completer.complete(mockCodec); |
| await tester.idle(); |
| expect(mockCodec.numFramesAsked, 1); |
| }); |
| |
| 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); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| 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(); |
| }); |
| |
| 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 { |
| 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); |
| }); |
| |
| testWidgets('Chunk events of MultiFrameImageStreamCompleter are not buffered before listener registration', (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, |
| ); |
| |
| 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); |
| }); |
| |
| testWidgets('getNextFrame future fails', (WidgetTester tester) async { |
| final MockCodec mockCodec = MockCodec(); |
| mockCodec.frameCount = 1; |
| final Completer<Codec> codecCompleter = Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| imageStream.addListener(ImageStreamListener(listener)); |
| 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'); |
| }); |
| |
| testWidgets('ImageStream emits frame (static image)', (WidgetTester tester) async { |
| final MockCodec mockCodec = MockCodec(); |
| mockCodec.frameCount = 1; |
| final Completer<Codec> codecCompleter = Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| })); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| mockCodec.completeNextFrame(frame); |
| await tester.idle(); |
| |
| expect(emittedImages.every((ImageInfo info) => info.image.isCloneOf(frame.image)), true); |
| }); |
| |
| testWidgets('ImageStream emits frames (animated images)', (WidgetTester tester) async { |
| 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, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| })); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| 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(); |
| expect(emittedImages.single.image.isCloneOf(frame1.image), true); |
| |
| final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100); |
| 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[0].image.isCloneOf(frame1.image), true); |
| expect(emittedImages[1].image.isCloneOf(frame2.image), true); |
| |
| // 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)); |
| }); |
| |
| testWidgets('animation wraps back', (WidgetTester tester) async { |
| 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, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| })); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FakeFrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| final FakeFrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100); |
| |
| mockCodec.completeNextFrame(frame1.clone()); |
| await tester.idle(); // let nextFrameFuture complete |
| await tester.pump(); // first animation frame shows on first app frame. |
| mockCodec.completeNextFrame(frame2.clone()); |
| await tester.idle(); // let nextFrameFuture complete |
| await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame. |
| mockCodec.completeNextFrame(frame1.clone()); |
| await tester.idle(); // let nextFrameFuture complete |
| await tester.pump(const Duration(milliseconds: 400)); // emit 3rd frame |
| |
| expect(emittedImages[0].image.isCloneOf(frame1.image), true); |
| expect(emittedImages[1].image.isCloneOf(frame2.image), true); |
| expect(emittedImages[2].image.isCloneOf(frame1.image), true); |
| |
| // 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)); |
| }); |
| |
| testWidgets("animation doesn't repeat more than specified", (WidgetTester tester) async { |
| final MockCodec mockCodec = MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = 0; |
| final Completer<Codec> codecCompleter = Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| })); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100); |
| |
| 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)); |
| |
| expect(emittedImages[0].image.isCloneOf(frame1.image), true); |
| expect(emittedImages[1].image.isCloneOf(frame2.image), true); |
| }); |
| |
| testWidgets('frames are only decoded when there are listeners', (WidgetTester tester) async { |
| 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, |
| ); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| imageStream.addListener(ImageStreamListener(listener)); |
| final ImageStreamCompleterHandle handle = imageStream.keepAlive(); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100); |
| |
| mockCodec.completeNextFrame(frame1); |
| await tester.idle(); // let nextFrameFuture complete |
| await tester.pump(); // first animation frame shows on first app frame. |
| mockCodec.completeNextFrame(frame2); |
| imageStream.removeListener(ImageStreamListener(listener)); |
| await tester.idle(); // let nextFrameFuture complete |
| await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame. |
| |
| // Decoding of the 3rd frame should not start as there are no registered |
| // listeners to the stream |
| expect(mockCodec.numFramesAsked, 2); |
| |
| imageStream.addListener(ImageStreamListener(listener)); |
| await tester.idle(); // let nextFrameFuture complete |
| expect(mockCodec.numFramesAsked, 3); |
| |
| handle.dispose(); |
| }); |
| |
| testWidgets('multiple stream listeners', (WidgetTester tester) async { |
| 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, |
| ); |
| |
| final List<ImageInfo> emittedImages1 = <ImageInfo>[]; |
| void listener1(ImageInfo image, bool synchronousCall) { |
| emittedImages1.add(image); |
| } |
| final List<ImageInfo> emittedImages2 = <ImageInfo>[]; |
| void listener2(ImageInfo image, bool synchronousCall) { |
| emittedImages2.add(image); |
| } |
| imageStream.addListener(ImageStreamListener(listener1)); |
| imageStream.addListener(ImageStreamListener(listener2)); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100); |
| |
| mockCodec.completeNextFrame(frame1); |
| await tester.idle(); // let nextFrameFuture complete |
| await tester.pump(); // first animation frame shows on first app frame. |
| |
| expect(emittedImages1.single.image.isCloneOf(frame1.image), true); |
| expect(emittedImages2.single.image.isCloneOf(frame1.image), true); |
| |
| mockCodec.completeNextFrame(frame2); |
| await tester.idle(); // let nextFrameFuture complete |
| await tester.pump(); // next app frame will schedule a timer. |
| imageStream.removeListener(ImageStreamListener(listener1)); |
| |
| await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame. |
| expect(emittedImages1.single.image.isCloneOf(frame1.image), true); |
| expect(emittedImages2[0].image.isCloneOf(frame1.image), true); |
| expect(emittedImages2[1].image.isCloneOf(frame2.image), true); |
| |
| }); |
| |
| testWidgets('timer is canceled when listeners are removed', (WidgetTester tester) async { |
| 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, |
| ); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| imageStream.addListener(ImageStreamListener(listener)); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100); |
| |
| 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(); |
| |
| imageStream.removeListener(ImageStreamListener(listener)); |
| // The test framework will fail this if there are pending timers at this |
| // point. |
| }); |
| |
| testWidgets('timeDilation affects animation frame timers', (WidgetTester tester) async { |
| 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, |
| ); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| imageStream.addListener(ImageStreamListener(listener)); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100); |
| |
| 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); |
| timeDilation = 1.0; // restore time dilation, or it will affect other tests |
| }); |
| |
| testWidgets('error handlers can intercept errors', (WidgetTester tester) async { |
| final MockCodec mockCodec = MockCodec(); |
| mockCodec.frameCount = 1; |
| final Completer<Codec> codecCompleter = Completer<Codec>(); |
| |
| final ImageStreamCompleter streamUnderTest = MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| dynamic capturedException; |
| void errorListener(dynamic exception, StackTrace? stackTrace) { |
| capturedException = exception; |
| } |
| |
| streamUnderTest.addListener(ImageStreamListener( |
| (ImageInfo image, bool synchronousCall) { }, |
| onError: errorListener, |
| )); |
| |
| 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'); |
| }); |
| |
| 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, |
| ); |
| |
| void listener(ImageInfo image, bool synchronousCall) { } |
| imageStream.addListener(ImageStreamListener(listener)); |
| |
| codecCompleter.complete(mockCodec); |
| |
| await tester.idle(); // let nextFrameFuture complete |
| |
| imageStream.addListener(ImageStreamListener(listener)); |
| imageStream.removeListener(ImageStreamListener(listener)); |
| |
| |
| final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); |
| |
| 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. |
| }); |
| |
| testWidgets('ImageStreamListener hashCode and equals', (WidgetTester tester) async { |
| 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); |
| }); |
| |
| testWidgets('Keep alive handles do not drive frames or prevent last listener callbacks', (WidgetTester tester) async { |
| final Image image10x10 = (await tester.runAsync(() => createTestImage(width: 10, height: 10)))!; |
| 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; |
| void activeListener(ImageInfo image, bool synchronousCall) { |
| onImageCount += 1; |
| } |
| bool lastListenerDropped = false; |
| imageStream.addOnLastListenerRemovedCallback(() { |
| lastListenerDropped = true; |
| }); |
| |
| expect(lastListenerDropped, false); |
| final ImageStreamCompleterHandle handle = imageStream.keepAlive(); |
| expect(lastListenerDropped, false); |
| SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners'); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| expect(onImageCount, 0); |
| |
| final FakeFrameInfo frame1 = FakeFrameInfo(Duration.zero, image20x10); |
| mockCodec.completeNextFrame(frame1); |
| await tester.idle(); |
| SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners'); |
| await tester.pump(); |
| expect(onImageCount, 0); |
| |
| imageStream.addListener(ImageStreamListener(activeListener)); |
| |
| final FakeFrameInfo frame2 = FakeFrameInfo(Duration.zero, image10x10); |
| mockCodec.completeNextFrame(frame2); |
| await tester.idle(); |
| expect(SchedulerBinding.instance.transientCallbackCount, 1); |
| await tester.pump(); |
| |
| expect(onImageCount, 1); |
| |
| imageStream.removeListener(ImageStreamListener(activeListener)); |
| expect(lastListenerDropped, true); |
| |
| mockCodec.completeNextFrame(frame1); |
| await tester.idle(); |
| expect(SchedulerBinding.instance.transientCallbackCount, 1); |
| await tester.pump(); |
| |
| expect(onImageCount, 1); |
| |
| SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners'); |
| |
| mockCodec.completeNextFrame(frame2); |
| await tester.idle(); |
| SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners'); |
| await tester.pump(); |
| |
| expect(onImageCount, 1); |
| |
| handle.dispose(); |
| }); |
| |
| 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 |
| |
| test('Multi-frame complete unsubscribes to chunk events when disposed', () async { |
| final FakeCodec codec = await FakeCodec.fromData(Uint8List.fromList(kTransparentImage)); |
| final StreamController<ImageChunkEvent> chunkStream = StreamController<ImageChunkEvent>(); |
| |
| final MultiFrameImageStreamCompleter completer = MultiFrameImageStreamCompleter( |
| codec: Future<Codec>.value(codec), |
| scale: 1.0, |
| chunkEvents: chunkStream.stream, |
| ); |
| |
| expect(chunkStream.hasListener, true); |
| |
| chunkStream.add(const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3)); |
| |
| final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {}); |
| // Cause the completer to dispose. |
| completer.addListener(listener); |
| completer.removeListener(listener); |
| |
| expect(chunkStream.hasListener, false); |
| |
| // The above expectation should cover this, but the point of this test is to |
| // make sure the completer does not assert that it's disposed and still |
| // receiving chunk events. Streams from the network can keep sending data |
| // even after evicting an image from the cache, for example. |
| chunkStream.add(const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3)); |
| }); |
| |
| test('ImageStream, setCompleter before addListener - synchronousCall should be true', () async { |
| final Image image = await createTestImage(width: 100, height: 100); |
| final OneFrameImageStreamCompleter imageStreamCompleter = |
| OneFrameImageStreamCompleter(SynchronousFuture<ImageInfo>(TestImageInfo(1, image: image))); |
| |
| final ImageStream imageStream = ImageStream(); |
| imageStream.setCompleter(imageStreamCompleter); |
| |
| bool? synchronouslyCalled; |
| imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) { |
| synchronouslyCalled = synchronousCall; |
| })); |
| |
| expect(synchronouslyCalled, true); |
| }); |
| |
| test('ImageStream, setCompleter after addListener - synchronousCall should be false', () async { |
| final Image image = await createTestImage(width: 100, height: 100); |
| final OneFrameImageStreamCompleter imageStreamCompleter = |
| OneFrameImageStreamCompleter(SynchronousFuture<ImageInfo>(TestImageInfo(1, image: image))); |
| |
| final ImageStream imageStream = ImageStream(); |
| |
| bool? synchronouslyCalled; |
| imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) { |
| synchronouslyCalled = synchronousCall; |
| })); |
| |
| imageStream.setCompleter(imageStreamCompleter); |
| |
| expect(synchronouslyCalled, false); |
| }); |
| } |