blob: 5f0c1e2febe30d5b5cc55d8052589f3850b20242 [file] [log] [blame]
// 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 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.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;
bool disposed = false;
Completer<FrameInfo> _nextFrameCompleter = Completer<FrameInfo>();
@override
Future<FrameInfo> getNextFrame() {
if (disposed) {
throw StateError('Codec is disposed');
}
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() {
if (disposed) {
throw StateError('Codec is already disposed');
}
disposed = true;
}
}
class FakeEventReportingImageStreamCompleter extends ImageStreamCompleter {
FakeEventReportingImageStreamCompleter({Stream<ImageChunkEvent>? chunkEvents}) {
chunkEvents?.listen(reportImageChunkEvent);
}
}
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 = 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 = Completer<Codec>();
final 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) {
addTearDown(image.dispose);
}
imageStream.addListener(ImageStreamListener(listener));
await tester.idle();
expect(mockCodec.numFramesAsked, 1);
expect(mockCodec.disposed, false);
});
testWidgets('Decoding starts when a codec is ready after a listener is added', (
WidgetTester tester,
) async {
final completer = Completer<Codec>();
final mockCodec = MockCodec();
mockCodec.frameCount = 1;
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: completer.future,
scale: 1.0,
);
void listener(ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
}
imageStream.addListener(ImageStreamListener(listener));
await tester.idle();
expect(mockCodec.numFramesAsked, 0);
completer.complete(mockCodec);
await tester.idle();
expect(mockCodec.numFramesAsked, 1);
expect(mockCodec.disposed, false);
});
testWidgets('Decoding does not crash when disposed', (WidgetTester tester) async {
final completer = Completer<Codec>();
final 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) {
addTearDown(image.dispose);
}
final 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);
expect(mockCodec.disposed, false);
imageStream.removeListener(streamListener);
expect(mockCodec.disposed, true);
await tester.idle();
});
testWidgets('Chunk events of base ImageStreamCompleter are delivered', (
WidgetTester tester,
) async {
final chunkEvents = <ImageChunkEvent>[];
final 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 chunkEvents = <ImageChunkEvent>[];
final 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 chunkEvents = <ImageChunkEvent>[];
final completer = Completer<Codec>();
final 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 chunkEvents = <ImageChunkEvent>[];
final completer = Completer<Codec>();
final 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 chunkEvents = <ImageChunkEvent>[];
final completer = Completer<Codec>();
final 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.frameCount = 1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
void listener(ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
}
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');
expect(mockCodec.disposed, false);
});
testWidgets('ImageStream emits frame (static image)', (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final emittedImages = <ImageInfo>[];
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
addTearDown(image.dispose);
});
imageStream.addListener(listener);
codecCompleter.complete(mockCodec);
await tester.idle();
final FrameInfo frame = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
mockCodec.completeNextFrame(frame);
expect(mockCodec.disposed, false);
await tester.idle();
expect(mockCodec.disposed, true);
expect(emittedImages.every((ImageInfo info) => info.image.isCloneOf(frame.image)), true);
imageStream.removeListener(listener);
imageCache.clear();
});
testWidgets('ImageStream emits frames (animated images)', (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final emittedImages = <ImageInfo>[];
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
addTearDown(image.dispose);
});
imageStream.addListener(listener);
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));
expect(mockCodec.disposed, false);
imageStream.removeListener(listener);
expect(mockCodec.disposed, true);
imageCache.clear();
});
testWidgets('animation wraps back', (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final emittedImages = <ImageInfo>[];
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
addTearDown(image.dispose);
});
imageStream.addListener(listener);
codecCompleter.complete(mockCodec);
await tester.idle();
final frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
final 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));
expect(mockCodec.disposed, false);
imageStream.removeListener(listener);
expect(mockCodec.disposed, true);
imageCache.clear();
});
testWidgets("animation doesn't repeat more than specified", (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = 0;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final emittedImages = <ImageInfo>[];
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
addTearDown(image.dispose);
});
imageStream.addListener(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
expect(mockCodec.disposed, false);
await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
expect(mockCodec.disposed, true);
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);
imageStream.removeListener(listener);
imageCache.clear();
});
testWidgets('frames are only decoded when there are listeners', (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
});
imageStream.addListener(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(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);
handle.dispose();
expect(mockCodec.disposed, false);
imageStream.removeListener(listener);
expect(mockCodec.disposed, true);
imageCache.clear();
});
testWidgets('multiple stream listeners', (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final emittedImages1 = <ImageInfo>[];
final listener1 = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages1.add(image);
addTearDown(image.dispose);
});
final emittedImages2 = <ImageInfo>[];
final listener2 = ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages2.add(image);
addTearDown(image.dispose);
});
imageStream.addListener(listener1);
imageStream.addListener(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(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);
expect(mockCodec.disposed, false);
imageStream.removeListener(listener2);
expect(mockCodec.disposed, true);
});
testWidgets('timer is canceled when listeners are removed', (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
void listener(ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
}
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();
expect(mockCodec.disposed, false);
imageStream.removeListener(ImageStreamListener(listener));
expect(mockCodec.disposed, true);
// 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.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
});
imageStream.addListener(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
expect(mockCodec.disposed, false);
imageStream.removeListener(listener);
expect(mockCodec.disposed, true);
});
testWidgets('error handlers can intercept errors', (WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 1;
final 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');
expect(mockCodec.disposed, false);
});
testWidgets(
'remove and add listener ',
experimentalLeakTesting: LeakTesting.settings
.withIgnoredAll(), // leaking by design because imageStream does not have a listener
(WidgetTester tester) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 3;
mockCodec.repetitionCount = 0;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
void listener(ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
}
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.
expect(mockCodec.disposed, false);
},
);
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 l1 = ImageStreamListener(onImage1, onChunk: onChunk1, onError: onError1);
final 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.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
var onImageCount = 0;
void activeListener(ImageInfo image, bool synchronousCall) {
onImageCount += 1;
addTearDown(image.dispose);
}
var 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 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 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);
expect(mockCodec.disposed, false);
handle.dispose();
expect(mockCodec.disposed, true);
});
test('MultiFrameImageStreamCompleter - one frame image should only be decoded once', () async {
final FakeCodec oneFrameCodec = await FakeCodec.fromData(Uint8List.fromList(kTransparentImage));
final codecCompleter = Completer<Codec>();
final decodeCompleter = Completer<void>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final 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 chunkStream = StreamController<ImageChunkEvent>();
final 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 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 imageStreamCompleter = OneFrameImageStreamCompleter(
SynchronousFuture<ImageInfo>(TestImageInfo(1, image: image)),
);
final 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 imageStreamCompleter = OneFrameImageStreamCompleter(
SynchronousFuture<ImageInfo>(TestImageInfo(1, image: image)),
);
final imageStream = ImageStream();
bool? synchronouslyCalled;
imageStream.addListener(
ImageStreamListener((ImageInfo image, bool synchronousCall) {
synchronouslyCalled = synchronousCall;
}),
);
imageStream.setCompleter(imageStreamCompleter);
expect(synchronouslyCalled, false);
});
test('ImageStreamCompleterHandle dispatches memory events', () async {
await expectLater(
await memoryEvents(() {
final streamController = StreamController<ImageChunkEvent>();
addTearDown(streamController.close);
final ImageStreamCompleterHandle imageStreamCompleterHandle =
FakeEventReportingImageStreamCompleter(
chunkEvents: streamController.stream,
).keepAlive();
imageStreamCompleterHandle.dispose();
}, ImageStreamCompleterHandle),
areCreateAndDispose,
);
});
testWidgets('ImageInfo dispatches memory events', (WidgetTester tester) async {
await expectLater(
await memoryEvents(() async {
final info = ImageInfo(image: image20x10);
info.dispose();
}, ImageInfo),
areCreateAndDispose,
);
});
testWidgets('MultiFrameImageStreamCompleter image callback can remove listener', (
WidgetTester tester,
) async {
final completer = Completer<Codec>();
final 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);
late ImageStreamListener streamListener;
void listener(ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
imageStream.removeListener(streamListener);
}
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);
await tester.idle();
expect(mockCodec.disposed, true);
});
testWidgets('ImageStream that has never had any listeners can be disposed', (
WidgetTester tester,
) async {
final mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = -1;
final codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
codecCompleter.complete(mockCodec);
await tester.idle();
expect(mockCodec.disposed, false);
imageStream.maybeDispose();
expect(mockCodec.disposed, true);
});
}