874 lines
33 KiB
Dart
874 lines
33 KiB
Dart
// 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;
|
|
|
|
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);
|
|
});
|
|
}
|