| // 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:ui'; |
| |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/scheduler.dart' show timeDilation; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| class FakeFrameInfo implements FrameInfo { |
| final Duration _duration; |
| final Image _image; |
| |
| FakeFrameInfo(int width, int height, this._duration) : |
| _image = new FakeImage(width, height); |
| |
| @override |
| Duration get duration => _duration; |
| |
| @override |
| Image get image => _image; |
| } |
| |
| class FakeImage implements Image { |
| final int _width; |
| final int _height; |
| |
| FakeImage(this._width, this._height); |
| |
| @override |
| int get width => _width; |
| |
| @override |
| int get height => _height; |
| |
| @override |
| void dispose() {} |
| } |
| |
| class MockCodec implements Codec { |
| |
| @override |
| int frameCount; |
| |
| @override |
| int repetitionCount; |
| |
| int numFramesAsked = 0; |
| |
| Completer<FrameInfo> _nextFrameCompleter = new Completer<FrameInfo>(); |
| |
| @override |
| Future<FrameInfo> getNextFrame() { |
| numFramesAsked += 1; |
| return _nextFrameCompleter.future; |
| } |
| |
| void completeNextFrame(FrameInfo frameInfo) { |
| _nextFrameCompleter.complete(frameInfo); |
| _nextFrameCompleter = new 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 = new Completer<Codec>(); |
| new MultiFrameImageStreamCompleter( |
| codec: completer.future, |
| scale: 1.0, |
| ); |
| completer.completeError('failure message'); |
| await tester.idle(); |
| expect(tester.takeException(), 'failure message'); |
| }); |
| |
| testWidgets('First frame decoding starts when codec is ready', (WidgetTester tester) async { |
| final Completer<Codec> completer = new Completer<Codec>(); |
| final MockCodec mockCodec = new MockCodec(); |
| mockCodec.frameCount = 1; |
| new MultiFrameImageStreamCompleter( |
| codec: completer.future, |
| scale: 1.0, |
| ); |
| |
| completer.complete(mockCodec); |
| await tester.idle(); |
| expect(mockCodec.numFramesAsked, 1); |
| }); |
| |
| testWidgets('getNextFrame future fails', (WidgetTester tester) async { |
| final MockCodec mockCodec = new MockCodec(); |
| mockCodec.frameCount = 1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| 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 = new MockCodec(); |
| mockCodec.frameCount = 1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| }); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); |
| mockCodec.completeNextFrame(frame); |
| await tester.idle(); |
| |
| expect(emittedImages, equals(<ImageInfo>[new ImageInfo(image: frame.image)])); |
| }); |
| |
| testWidgets('ImageStream emits frames (animated images)', (WidgetTester tester) async { |
| final MockCodec mockCodec = new MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = -1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| }); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = new 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>[new ImageInfo(image: frame1.image)])); |
| |
| final FrameInfo frame2 = new 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>[ |
| new ImageInfo(image: frame1.image), |
| new 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 = new MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = -1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| }); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); |
| final FrameInfo frame2 = new 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>[ |
| new ImageInfo(image: frame1.image), |
| new ImageInfo(image: frame2.image), |
| new 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 = new MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = 0; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final List<ImageInfo> emittedImages = <ImageInfo>[]; |
| imageStream.addListener((ImageInfo image, bool synchronousCall) { |
| emittedImages.add(image); |
| }); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); |
| final FrameInfo frame2 = new 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>[ |
| new ImageInfo(image: frame1.image), |
| new ImageInfo(image: frame2.image), |
| ])); |
| }); |
| |
| testWidgets('frames are only decoded when there are active listeners', (WidgetTester tester) async { |
| final MockCodec mockCodec = new MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = -1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final ImageListener listener = (ImageInfo image, bool synchronousCall) {}; |
| imageStream.addListener(listener); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); |
| final FrameInfo frame2 = new 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(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(listener); |
| await tester.idle(); // let nextFrameFuture complete |
| expect(mockCodec.numFramesAsked, 3); |
| }); |
| |
| testWidgets('multiple stream listeners', (WidgetTester tester) async { |
| final MockCodec mockCodec = new MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = -1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new 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(listener1); |
| imageStream.addListener(listener2); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); |
| final FrameInfo frame2 = new 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>[new ImageInfo(image: frame1.image)])); |
| expect(emittedImages2, equals(<ImageInfo>[new 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(listener1); |
| |
| await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame. |
| expect(emittedImages1, equals(<ImageInfo>[new ImageInfo(image: frame1.image)])); |
| expect(emittedImages2, equals(<ImageInfo>[ |
| new ImageInfo(image: frame1.image), |
| new ImageInfo(image: frame2.image), |
| ])); |
| }); |
| |
| testWidgets('timer is canceled when listeners are removed', (WidgetTester tester) async { |
| final MockCodec mockCodec = new MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = -1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final ImageListener listener = (ImageInfo image, bool synchronousCall) {}; |
| imageStream.addListener(listener); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); |
| final FrameInfo frame2 = new 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(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 = new MockCodec(); |
| mockCodec.frameCount = 2; |
| mockCodec.repetitionCount = -1; |
| final Completer<Codec> codecCompleter = new Completer<Codec>(); |
| |
| final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter( |
| codec: codecCompleter.future, |
| scale: 1.0, |
| ); |
| |
| final ImageListener listener = (ImageInfo image, bool synchronousCall) {}; |
| imageStream.addListener(listener); |
| |
| codecCompleter.complete(mockCodec); |
| await tester.idle(); |
| |
| final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); |
| final FrameInfo frame2 = new 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); |
| |
| }); |
| } |