// 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 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import '../scheduler/scheduler_tester.dart'; class BogusCurve extends Curve { const BogusCurve(); @override double transform(double t) => 100.0; } void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); WidgetsBinding.instance ..resetEpoch() ..platformDispatcher.onBeginFrame = null ..platformDispatcher.onDrawFrame = null; }); test('toString control test', () { expect(kAlwaysCompleteAnimation, hasOneLineDescription); expect(kAlwaysDismissedAnimation, hasOneLineDescription); expect(const AlwaysStoppedAnimation(0.5), hasOneLineDescription); var curvedAnimation = CurvedAnimation(parent: kAlwaysDismissedAnimation, curve: Curves.ease); expect(curvedAnimation, hasOneLineDescription); curvedAnimation.reverseCurve = Curves.elasticOut; expect(curvedAnimation, hasOneLineDescription); final controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: const TestVSync(), ); controller ..value = 0.5 ..reverse(); curvedAnimation = CurvedAnimation( parent: controller, curve: Curves.ease, reverseCurve: Curves.elasticOut, ); expect(curvedAnimation, hasOneLineDescription); controller.stop(); }); test('ProxyAnimation.toString control test', () { final animation = ProxyAnimation(); expect(animation.value, 0.0); expect(animation.status, AnimationStatus.dismissed); expect(animation, hasOneLineDescription); animation.parent = kAlwaysDismissedAnimation; expect(animation, hasOneLineDescription); }); test('ProxyAnimation set parent generates value changed', () { final controller = AnimationController(vsync: const TestVSync()); controller.value = 0.5; var didReceiveCallback = false; final animation = ProxyAnimation() ..addListener(() { didReceiveCallback = true; }); expect(didReceiveCallback, isFalse); animation.parent = controller; expect(didReceiveCallback, isTrue); didReceiveCallback = false; expect(didReceiveCallback, isFalse); controller.value = 0.6; expect(didReceiveCallback, isTrue); }); test('ReverseAnimation calls listeners', () { final controller = AnimationController(vsync: const TestVSync()); controller.value = 0.5; var didReceiveCallback = false; void listener() { didReceiveCallback = true; } final animation = ReverseAnimation(controller)..addListener(listener); expect(didReceiveCallback, isFalse); controller.value = 0.6; expect(didReceiveCallback, isTrue); didReceiveCallback = false; animation.removeListener(listener); expect(didReceiveCallback, isFalse); controller.value = 0.7; expect(didReceiveCallback, isFalse); expect(animation, hasOneLineDescription); }); test('TrainHoppingAnimation', () { final currentTrain = AnimationController(vsync: const TestVSync()); addTearDown(currentTrain.dispose); final nextTrain = AnimationController(vsync: const TestVSync()); addTearDown(nextTrain.dispose); currentTrain.value = 0.5; nextTrain.value = 0.75; var didSwitchTrains = false; final animation = TrainHoppingAnimation( currentTrain, nextTrain, onSwitchedTrain: () { didSwitchTrains = true; }, ); expect(didSwitchTrains, isFalse); expect(animation.value, 0.5); expect(animation, hasOneLineDescription); nextTrain.value = 0.25; expect(didSwitchTrains, isTrue); expect(animation.value, 0.25); expect(animation, hasOneLineDescription); expect(animation.toString(), contains('no next')); }); test('TrainHoppingAnimation dispatches memory events', () async { await expectLater( await memoryEvents( () => TrainHoppingAnimation( const AlwaysStoppedAnimation(1), const AlwaysStoppedAnimation(1), ).dispose(), TrainHoppingAnimation, ), areCreateAndDispose, ); }); // Regression test for https://github.com/flutter/flutter/issues/178336. test('TrainHoppingAnimation notifies status listeners', () { final controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); addTearDown(controller.dispose); final animation = TrainHoppingAnimation( Tween(begin: 1.0, end: -1.0).animate(controller), Tween(begin: -1.0, end: 1.0).animate(controller), ); addTearDown(animation.dispose); final statusLog = []; animation.addStatusListener(statusLog.add); expect(statusLog, isEmpty); controller.forward(); expect(statusLog, equals([AnimationStatus.forward])); statusLog.clear(); controller.reverse(); expect(statusLog, equals([AnimationStatus.dismissed])); }); test('AnimationMean control test', () { final left = AnimationController(value: 0.5, vsync: const TestVSync()); final right = AnimationController(vsync: const TestVSync()); final mean = AnimationMean(left: left, right: right); expect(mean, hasOneLineDescription); expect(mean.value, equals(0.25)); final log = []; void logValue() { log.add(mean.value); } mean.addListener(logValue); right.value = 1.0; expect(mean.value, equals(0.75)); expect(log, equals([0.75])); log.clear(); mean.removeListener(logValue); left.value = 0.0; expect(mean.value, equals(0.50)); expect(log, isEmpty); }); test('AnimationMax control test', () { final first = AnimationController(value: 0.5, vsync: const TestVSync()); final second = AnimationController(vsync: const TestVSync()); final max = AnimationMax(first, second); expect(max, hasOneLineDescription); expect(max.value, equals(0.5)); final log = []; void logValue() { log.add(max.value); } max.addListener(logValue); second.value = 1.0; expect(max.value, equals(1.0)); expect(log, equals([1.0])); log.clear(); max.removeListener(logValue); first.value = 0.0; expect(max.value, equals(1.0)); expect(log, isEmpty); }); test('AnimationMin control test', () { final first = AnimationController(value: 0.5, vsync: const TestVSync()); final second = AnimationController(vsync: const TestVSync()); final min = AnimationMin(first, second); expect(min, hasOneLineDescription); expect(min.value, equals(0.0)); final log = []; void logValue() { log.add(min.value); } min.addListener(logValue); second.value = 1.0; expect(min.value, equals(0.5)); expect(log, equals([0.5])); log.clear(); min.removeListener(logValue); first.value = 0.25; expect(min.value, equals(0.25)); expect(log, isEmpty); }); test('CurvedAnimation with bogus curve', () { final controller = AnimationController(vsync: const TestVSync()); final curved = CurvedAnimation(parent: controller, curve: const BogusCurve()); FlutterError? error; try { curved.value; } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect( error!.toStringDeep(), // RegExp matcher is required here due to flutter web and flutter mobile generating // slightly different floating point numbers // in Flutter web 0.0 sometimes just appears as 0. or 0 matches( RegExp(r''' FlutterError Invalid curve endpoint at \d+(\.\d*)?\. Curves must map 0\.0 to near zero and 1\.0 to near one but BogusCurve mapped \d+(\.\d*)? to \d+(\.\d*)?, which is near \d+(\.\d*)?\. ''', multiLine: true), ), ); }); test('CurvedAnimation running with different forward and reverse durations.', () { final controller = AnimationController( duration: const Duration(milliseconds: 100), reverseDuration: const Duration(milliseconds: 50), vsync: const TestVSync(), ); final curved = CurvedAnimation( parent: controller, curve: Curves.linear, reverseCurve: Curves.linear, ); controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 10)); expect(curved.value, moreOrLessEquals(0.1)); tick(const Duration(milliseconds: 20)); expect(curved.value, moreOrLessEquals(0.2)); tick(const Duration(milliseconds: 30)); expect(curved.value, moreOrLessEquals(0.3)); tick(const Duration(milliseconds: 40)); expect(curved.value, moreOrLessEquals(0.4)); tick(const Duration(milliseconds: 50)); expect(curved.value, moreOrLessEquals(0.5)); tick(const Duration(milliseconds: 60)); expect(curved.value, moreOrLessEquals(0.6)); tick(const Duration(milliseconds: 70)); expect(curved.value, moreOrLessEquals(0.7)); tick(const Duration(milliseconds: 80)); expect(curved.value, moreOrLessEquals(0.8)); tick(const Duration(milliseconds: 90)); expect(curved.value, moreOrLessEquals(0.9)); tick(const Duration(milliseconds: 100)); expect(curved.value, moreOrLessEquals(1.0)); controller.reverse(); tick(const Duration(milliseconds: 110)); expect(curved.value, moreOrLessEquals(1.0)); tick(const Duration(milliseconds: 120)); expect(curved.value, moreOrLessEquals(0.8)); tick(const Duration(milliseconds: 130)); expect(curved.value, moreOrLessEquals(0.6)); tick(const Duration(milliseconds: 140)); expect(curved.value, moreOrLessEquals(0.4)); tick(const Duration(milliseconds: 150)); expect(curved.value, moreOrLessEquals(0.2)); tick(const Duration(milliseconds: 160)); expect(curved.value, moreOrLessEquals(0.0)); }); test('CurvedAnimation stops listening to parent when disposed.', () async { const forwardCurve = Interval(0.0, 0.5); const reverseCurve = Interval(0.5, 1.0); final controller = AnimationController( duration: const Duration(milliseconds: 100), reverseDuration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); final curved = CurvedAnimation( parent: controller, curve: forwardCurve, reverseCurve: reverseCurve, ); expect(forwardCurve.transform(0.5), 1.0); expect(reverseCurve.transform(0.5), 0.0); controller.forward(from: 0.5); expect(controller.status, equals(AnimationStatus.forward)); expect(curved.value, equals(1.0)); controller.value = 1.0; expect(controller.status, equals(AnimationStatus.completed)); controller.reverse(from: 0.5); expect(controller.status, equals(AnimationStatus.reverse)); expect(curved.value, equals(0.0)); expect(curved.isDisposed, isFalse); curved.dispose(); expect(curved.isDisposed, isTrue); controller.value = 0.0; expect(controller.status, equals(AnimationStatus.dismissed)); controller.forward(from: 0.5); expect(controller.status, equals(AnimationStatus.forward)); expect(curved.value, equals(0.0)); }); test('ReverseAnimation running with different forward and reverse durations.', () { final controller = AnimationController( duration: const Duration(milliseconds: 100), reverseDuration: const Duration(milliseconds: 50), vsync: const TestVSync(), ); final reversed = ReverseAnimation( CurvedAnimation(parent: controller, curve: Curves.linear, reverseCurve: Curves.linear), ); controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 10)); expect(reversed.value, moreOrLessEquals(0.9)); tick(const Duration(milliseconds: 20)); expect(reversed.value, moreOrLessEquals(0.8)); tick(const Duration(milliseconds: 30)); expect(reversed.value, moreOrLessEquals(0.7)); tick(const Duration(milliseconds: 40)); expect(reversed.value, moreOrLessEquals(0.6)); tick(const Duration(milliseconds: 50)); expect(reversed.value, moreOrLessEquals(0.5)); tick(const Duration(milliseconds: 60)); expect(reversed.value, moreOrLessEquals(0.4)); tick(const Duration(milliseconds: 70)); expect(reversed.value, moreOrLessEquals(0.3)); tick(const Duration(milliseconds: 80)); expect(reversed.value, moreOrLessEquals(0.2)); tick(const Duration(milliseconds: 90)); expect(reversed.value, moreOrLessEquals(0.1)); tick(const Duration(milliseconds: 100)); expect(reversed.value, moreOrLessEquals(0.0)); controller.reverse(); tick(const Duration(milliseconds: 110)); expect(reversed.value, moreOrLessEquals(0.0)); tick(const Duration(milliseconds: 120)); expect(reversed.value, moreOrLessEquals(0.2)); tick(const Duration(milliseconds: 130)); expect(reversed.value, moreOrLessEquals(0.4)); tick(const Duration(milliseconds: 140)); expect(reversed.value, moreOrLessEquals(0.6)); tick(const Duration(milliseconds: 150)); expect(reversed.value, moreOrLessEquals(0.8)); tick(const Duration(milliseconds: 160)); expect(reversed.value, moreOrLessEquals(1.0)); }); test('TweenSequence', () { final controller = AnimationController(vsync: const TestVSync()); final Animation animation = TweenSequence(>[ TweenSequenceItem(tween: Tween(begin: 5.0, end: 10.0), weight: 4.0), TweenSequenceItem(tween: ConstantTween(10.0), weight: 2.0), TweenSequenceItem(tween: Tween(begin: 10.0, end: 5.0), weight: 4.0), ]).animate(controller); expect(animation.value, 5.0); controller.value = 0.2; expect(animation.value, 7.5); controller.value = 0.4; expect(animation.value, 10.0); controller.value = 0.6; expect(animation.value, 10.0); controller.value = 0.8; expect(animation.value, 7.5); controller.value = 1.0; expect(animation.value, 5.0); }); test('TweenSequence with curves', () { final controller = AnimationController(vsync: const TestVSync()); final Animation animation = TweenSequence(>[ TweenSequenceItem( tween: Tween( begin: 5.0, end: 10.0, ).chain(CurveTween(curve: const Interval(0.5, 1.0))), weight: 4.0, ), TweenSequenceItem( tween: ConstantTween( 10.0, ).chain(CurveTween(curve: Curves.linear)), // linear is a no-op weight: 2.0, ), TweenSequenceItem( tween: Tween( begin: 10.0, end: 5.0, ).chain(CurveTween(curve: const Interval(0.0, 0.5))), weight: 4.0, ), ]).animate(controller); expect(animation.value, 5.0); controller.value = 0.2; expect(animation.value, 5.0); controller.value = 0.4; expect(animation.value, 10.0); controller.value = 0.6; expect(animation.value, 10.0); controller.value = 0.8; expect(animation.value, 5.0); controller.value = 1.0; expect(animation.value, 5.0); }); test('TweenSequence, one tween', () { final controller = AnimationController(vsync: const TestVSync()); final Animation animation = TweenSequence(>[ TweenSequenceItem(tween: Tween(begin: 5.0, end: 10.0), weight: 1.0), ]).animate(controller); expect(animation.value, 5.0); controller.value = 0.5; expect(animation.value, 7.5); controller.value = 1.0; expect(animation.value, 10.0); }); test('$CurvedAnimation dispatches memory events', () async { await expectLater( await memoryEvents( () => CurvedAnimation( parent: AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ), curve: Curves.linear, ).dispose(), CurvedAnimation, ), areCreateAndDispose, ); }); }