blob: d251a1bef4473f2c7794707b7eb2487a03b72de1 [file] [log] [blame] [edit]
// 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<double> animation) => const Text('Body'),
headerBuilder: (BuildContext context, Animation<double> 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<double> animation) => const Text('Body'),
headerBuilder: (BuildContext context, Animation<double> 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: <Widget>[
Expansible(
controller: controller,
bodyBuilder: (BuildContext context, Animation<double> animation) =>
const Text('Body'),
headerBuilder: (BuildContext context, Animation<double> 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<double> animation) => const Text('Body'),
headerBuilder: (BuildContext context, Animation<double> animation) => GestureDetector(
onTap: controller.isExpanded ? controller.collapse : controller.expand,
child: const Text('Header'),
),
expansibleBuilder:
(BuildContext context, Widget header, Widget body, Animation<double> 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: <Widget>[
Expansible(
controller: controller1,
maintainState: false,
bodyBuilder: (BuildContext context, Animation<double> animation) =>
const Text('Maintaining State'),
headerBuilder: (BuildContext context, Animation<double> animation) =>
GestureDetector(
onTap: controller1.isExpanded ? controller1.collapse : controller1.expand,
child: const Text('Header'),
),
),
Expansible(
controller: controller2,
bodyBuilder: (BuildContext context, Animation<double> animation) =>
const Text('Discarding State'),
headerBuilder: (BuildContext context, Animation<double> 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<double> animation) =>
const SizedBox(height: 50.0, child: Placeholder()),
headerBuilder: (BuildContext context, Animation<double> 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<double> animation) =>
const SizedBox(height: 50.0, child: Placeholder()),
headerBuilder: (BuildContext context, Animation<double> 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<double> animation) =>
const SizedBox(height: 50.0, child: Placeholder()),
headerBuilder: (BuildContext context, Animation<double> 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<double> animation) => const Text('Body'),
headerBuilder: (BuildContext context, Animation<double> 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();
});
}