
This is a bug in my previous CL: instead of cancelling the timer if there are no more listeners, I canceled it if there were listeners (I can claim I just missed a not :) ). Not cancelling the timer when removing the last listener was not that bad, as the timer callback is guarded by a check to see if there are listeners. So the animation will not continue. But in the case there were multiple listeners on the same stream, and one of them is removed, this bug will stop the animation for all other listeners. I added a test case for this scenario.
432 lines
15 KiB
Dart
432 lines
15 KiB
Dart
// 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/scheduler.dart' show timeDilation;
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
class FakeFrameInfo extends 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 extends 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);
|
|
|
|
});
|
|
}
|