492 lines
16 KiB
Dart
492 lines
16 KiB
Dart
// 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();
|
|
});
|
|
}
|