flutter/packages/flutter/test/services/image_stream_test.dart
amirh 78e044f5ec
Cancel the animated image stream timer if all listeners were removed. (#13158)
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.
2017-11-22 15:27:26 -08:00

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