// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/painting.dart'; import 'package:flutter/scheduler.dart' show timeDilation; import 'package:flutter_test/flutter_test.dart'; import 'package:meta/meta.dart'; class FakeFrameInfo implements FrameInfo { FakeFrameInfo(int width, int height, this._duration) : _image = FakeImage(width, height); final Duration _duration; final Image _image; @override Duration get duration => _duration; @override Image get image => _image; } class FakeImage implements Image { FakeImage(this._width, this._height); final int _width; final int _height; @override int get width => _width; @override int get height => _height; @override void dispose() { } @override Future<ByteData> toByteData({ ImageByteFormat format = ImageByteFormat.rawRgba }) async { throw UnsupportedError('Cannot encode test image'); } } class MockCodec implements Codec { @override int frameCount; @override 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() { } } void main() { 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); final ImageListener 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, ); final ImageListener 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('Chunk events 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 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, ); final ImageListener 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(20, 10, const Duration(milliseconds: 200)); mockCodec.completeNextFrame(frame); await tester.idle(); expect(emittedImages, equals(<ImageInfo>[ImageInfo(image: frame.image)])); }); 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(20, 10, const Duration(milliseconds: 200)); 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, equals(<ImageInfo>[ImageInfo(image: frame1.image)])); final FrameInfo frame2 = FakeFrameInfo(200, 100, const Duration(milliseconds: 400)); mockCodec.completeNextFrame(frame2); await tester.pump(const Duration(milliseconds: 100)); // The duration for the current frame was 200ms, so we don't emit the next // frame yet even though it is ready. expect(emittedImages.length, 1); await tester.pump(const Duration(milliseconds: 100)); expect(emittedImages, equals(<ImageInfo>[ ImageInfo(image: frame1.image), ImageInfo(image: frame2.image), ])); // 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 FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); final FrameInfo frame2 = FakeFrameInfo(200, 100, const Duration(milliseconds: 400)); mockCodec.completeNextFrame(frame1); await tester.idle(); // let nextFrameFuture complete await tester.pump(); // first animation frame shows on first app frame. mockCodec.completeNextFrame(frame2); await tester.idle(); // let nextFrameFuture complete await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame. mockCodec.completeNextFrame(frame1); await tester.idle(); // let nextFrameFuture complete await tester.pump(const Duration(milliseconds: 400)); // emit 3rd frame expect(emittedImages, equals(<ImageInfo>[ ImageInfo(image: frame1.image), ImageInfo(image: frame2.image), ImageInfo(image: frame1.image), ])); // 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 doesnt 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(20, 10, const Duration(milliseconds: 200)); final FrameInfo frame2 = FakeFrameInfo(200, 100, const Duration(milliseconds: 400)); 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, equals(<ImageInfo>[ ImageInfo(image: frame1.image), ImageInfo(image: frame2.image), ])); }); testWidgets('frames are only decoded when there are active 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 ImageListener listener = (ImageInfo image, bool synchronousCall) { }; imageStream.addListener(ImageStreamListener(listener)); 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)); 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); }); 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>[]; final ImageListener listener1 = (ImageInfo image, bool synchronousCall) { emittedImages1.add(image); }; final List<ImageInfo> emittedImages2 = <ImageInfo>[]; final ImageListener listener2 = (ImageInfo image, bool synchronousCall) { emittedImages2.add(image); }; imageStream.addListener(ImageStreamListener(listener1)); imageStream.addListener(ImageStreamListener(listener2)); 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)); mockCodec.completeNextFrame(frame1); await tester.idle(); // let nextFrameFuture complete await tester.pump(); // first animation frame shows on first app frame. expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)])); expect(emittedImages2, equals(<ImageInfo>[ImageInfo(image: frame1.image)])); 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, equals(<ImageInfo>[ImageInfo(image: frame1.image)])); expect(emittedImages2, equals(<ImageInfo>[ ImageInfo(image: frame1.image), ImageInfo(image: frame2.image), ])); }); 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, ); final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; imageStream.addListener(ImageStreamListener(listener)); 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)); 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, ); final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; imageStream.addListener(ImageStreamListener(listener)); 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)); 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); }); 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; final ImageErrorListener 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, ); final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; imageStream.addListener(ImageStreamListener(listener)); codecCompleter.complete(mockCodec); await tester.idle(); // let nextFrameFuture complete imageStream.removeListener(ImageStreamListener(listener)); imageStream.addListener(ImageStreamListener(listener)); final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); 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); }); // 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) { }; // imageStream.addListener(ImageLoadingListener(listener)); // // codecCompleter.complete(mockCodec); // await tester.idle(); // // final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); // final FrameInfo frame2 = FakeFrameInfo(200, 100, const Duration(milliseconds: 400)); // final FrameInfo frame3 = FakeFrameInfo(200, 100, const Duration(milliseconds: 0)); // // mockCodec.completeNextFrame(frame1); // await tester.idle(); // let nextFrameFuture complete // await tester.pump(); // first animation frame shows on first app frame. // // mockCodec.completeNextFrame(frame2); // await tester.pump(const Duration(milliseconds: 100)); // emit 2nd frame. // // tester.flushTimers(); // // imageStream.removeListener(listener); // imageStream.addListener(ImageLoadingListener(listener)); // // mockCodec.completeNextFrame(frame3); // await tester.idle(); // let nextFrameFuture complete // // await tester.pump(); // }); }