twonly-app-dependencies/lottie/test/lottie_test.dart
2025-12-07 16:10:41 +01:00

558 lines
15 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/lottie.dart';
void main() {
tearDown(() {
Lottie.cache.clear();
});
testWidgets('Should settle if no animation', (tester) async {
var data = File('example/assets/HamburgerArrow.json').readAsBytesSync();
var composition = await LottieComposition.fromBytes(data);
await tester.pumpWidget(Lottie(composition: composition, animate: false));
await tester.pumpAndSettle();
});
testWidgets('onLoaded called with the correct composition', (tester) async {
late LottieComposition composition;
var file = SynchronousFile(File('example/assets/HamburgerArrow.json'));
await tester.pumpWidget(
LottieBuilder.file(
file,
onLoaded: (c) {
composition = c;
},
),
);
await tester.pump();
expect(composition.endFrame, 179.99);
});
testWidgets('onLoaded called when remplacing the widget animation', (
tester,
) async {
var hamburgerData = Future.value(
bytesForFile('example/assets/HamburgerArrow.json'),
);
var androidData = Future.value(
bytesForFile('example/assets/AndroidWave.json'),
);
var mockAsset = FakeAssetBundle({
'hamburger.json': hamburgerData,
'android.json': androidData,
});
var animation = AnimationController(vsync: tester);
LottieComposition? composition;
await tester.pumpWidget(
Lottie.asset(
'hamburger.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
},
),
);
await tester.pump();
expect(tester.takeException(), isNull);
//expect(find.byType(Lottie), findsOneWidget);
expect(composition, isNotNull);
expect(composition!.duration, const Duration(seconds: 6));
composition = null;
await tester.pumpWidget(
Lottie.asset(
'android.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
},
),
);
await tester.pump();
expect(composition, isNotNull);
expect(composition!.duration, const Duration(seconds: 2, milliseconds: 50));
});
testWidgets('onLoaded data race 1', (tester) async {
var hamburgerCompleter = Completer<ByteData>();
var androidCompleter = Completer<ByteData>();
var hamburgerData = bytesForFile('example/assets/HamburgerArrow.json');
var androidData = bytesForFile('example/assets/AndroidWave.json');
var mockAsset = FakeAssetBundle({
'hamburger.json': hamburgerCompleter.future,
'android.json': androidCompleter.future,
});
var animation = AnimationController(vsync: tester);
var onLoadedCount = 0;
LottieComposition? composition;
await tester.pumpWidget(
Lottie.asset(
'hamburger.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(
find.byWidgetPredicate((w) => w is RawLottie && w.composition == null),
findsOneWidget,
);
expect(composition, isNull);
expect(onLoadedCount, 0);
await tester.pumpWidget(
Lottie.asset(
'android.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(composition, isNull);
expect(onLoadedCount, 0);
hamburgerCompleter.complete(hamburgerData);
await tester.pump();
expect(composition, isNull);
expect(onLoadedCount, 0);
androidCompleter.complete(androidData);
await tester.pump();
expect(composition!.duration, const Duration(seconds: 2, milliseconds: 50));
expect(onLoadedCount, 1);
});
testWidgets('onLoaded data race 2', (tester) async {
var hamburgerCompleter = Completer<ByteData>();
var androidCompleter = Completer<ByteData>();
var hamburgerData = bytesForFile('example/assets/HamburgerArrow.json');
var androidData = bytesForFile('example/assets/AndroidWave.json');
var mockAsset = FakeAssetBundle({
'hamburger.json': hamburgerCompleter.future,
'android.json': androidCompleter.future,
});
var animation = AnimationController(vsync: tester);
var onLoadedCount = 0;
LottieComposition? composition;
await tester.pumpWidget(
Lottie.asset(
'hamburger.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(
find.byWidgetPredicate((w) => w is RawLottie && w.composition == null),
findsOneWidget,
);
expect(composition, isNull);
expect(onLoadedCount, 0);
await tester.pumpWidget(
Lottie.asset(
'android.json',
controller: animation,
bundle: mockAsset,
onLoaded: (c) {
composition = c;
++onLoadedCount;
},
),
);
await tester.pump();
expect(composition, isNull);
expect(onLoadedCount, 0);
androidCompleter.complete(androidData);
await tester.pump();
expect(composition!.duration, const Duration(seconds: 2, milliseconds: 50));
expect(onLoadedCount, 1);
hamburgerCompleter.complete(hamburgerData);
await tester.pump();
expect(composition!.duration, const Duration(seconds: 2, milliseconds: 50));
expect(onLoadedCount, 1);
});
testWidgets('Should auto animate', (tester) async {
var composition = await LottieComposition.fromBytes(
File('example/assets/HamburgerArrow.json').readAsBytesSync(),
);
await tester.pumpWidget(Lottie(composition: composition));
await tester.pump();
var lottie = tester.firstWidget<AnimatedBuilder>(
find.byType(AnimatedBuilder),
);
expect(lottie.listenable, isNotNull);
expect(
(lottie.listenable as AnimationController).duration,
const Duration(seconds: 6),
);
expect((lottie.listenable as AnimationController).isAnimating, true);
await tester.pumpWidget(Lottie(composition: composition, animate: false));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect(
(lottie.listenable as AnimationController).duration,
const Duration(seconds: 6),
);
expect((lottie.listenable as AnimationController).isAnimating, false);
await tester.pumpWidget(Lottie(composition: composition));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect(
(lottie.listenable as AnimationController).duration,
const Duration(seconds: 6),
);
var animationController = AnimationController(
vsync: tester,
duration: const Duration(seconds: 2),
);
await tester.pumpWidget(
Lottie(composition: composition, controller: animationController.view),
);
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect(
(lottie.listenable as AnimationController).duration,
const Duration(seconds: 2),
);
await tester.pumpWidget(
Lottie(
composition: composition,
controller: animationController.view,
animate: false,
),
);
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect(
(lottie.listenable as AnimationController).duration,
const Duration(seconds: 2),
);
await tester.pumpWidget(Lottie(composition: composition, animate: false));
lottie = tester.firstWidget<AnimatedBuilder>(find.byType(AnimatedBuilder));
expect(lottie.listenable, isNotNull);
expect(
(lottie.listenable as AnimationController).duration,
const Duration(seconds: 6),
);
expect((lottie.listenable as AnimationController).isAnimating, false);
});
testWidgets('errorBuilder called when error', (tester) async {
var hamburgerData = Future.value(
bytesForFile('example/assets/HamburgerArrow.json'),
);
var mockAsset = FakeAssetBundle({'hamburger.json': hamburgerData});
var errorKey = UniqueKey();
var loadedCall = 0;
await tester.pumpWidget(
LottieBuilder.asset(
'error.json',
bundle: mockAsset,
errorBuilder: (c, e, stackTrace) => Container(key: errorKey),
onLoaded: (c) {
++loadedCall;
},
),
);
await tester.pump();
expect(find.byKey(errorKey), findsOneWidget);
expect(loadedCall, 0);
await tester.pumpWidget(
LottieBuilder.asset(
'hamburger.json',
bundle: mockAsset,
errorBuilder: (c, e, stackTrace) => Container(key: errorKey),
onLoaded: (c) {
++loadedCall;
},
),
);
await tester.pump();
expect(find.byType(Lottie), findsOneWidget);
expect(find.byKey(errorKey), findsNothing);
expect(loadedCall, 1);
});
testWidgets('Cache should be synchronous', (tester) async {
var hamburgerData = Future.value(
bytesForFile('example/assets/HamburgerArrow.json'),
);
var mockAsset = FakeAssetBundle({'hamburger.json': hamburgerData});
var loadedCall = 0;
var lottieWidget = LottieBuilder.asset(
'hamburger.json',
bundle: mockAsset,
onLoaded: (c) {
++loadedCall;
},
);
await tester.pumpWidget(lottieWidget);
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNull);
await tester.pump();
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNotNull);
await tester.pumpWidget(Column(children: [lottieWidget, lottieWidget]));
expect(
tester.widget<Lottie>(find.byType(Lottie).at(0)).composition,
isNotNull,
);
expect(
tester.widget<Lottie>(find.byType(Lottie).at(1)).composition,
isNotNull,
);
expect(loadedCall, 3);
});
testWidgets('Cache can be cleared', (tester) async {
var hamburgerData = Future.value(
bytesForFile('example/assets/HamburgerArrow.json'),
);
var mockAsset = FakeAssetBundle({'hamburger.json': hamburgerData});
var loadedCall = 0;
var lottieWidget = LottieBuilder.asset(
'hamburger.json',
bundle: mockAsset,
onLoaded: (c) {
++loadedCall;
},
);
await tester.pumpWidget(lottieWidget);
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNull);
await tester.pump();
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNotNull);
Lottie.cache.clear();
await tester.pumpWidget(Center(child: lottieWidget));
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNull);
await tester.pump();
expect(tester.widget<Lottie>(find.byType(Lottie)).composition, isNotNull);
expect(loadedCall, 2);
});
testWidgets('onLoaded is ', (tester) async {
var hamburgerData = Future.value(
bytesForFile('example/assets/HamburgerArrow.json'),
);
var mockAsset = FakeAssetBundle({'hamburger.json': hamburgerData});
var provider = AssetLottie('hamburger.json', bundle: mockAsset);
await tester.pumpWidget(
KeyedSubtree(
key: UniqueKey(),
child: _LottieWithSetStateInOnLoaded(provider),
),
);
var state1 = tester.state<__LottieWithSetStateInOnLoadedState>(
find.byType(_LottieWithSetStateInOnLoaded),
);
expect(state1.loadedCount, 1);
await tester.pump();
expect(state1.loadedCount, 1);
await tester.pumpWidget(
KeyedSubtree(
key: UniqueKey(),
child: _LottieWithSetStateInOnLoaded(provider),
),
);
var state2 = tester.state<__LottieWithSetStateInOnLoadedState>(
find.byType(_LottieWithSetStateInOnLoaded),
);
expect(state2.loadedCount, 1);
await tester.pump();
expect(state2.loadedCount, 1);
expect(state1, isNot(state2));
});
testWidgets(
'if composition is static should create Lottie with [animate] false by default',
(tester) async {
await tester.pumpWidget(
LottieBuilder.memory(
File('test/data/static_lottie.json').readAsBytesSync(),
),
);
expect(tester.hasRunningAnimations, false);
},
);
testWidgets(
'if composition is static and [animate] is true, should have animations',
(tester) async {
await tester.pumpWidget(
LottieBuilder.memory(
File('test/data/static_lottie.json').readAsBytesSync(),
animate: true,
),
);
expect(tester.hasRunningAnimations, true);
},
);
testWidgets('AssetLottie uses DefaultAssetBundle', (tester) async {
var hamburgerData = Future.value(
bytesForFile('example/assets/HamburgerArrow.json'),
);
var mockAsset = FakeAssetBundle({
'hamburger.json': hamburgerData,
'other.json': hamburgerData,
});
await tester.pumpWidget(
DefaultAssetBundle(
bundle: mockAsset,
child: Lottie.asset('hamburger.json'),
),
);
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(RawLottie), findsOneWidget);
expect(
find.byWidgetPredicate((w) => w is RawLottie && w.composition != null),
findsOneWidget,
);
await tester.pumpWidget(
DefaultAssetBundle(bundle: mockAsset, child: Lottie.asset('other.json')),
);
await tester.pump(const Duration(milliseconds: 100));
expect(
find.byWidgetPredicate((w) => w is RawLottie && w.composition != null),
findsOneWidget,
);
});
testWidgets('expected an int', (tester) async {
var data = File(
'example/assets/Tests/kona_splash_animation.json',
).readAsBytesSync();
var composition = await LottieComposition.fromBytes(data);
await tester.pumpWidget(Lottie(composition: composition, animate: false));
await tester.pumpAndSettle();
});
}
class SynchronousFile extends Fake implements File {
final File _real;
SynchronousFile(this._real);
@override
String get path => _real.path;
@override
Future<Uint8List> readAsBytes() => Future.value(_real.readAsBytesSync());
}
ByteData bytesForFile(String path) =>
File(path).readAsBytesSync().buffer.asByteData();
class FakeAssetBundle extends Fake implements AssetBundle {
final Map<String, Future<ByteData>> data;
FakeAssetBundle(this.data);
@override
Future<ByteData> load(String key) {
return data[key] ?? (Future.error('Asset $key not found'));
}
}
class _LottieWithSetStateInOnLoaded extends StatefulWidget {
final LottieProvider lottie;
const _LottieWithSetStateInOnLoaded(this.lottie);
@override
State<_LottieWithSetStateInOnLoaded> createState() =>
__LottieWithSetStateInOnLoadedState();
}
class __LottieWithSetStateInOnLoadedState
extends State<_LottieWithSetStateInOnLoaded> {
var loadedCount = 0;
@override
Widget build(BuildContext context) {
return LottieBuilder(
lottie: widget.lottie,
onLoaded: (_) {
setState(() {
++loadedCount;
});
},
);
}
}