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(); var androidCompleter = Completer(); 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(); var androidCompleter = Completer(); 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( 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(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(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(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(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(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(find.byType(Lottie)).composition, isNull); await tester.pump(); expect(tester.widget(find.byType(Lottie)).composition, isNotNull); await tester.pumpWidget(Column(children: [lottieWidget, lottieWidget])); expect( tester.widget(find.byType(Lottie).at(0)).composition, isNotNull, ); expect( tester.widget(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(find.byType(Lottie)).composition, isNull); await tester.pump(); expect(tester.widget(find.byType(Lottie)).composition, isNotNull); Lottie.cache.clear(); await tester.pumpWidget(Center(child: lottieWidget)); expect(tester.widget(find.byType(Lottie)).composition, isNull); await tester.pump(); expect(tester.widget(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 readAsBytes() => Future.value(_real.readAsBytesSync()); } ByteData bytesForFile(String path) => File(path).readAsBytesSync().buffer.asByteData(); class FakeAssetBundle extends Fake implements AssetBundle { final Map> data; FakeAssetBundle(this.data); @override Future 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; }); }, ); } }