// 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/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('AnimatedCrossFade test', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Center( child: AnimatedCrossFade( firstChild: SizedBox( width: 100.0, height: 100.0, ), secondChild: SizedBox( width: 200.0, height: 200.0, ), duration: Duration(milliseconds: 200), crossFadeState: CrossFadeState.showFirst, ), ), ), ); expect(find.byType(FadeTransition), findsNWidgets(2)); RenderBox box = tester.renderObject(find.byType(AnimatedCrossFade)); expect(box.size.width, equals(100.0)); expect(box.size.height, equals(100.0)); await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Center( child: AnimatedCrossFade( firstChild: SizedBox( width: 100.0, height: 100.0, ), secondChild: SizedBox( width: 200.0, height: 200.0, ), duration: Duration(milliseconds: 200), crossFadeState: CrossFadeState.showSecond, ), ), ), ); await tester.pump(const Duration(milliseconds: 100)); expect(find.byType(FadeTransition), findsNWidgets(2)); box = tester.renderObject(find.byType(AnimatedCrossFade)); expect(box.size.width, equals(150.0)); expect(box.size.height, equals(150.0)); }); testWidgets('AnimatedCrossFade test showSecond', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Center( child: AnimatedCrossFade( firstChild: SizedBox( width: 100.0, height: 100.0, ), secondChild: SizedBox( width: 200.0, height: 200.0, ), duration: Duration(milliseconds: 200), crossFadeState: CrossFadeState.showSecond, ), ), ), ); expect(find.byType(FadeTransition), findsNWidgets(2)); final RenderBox box = tester.renderObject(find.byType(AnimatedCrossFade)); expect(box.size.width, equals(200.0)); expect(box.size.height, equals(200.0)); }); testWidgets('AnimatedCrossFade alignment (VISUAL)', (WidgetTester tester) async { final Key firstKey = UniqueKey(); final Key secondKey = UniqueKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: AnimatedCrossFade( alignment: Alignment.bottomRight, firstChild: SizedBox( key: firstKey, width: 100.0, height: 100.0, ), secondChild: SizedBox( key: secondKey, width: 200.0, height: 200.0, ), duration: const Duration(milliseconds: 200), crossFadeState: CrossFadeState.showFirst, ), ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: AnimatedCrossFade( alignment: Alignment.bottomRight, firstChild: SizedBox( key: firstKey, width: 100.0, height: 100.0, ), secondChild: SizedBox( key: secondKey, width: 200.0, height: 200.0, ), duration: const Duration(milliseconds: 200), crossFadeState: CrossFadeState.showSecond, ), ), ), ); await tester.pump(const Duration(milliseconds: 100)); final RenderBox box1 = tester.renderObject(find.byKey(firstKey)); final RenderBox box2 = tester.renderObject(find.byKey(secondKey)); expect(box1.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); expect(box2.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); }); testWidgets('AnimatedCrossFade alignment (LTR)', (WidgetTester tester) async { final Key firstKey = UniqueKey(); final Key secondKey = UniqueKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: AnimatedCrossFade( alignment: AlignmentDirectional.bottomEnd, firstChild: SizedBox( key: firstKey, width: 100.0, height: 100.0, ), secondChild: SizedBox( key: secondKey, width: 200.0, height: 200.0, ), duration: const Duration(milliseconds: 200), crossFadeState: CrossFadeState.showFirst, ), ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: AnimatedCrossFade( alignment: AlignmentDirectional.bottomEnd, firstChild: SizedBox( key: firstKey, width: 100.0, height: 100.0, ), secondChild: SizedBox( key: secondKey, width: 200.0, height: 200.0, ), duration: const Duration(milliseconds: 200), crossFadeState: CrossFadeState.showSecond, ), ), ), ); await tester.pump(const Duration(milliseconds: 100)); final RenderBox box1 = tester.renderObject(find.byKey(firstKey)); final RenderBox box2 = tester.renderObject(find.byKey(secondKey)); expect(box1.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); expect(box2.localToGlobal(Offset.zero), const Offset(275.0, 175.0)); }); testWidgets('AnimatedCrossFade alignment (RTL)', (WidgetTester tester) async { final Key firstKey = UniqueKey(); final Key secondKey = UniqueKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: Center( child: AnimatedCrossFade( alignment: AlignmentDirectional.bottomEnd, firstChild: SizedBox( key: firstKey, width: 100.0, height: 100.0, ), secondChild: SizedBox( key: secondKey, width: 200.0, height: 200.0, ), duration: const Duration(milliseconds: 200), crossFadeState: CrossFadeState.showFirst, ), ), ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: Center( child: AnimatedCrossFade( alignment: AlignmentDirectional.bottomEnd, firstChild: SizedBox( key: firstKey, width: 100.0, height: 100.0, ), secondChild: SizedBox( key: secondKey, width: 200.0, height: 200.0, ), duration: const Duration(milliseconds: 200), crossFadeState: CrossFadeState.showSecond, ), ), ), ); await tester.pump(const Duration(milliseconds: 100)); final RenderBox box1 = tester.renderObject(find.byKey(firstKey)); final RenderBox box2 = tester.renderObject(find.byKey(secondKey)); expect(box1.localToGlobal(Offset.zero), const Offset(325.0, 175.0)); expect(box2.localToGlobal(Offset.zero), const Offset(325.0, 175.0)); }); Widget crossFadeWithWatcher({ bool towardsSecond = false }) { return Directionality( textDirection: TextDirection.ltr, child: AnimatedCrossFade( firstChild: const _TickerWatchingWidget(), secondChild: Container(), crossFadeState: towardsSecond ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: const Duration(milliseconds: 50), ), ); } testWidgets('AnimatedCrossFade preserves widget state', (WidgetTester tester) async { await tester.pumpWidget(crossFadeWithWatcher()); _TickerWatchingWidgetState findState() => tester.state(find.byType(_TickerWatchingWidget)); final _TickerWatchingWidgetState state = findState(); await tester.pumpWidget(crossFadeWithWatcher(towardsSecond: true)); for (int i = 0; i < 3; i += 1) { await tester.pump(const Duration(milliseconds: 25)); expect(findState(), same(state)); } }); testWidgets('AnimatedCrossFade switches off TickerMode and semantics on faded out widget', (WidgetTester tester) async { ExcludeSemantics findSemantics() { return tester.widget(find.descendant( of: find.byKey(const ValueKey<CrossFadeState>(CrossFadeState.showFirst)), matching: find.byType(ExcludeSemantics), )); } await tester.pumpWidget(crossFadeWithWatcher()); final _TickerWatchingWidgetState state = tester.state(find.byType(_TickerWatchingWidget)); expect(state.ticker.muted, false); expect(findSemantics().excluding, false); await tester.pumpWidget(crossFadeWithWatcher(towardsSecond: true)); for (int i = 0; i < 2; i += 1) { await tester.pump(const Duration(milliseconds: 25)); // Animations are kept alive in the middle of cross-fade expect(state.ticker.muted, false); // Semantics are turned off immediately on the widget that's fading out expect(findSemantics().excluding, true); } // In the final state both animations and semantics should be off on the // widget that's faded out. await tester.pump(const Duration(milliseconds: 25)); expect(state.ticker.muted, true); expect(findSemantics().excluding, true); }); testWidgets('AnimatedCrossFade.layoutBuilder', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: AnimatedCrossFade( firstChild: Text('AAA', textDirection: TextDirection.ltr), secondChild: Text('BBB', textDirection: TextDirection.ltr), crossFadeState: CrossFadeState.showFirst, duration: Duration(milliseconds: 50), ), ), ); expect(find.text('AAA'), findsOneWidget); expect(find.text('BBB'), findsOneWidget); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: AnimatedCrossFade( firstChild: const Text('AAA', textDirection: TextDirection.ltr), secondChild: const Text('BBB', textDirection: TextDirection.ltr), crossFadeState: CrossFadeState.showFirst, duration: const Duration(milliseconds: 50), layoutBuilder: (Widget a, Key aKey, Widget b, Key bKey) => a, ), ), ); expect(find.text('AAA'), findsOneWidget); expect(find.text('BBB'), findsNothing); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: AnimatedCrossFade( firstChild: const Text('AAA', textDirection: TextDirection.ltr), secondChild: const Text('BBB', textDirection: TextDirection.ltr), crossFadeState: CrossFadeState.showSecond, duration: const Duration(milliseconds: 50), layoutBuilder: (Widget a, Key aKey, Widget b, Key bKey) => a, ), ), ); expect(find.text('BBB'), findsOneWidget); expect(find.text('AAA'), findsNothing); }); testWidgets('AnimatedCrossFade test focus', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: AnimatedCrossFade( firstChild: TextButton(onPressed: () {}, child: const Text('AAA')), secondChild: TextButton(onPressed: () {}, child: const Text('BBB')), crossFadeState: CrossFadeState.showFirst, duration: const Duration(milliseconds: 50), ), ), ); final FocusNode visibleNode = Focus.of(tester.element(find.text('AAA')), scopeOk: true); visibleNode.requestFocus(); await tester.pump(); expect(visibleNode.hasPrimaryFocus, isTrue); final FocusNode hiddenNode = Focus.of(tester.element(find.text('BBB')), scopeOk: true); hiddenNode.requestFocus(); await tester.pump(); expect(hiddenNode.hasPrimaryFocus, isFalse); }); testWidgets('AnimatedCrossFade bottom child can have focus', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: AnimatedCrossFade( firstChild: TextButton(onPressed: () {}, child: const Text('AAA')), secondChild: TextButton(onPressed: () {}, child: const Text('BBB')), crossFadeState: CrossFadeState.showFirst, duration: const Duration(milliseconds: 50), excludeBottomFocus: false, ), ), ); final FocusNode visibleNode = Focus.of(tester.element(find.text('AAA')), scopeOk: true); visibleNode.requestFocus(); await tester.pump(); expect(visibleNode.hasPrimaryFocus, isTrue); final FocusNode hiddenNode = Focus.of(tester.element(find.text('BBB')), scopeOk: true); hiddenNode.requestFocus(); await tester.pump(); expect(hiddenNode.hasPrimaryFocus, isTrue); }); testWidgets('AnimatedCrossFade second child do not receive touch events', (WidgetTester tester) async { int numberOfTouchEventNoticed = 0; Future<void> buildAnimatedFrame(CrossFadeState crossFadeState) { return tester.pumpWidget( SizedBox( width: 300, height: 600, child: Directionality( textDirection: TextDirection.ltr, child: AnimatedCrossFade( firstChild: const Text('AAA'), secondChild: TextButton( style: TextButton.styleFrom(minimumSize: const Size(double.infinity, 600)), onPressed: () { numberOfTouchEventNoticed++; }, child: const Text('BBB'), ), crossFadeState: crossFadeState, duration: const Duration(milliseconds: 50), ), ), ), ); } Future<void> touchSecondButton() async { final TestGesture gestureTouchSecondButton = await tester .startGesture(const Offset(150, 300)); return gestureTouchSecondButton.up(); } await buildAnimatedFrame(CrossFadeState.showSecond); await touchSecondButton(); expect(numberOfTouchEventNoticed, 1); await buildAnimatedFrame(CrossFadeState.showFirst); await touchSecondButton(); await touchSecondButton(); expect(numberOfTouchEventNoticed, 1); }); } class _TickerWatchingWidget extends StatefulWidget { const _TickerWatchingWidget(); @override State<StatefulWidget> createState() => _TickerWatchingWidgetState(); } class _TickerWatchingWidgetState extends State<_TickerWatchingWidget> with SingleTickerProviderStateMixin { late Ticker ticker; @override void initState() { super.initState(); ticker = createTicker((_) { })..start(); } @override Widget build(BuildContext context) => Container(); @override void dispose() { ticker.dispose(); super.dispose(); } }