// 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_test/flutter_test.dart'; void main() { testWidgets('Controller expands and collapses the widget', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( MaterialApp( home: Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), ), ), ); expect(find.text('Body'), findsNothing); controller.expand(); await tester.pumpAndSettle(); expect(find.text('Body'), findsOneWidget); controller.collapse(); await tester.pumpAndSettle(); expect(find.text('Body'), findsNothing); controller.dispose(); }); testWidgets('Can listen to the expansion state', (WidgetTester tester) async { final controller = ExpansibleController(); bool? expansionState; controller.addListener(() { expansionState = controller.isExpanded; }); await tester.pumpWidget( MaterialApp( home: Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), ), ), ); // Tap on the header to toggle the expansion. await tester.tap(find.text('Header')); await tester.pumpAndSettle(); expect(expansionState, true); await tester.tap(find.text('Header')); await tester.pumpAndSettle(); expect(expansionState, false); // Use the controller to toggle the expansion. controller.expand(); await tester.pumpAndSettle(); expect(expansionState, true); controller.collapse(); await tester.pumpAndSettle(); expect(expansionState, false); controller.dispose(); }); testWidgets('Can set expansible to be initially expanded', (WidgetTester tester) async { final controller = ExpansibleController(); controller.expand(); await tester.pumpWidget( MaterialApp( home: SingleChildScrollView( child: Column( children: [ Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), ), ], ), ), ), ); expect(find.text('Body'), findsOneWidget); await tester.tap(find.text('Header')); await tester.pumpAndSettle(); expect(find.text('Body'), findsNothing); controller.dispose(); }); testWidgets('Can compose header and body with expansibleBuilder', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( MaterialApp( home: Expansible( controller: controller, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), expansibleBuilder: (BuildContext context, Widget header, Widget body, Animation animation) { return header; }, ), ), ); // Tap on the header to toggle the expansion. await tester.tap(find.text('Header')); await tester.pumpAndSettle(); expect(find.text('Header'), findsOneWidget); expect(find.text('Body'), findsNothing); await tester.tap(find.text('Header')); await tester.pumpAndSettle(); expect(find.text('Header'), findsOneWidget); expect(find.text('Body'), findsNothing); // Use the controller to toggle the expansion. controller.expand(); await tester.pumpAndSettle(); expect(find.text('Header'), findsOneWidget); expect(find.text('Body'), findsNothing); controller.collapse(); await tester.pumpAndSettle(); expect(find.text('Header'), findsOneWidget); expect(find.text('Body'), findsNothing); controller.dispose(); }); testWidgets('Respects maintainState', (WidgetTester tester) async { final controller1 = ExpansibleController(); final controller2 = ExpansibleController(); await tester.pumpWidget( MaterialApp( home: SingleChildScrollView( child: Column( children: [ Expansible( controller: controller1, maintainState: false, bodyBuilder: (BuildContext context, Animation animation) => const Text('Maintaining State'), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller1.isExpanded ? controller1.collapse : controller1.expand, child: const Text('Header'), ), ), Expansible( controller: controller2, bodyBuilder: (BuildContext context, Animation animation) => const Text('Discarding State'), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller2.isExpanded ? controller2.collapse : controller2.expand, child: const Text('Header'), ), ), ], ), ), ), ); // This text is not offstage while the expansible widget is collapsed. expect(find.text('Maintaining State', skipOffstage: false), findsNothing); expect(find.text('Maintaining State'), findsNothing); // This text is not displayed while the expansible widget is collapsed. expect(find.text('Discarding State'), findsNothing); controller1.dispose(); controller2.dispose(); }); testWidgets('Respects animation duration and curves', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( MaterialApp( home: Expansible( controller: controller, duration: const Duration(milliseconds: 120), curve: Curves.easeOut, reverseCurve: Curves.easeIn, bodyBuilder: (BuildContext context, Animation animation) => const SizedBox(height: 50.0, child: Placeholder()), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), ), ), ); expect(find.byType(Placeholder), findsNothing); await tester.tap(find.text('Header')); // Check that the curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 90.08984375); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); // Since the animation has completed, the vertical position doesn't change. await tester.pump(const Duration(milliseconds: 60)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); await tester.pumpAndSettle(); await tester.tap(find.text('Header')); // Check that the reverse curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 80.91015625); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); expect(find.byType(Placeholder), findsNothing); controller.dispose(); }); testWidgets('ExpansionTile can accept a new controller', (WidgetTester tester) async { final controller1 = ExpansibleController(); final controller2 = ExpansibleController(); addTearDown(() { controller1.dispose(); controller2.dispose(); }); await tester.pumpWidget( MaterialApp( home: Material( child: Expansible( controller: controller1, headerBuilder: (_, _) => const Text('Header'), bodyBuilder: (_, _) => const Text('Body'), ), ), ), ); expect(find.text('Body'), findsNothing); expect(controller1.isExpanded, isFalse); controller1.expand(); expect(controller1.isExpanded, isTrue); await tester.pumpAndSettle(); expect(find.text('Body'), findsOne); controller1.collapse(); expect(controller1.isExpanded, isFalse); await tester.pumpAndSettle(); expect(find.text('Body'), findsNothing); await tester.pumpWidget( MaterialApp( home: Material( child: Expansible( controller: controller2, headerBuilder: (_, _) => const Text('Header'), bodyBuilder: (_, _) => const Text('Body'), ), ), ), ); expect(find.text('Body'), findsNothing); expect(controller2.isExpanded, isFalse); controller2.expand(); expect(controller2.isExpanded, isTrue); await tester.pumpAndSettle(); expect(find.text('Body'), findsOne); controller2.collapse(); expect(controller2.isExpanded, isFalse); await tester.pumpAndSettle(); expect(find.text('Body'), findsNothing); }); testWidgets('Expansible can accept a new controller with a different state', ( WidgetTester tester, ) async { final controller1 = ExpansibleController(); final controller2 = ExpansibleController(); addTearDown(() { controller1.dispose(); controller2.dispose(); }); await tester.pumpWidget( MaterialApp( home: Material( child: Expansible( controller: controller1, headerBuilder: (_, _) => const Text('Header'), bodyBuilder: (_, _) => const Text('Body'), ), ), ), ); expect(find.text('Body'), findsNothing); expect(controller1.isExpanded, isFalse); controller1.expand(); expect(controller1.isExpanded, isTrue); await tester.pumpAndSettle(); expect(find.text('Body'), findsOne); await tester.pumpWidget( MaterialApp( home: Material( child: Expansible( controller: controller2, headerBuilder: (_, _) => const Text('Header'), bodyBuilder: (_, _) => const Text('Body'), ), ), ), ); await tester.pumpAndSettle(); expect(controller2.isExpanded, isFalse); expect(find.text('Body'), findsNothing); controller2.expand(); expect(controller2.isExpanded, isTrue); await tester.pumpAndSettle(); expect(find.text('Body'), findsOne); controller2.collapse(); expect(controller2.isExpanded, isFalse); await tester.pumpAndSettle(); expect(find.text('Body'), findsNothing); }); testWidgets('Respects animationStyle duration and curves', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( MaterialApp( home: Expansible( controller: controller, animationStyle: const AnimationStyle( duration: Duration(milliseconds: 120), curve: Curves.easeOut, reverseCurve: Curves.easeIn, ), bodyBuilder: (BuildContext context, Animation animation) => const SizedBox(height: 50.0, child: Placeholder()), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), ), ), ); expect(find.byType(Placeholder), findsNothing); await tester.tap(find.text('Header')); // Check that the curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 90.08984375); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); // Since the animation has completed, the vertical position doesn't change. await tester.pump(const Duration(milliseconds: 60)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); await tester.pumpAndSettle(); await tester.tap(find.text('Header')); // Check that the reverse curve is respected. await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 80.91015625); // The animation has completed. await tester.pump(const Duration(milliseconds: 60) + const Duration(microseconds: 1)); expect(find.byType(Placeholder), findsNothing); controller.dispose(); }); testWidgets('AnimationStyle takes precedence over deprecated properties', ( WidgetTester tester, ) async { final controller = ExpansibleController(); await tester.pumpWidget( MaterialApp( home: Expansible( controller: controller, animationStyle: const AnimationStyle( duration: Duration(milliseconds: 100), curve: Curves.linear, ), bodyBuilder: (BuildContext context, Animation animation) => const SizedBox(height: 50.0, child: Placeholder()), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), ), ), ); expect(find.byType(Placeholder), findsNothing); await tester.tap(find.text('Header')); // Check that the animationStyle duration (100ms) is used await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // With linear curve at 50ms out of 100ms, should be at approximately 50% height final double midAnimationY = tester.getBottomLeft(find.byType(Placeholder)).dy; // Should be more than base (48.0) and less than fully expanded (98.0) expect(midAnimationY, greaterThan(48.0)); expect(midAnimationY, lessThan(98.0)); // Animation should complete at 100ms await tester.pump(const Duration(milliseconds: 50) + const Duration(microseconds: 1)); expect(tester.getBottomLeft(find.byType(Placeholder)).dy, 98.0); controller.dispose(); }); testWidgets('AnimationStyle.noAnimation disables animation', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( MaterialApp( home: Expansible( controller: controller, animationStyle: AnimationStyle.noAnimation, bodyBuilder: (BuildContext context, Animation animation) => const Text('Body'), headerBuilder: (BuildContext context, Animation animation) => GestureDetector( onTap: controller.isExpanded ? controller.collapse : controller.expand, child: const Text('Header'), ), ), ), ); expect(find.text('Body'), findsNothing); controller.expand(); await tester.pump(); expect(find.text('Body'), findsOneWidget); controller.collapse(); await tester.pump(); expect(find.text('Body'), findsNothing); controller.dispose(); }); }