blob: cea8f6a88c5ae3f84869ee9f2781b83bdb8529ca [file] [log] [blame]
// 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';
class SimpleExpansionPanelListTestWidget extends StatefulWidget {
const SimpleExpansionPanelListTestWidget({
super.key,
this.firstPanelKey,
this.secondPanelKey,
this.canTapOnHeader = false,
this.expandedHeaderPadding,
this.dividerColor,
this.elevation = 2,
});
final Key? firstPanelKey;
final Key? secondPanelKey;
final bool canTapOnHeader;
final Color? dividerColor;
final double elevation;
/// If null, the default [ExpansionPanelList]'s expanded header padding value is applied via [defaultExpandedHeaderPadding]
final EdgeInsets? expandedHeaderPadding;
/// Mirrors the default expanded header padding as its source constants are private.
static EdgeInsets defaultExpandedHeaderPadding()
{
return const ExpansionPanelList().expandedHeaderPadding;
}
@override
State<SimpleExpansionPanelListTestWidget> createState() => _SimpleExpansionPanelListTestWidgetState();
}
class _SimpleExpansionPanelListTestWidgetState extends State<SimpleExpansionPanelListTestWidget> {
List<bool> extendedState = <bool>[false, false];
@override
Widget build(BuildContext context) {
return ExpansionPanelList(
expandedHeaderPadding: widget.expandedHeaderPadding ?? SimpleExpansionPanelListTestWidget.defaultExpandedHeaderPadding(),
expansionCallback: (int index, bool isExpanded) {
setState(() {
extendedState[index] = !extendedState[index];
});
},
dividerColor: widget.dividerColor,
elevation: widget.elevation,
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: widget.firstPanelKey);
},
body: const SizedBox(height: 100.0),
canTapOnHeader: widget.canTapOnHeader,
isExpanded: extendedState[0],
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: widget.secondPanelKey);
},
body: const SizedBox(height: 100.0),
canTapOnHeader: widget.canTapOnHeader,
isExpanded: extendedState[1],
),
],
);
}
}
class ExpansionPanelListSemanticsTest extends StatefulWidget {
const ExpansionPanelListSemanticsTest({ super.key, required this.headerKey });
final Key headerKey;
@override
ExpansionPanelListSemanticsTestState createState() => ExpansionPanelListSemanticsTestState();
}
class ExpansionPanelListSemanticsTestState extends State<ExpansionPanelListSemanticsTest> {
bool headerTapped = false;
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
ExpansionPanelList(
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return MergeSemantics(
key: widget.headerKey,
child: GestureDetector(
onTap: () => headerTapped = true,
child: const Text.rich(
TextSpan(
text:'head1',
),
),
),
);
},
body: const Placeholder(),
),
],
),
],
);
}
}
void main() {
testWidgets('ExpansionPanelList test', (WidgetTester tester) async {
late int capturedIndex;
late bool capturedIsExpanded;
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
capturedIndex = index;
capturedIsExpanded = isExpanded;
},
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
),
],
),
),
),
);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
RenderBox box = tester.renderObject(find.byType(ExpansionPanelList));
final double oldHeight = box.size.height;
expect(find.byType(ExpandIcon), findsOneWidget);
await tester.tap(find.byType(ExpandIcon));
expect(capturedIndex, 0);
expect(capturedIsExpanded, isFalse);
box = tester.renderObject(find.byType(ExpansionPanelList));
expect(box.size.height, equals(oldHeight));
// Now, expand the child panel.
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
capturedIndex = index;
capturedIsExpanded = isExpanded;
},
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
isExpanded: true, // this is the addition
),
],
),
),
),
);
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
box = tester.renderObject(find.byType(ExpansionPanelList));
expect(box.size.height - oldHeight, greaterThanOrEqualTo(100.0)); // 100 + some margin
});
testWidgets('ExpansionPanelList does not merge header when canTapOnHeader is false', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final Key headerKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: ExpansionPanelListSemanticsTest(headerKey: headerKey),
),
);
// Make sure custom gesture detector widget is clickable.
await tester.tap(find.text('head1'));
await tester.pump();
final ExpansionPanelListSemanticsTestState state =
tester.state(find.byType(ExpansionPanelListSemanticsTest));
expect(state.headerTapped, true);
// Check the expansion icon semantics does not merged with header widget.
final Finder expansionIcon = find.descendant(
of: find.ancestor(
of: find.byKey(headerKey),
matching: find.byType(Row),
),
matching: find.byType(ExpandIcon),
);
expect(tester.getSemantics(expansionIcon), matchesSemantics(
label: 'Expand',
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
));
// Check custom header widget semantics is preserved.
final Finder headerWidget = find.descendant(
of: find.byKey(headerKey),
matching: find.byType(RichText),
);
expect(tester.getSemantics(headerWidget), matchesSemantics(
label: 'head1',
hasTapAction: true,
));
handle.dispose();
});
testWidgets('Multiple Panel List test', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: ListView(
children: <ExpansionPanelList>[
ExpansionPanelList(
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
isExpanded: true,
),
],
),
ExpansionPanelList(
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
isExpanded: true,
),
],
),
],
),
),
);
await tester.pump(const Duration(milliseconds: 200));
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
});
testWidgets('Open/close animations', (WidgetTester tester) async {
const Duration kSizeAnimationDuration = Duration(milliseconds: 1000);
// The MaterialGaps animate in using kThemeAnimationDuration (hardcoded),
// which should be less than our test size animation length. So we can assume that they
// appear immediately. Here we just verify that our assumption is true.
expect(kThemeAnimationDuration, lessThan(kSizeAnimationDuration ~/ 2));
Widget build(bool a, bool b, bool c) {
return MaterialApp(
home: Column(
children: <Widget>[
ExpansionPanelList(
animationDuration: kSizeAnimationDuration,
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) => const Placeholder(
fallbackHeight: 12.0,
),
body: const SizedBox(height: 100.0, child: Placeholder(
fallbackHeight: 12.0,
)),
isExpanded: a,
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) => const Placeholder(
fallbackHeight: 12.0,
),
body: const SizedBox(height: 100.0, child: Placeholder()),
isExpanded: b,
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) => const Placeholder(
fallbackHeight: 12.0,
),
body: const SizedBox(height: 100.0, child: Placeholder()),
isExpanded: c,
),
],
),
],
),
);
}
await tester.pumpWidget(build(false, false, false));
expect(tester.renderObjectList(find.byType(AnimatedSize)), hasLength(3));
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 113.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 170.0, 800.0, 0.0));
await tester.pump(const Duration(milliseconds: 200));
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 113.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 170.0, 800.0, 0.0));
await tester.pumpWidget(build(false, true, false));
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 113.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 170.0, 800.0, 0.0));
await tester.pump(kSizeAnimationDuration ~/ 2);
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
final Rect rect1 = tester.getRect(find.byType(AnimatedSize).at(1));
expect(rect1.left, 0.0);
expect(rect1.top, inExclusiveRange(113.0, 113.0 + 16.0 + 32.0)); // 16.0 material gap, plus 16.0 top and bottom margins added to the header
expect(rect1.width, 800.0);
expect(rect1.height, inExclusiveRange(0.0, 100.0));
final Rect rect2 = tester.getRect(find.byType(AnimatedSize).at(2));
expect(rect2, Rect.fromLTWH(0.0, rect1.bottom + 16.0 + 56.0, 800.0, 0.0)); // the 16.0 comes from the MaterialGap being introduced, the 56.0 is the header height.
await tester.pumpWidget(build(false, false, false));
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(1)), rect1);
expect(tester.getRect(find.byType(AnimatedSize).at(2)), rect2);
await tester.pumpWidget(build(false, false, true));
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(1)), rect1);
expect(tester.getRect(find.byType(AnimatedSize).at(2)), rect2);
// a few no-op pumps to make sure there's nothing fishy going on
await tester.pump();
await tester.pump();
await tester.pump();
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(1)), rect1);
expect(tester.getRect(find.byType(AnimatedSize).at(2)), rect2);
await tester.pumpAndSettle();
expect(tester.getRect(find.byType(AnimatedSize).at(0)), const Rect.fromLTWH(0.0, 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(1)), const Rect.fromLTWH(0.0, 56.0 + 1.0 + 56.0, 800.0, 0.0));
expect(tester.getRect(find.byType(AnimatedSize).at(2)), const Rect.fromLTWH(0.0, 56.0 + 1.0 + 56.0 + 16.0 + 16.0 + 48.0 + 16.0, 800.0, 100.0));
});
testWidgets('Radio mode has max of one panel open at a time', (WidgetTester tester) async {
final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
value: 1,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'F' : 'E');
},
body: const SizedBox(height: 100.0),
value: 2,
),
];
final ExpansionPanelList expansionListRadio = ExpansionPanelList.radio(
children: demoItemsRadio,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionListRadio,
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
RenderBox box = tester.renderObject(find.byType(ExpansionPanelList));
double oldHeight = box.size.height;
expect(find.byType(ExpandIcon), findsNWidgets(3));
await tester.tap(find.byType(ExpandIcon).at(0));
box = tester.renderObject(find.byType(ExpansionPanelList));
expect(box.size.height, equals(oldHeight));
await tester.pump(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
// Now the first panel is open
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
box = tester.renderObject(find.byType(ExpansionPanelList));
expect(box.size.height - oldHeight, greaterThanOrEqualTo(100.0)); // 100 + some margin
await tester.tap(find.byType(ExpandIcon).at(1));
box = tester.renderObject(find.byType(ExpansionPanelList));
oldHeight = box.size.height;
await tester.pump(const Duration(milliseconds: 200));
// Now the first panel is closed and the second should be opened
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
expect(box.size.height, greaterThanOrEqualTo(oldHeight));
demoItemsRadio.removeAt(0);
await tester.pumpAndSettle();
// Now the first panel should be opened
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
final List<ExpansionPanel> demoItems = <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'F' : 'E');
},
body: const SizedBox(height: 100.0),
),
];
final ExpansionPanelList expansionList = ExpansionPanelList(
children: demoItems,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionList,
),
),
);
// We've reinitialized with a regular expansion panel so they should all be closed again
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
});
testWidgets('Radio mode calls expansionCallback once if other panels closed', (WidgetTester tester) async {
final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
value: 1,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'F' : 'E');
},
body: const SizedBox(height: 100.0),
value: 2,
),
];
final List<Map<String, dynamic>> callbackHistory = <Map<String, dynamic>>[];
final ExpansionPanelList expansionListRadio = ExpansionPanelList.radio(
expansionCallback: (int index, bool isExpanded) {
callbackHistory.add(<String, dynamic>{
'index': index,
'isExpanded': isExpanded,
});
},
children: demoItemsRadio,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionListRadio,
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
// Open one panel
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pumpAndSettle();
// Callback is invoked once with appropriate arguments
expect(callbackHistory.length, equals(1));
expect(callbackHistory.last['index'], equals(1));
expect(callbackHistory.last['isExpanded'], equals(false));
// Close the same panel
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pumpAndSettle();
// Callback is invoked once with appropriate arguments
expect(callbackHistory.length, equals(2));
expect(callbackHistory.last['index'], equals(1));
expect(callbackHistory.last['isExpanded'], equals(true));
});
testWidgets('Radio mode calls expansionCallback twice if other panel open prior', (WidgetTester tester) async {
final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
value: 1,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'F' : 'E');
},
body: const SizedBox(height: 100.0),
value: 2,
),
];
final List<Map<String, dynamic>> callbackHistory = <Map<String, dynamic>>[];
Map<String, dynamic> callbackResults;
final ExpansionPanelList expansionListRadio = ExpansionPanelList.radio(
expansionCallback: (int index, bool isExpanded) {
callbackHistory.add(<String, dynamic>{
'index': index,
'isExpanded': isExpanded,
});
},
children: demoItemsRadio,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionListRadio,
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
// Open one panel
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pumpAndSettle();
// Callback is invoked once with appropriate arguments
expect(callbackHistory.length, equals(1));
callbackResults = callbackHistory[callbackHistory.length - 1];
expect(callbackResults['index'], equals(1));
expect(callbackResults['isExpanded'], equals(false));
// Close a different panel
await tester.tap(find.byType(ExpandIcon).at(2));
await tester.pumpAndSettle();
// Callback is invoked the first time with correct arguments
expect(callbackHistory.length, equals(3));
callbackResults = callbackHistory[callbackHistory.length - 2];
expect(callbackResults['index'], equals(2));
expect(callbackResults['isExpanded'], equals(false));
// Callback is invoked the second time with correct arguments
callbackResults = callbackHistory[callbackHistory.length - 1];
expect(callbackResults['index'], equals(1));
expect(callbackResults['isExpanded'], equals(false));
});
testWidgets(
'didUpdateWidget accounts for toggling between ExpansionPanelList '
'and ExpansionPaneList.radio',
(WidgetTester tester) async {
bool isRadioList = false;
final List<bool> panelExpansionState = <bool>[
false,
false,
false,
];
ExpansionPanelList buildRadioExpansionPanelList() {
return ExpansionPanelList.radio(
initialOpenPanelValue: 2,
children: <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
value: 1,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'F' : 'E');
},
body: const SizedBox(height: 100.0),
value: 2,
),
],
);
}
ExpansionPanelList buildExpansionPanelList(StateSetter setState) {
return ExpansionPanelList(
expansionCallback: (int index, _) => setState(() { panelExpansionState[index] = !panelExpansionState[index]; }),
children: <ExpansionPanel>[
ExpansionPanel(
isExpanded: panelExpansionState[0],
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
),
ExpansionPanel(
isExpanded: panelExpansionState[1],
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
),
ExpansionPanel(
isExpanded: panelExpansionState[2],
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'F' : 'E');
},
body: const SizedBox(height: 100.0),
),
],
);
}
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: isRadioList
? buildRadioExpansionPanelList()
: buildExpansionPanelList(setState),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() { isRadioList = !isRadioList; }),
),
),
);
},
),
);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
await tester.tap(find.byType(ExpandIcon).at(0));
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pumpAndSettle();
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
// ExpansionPanelList --> ExpansionPanelList.radio
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsNothing);
expect(find.text('F'), findsOneWidget);
// ExpansionPanelList.radio --> ExpansionPanelList
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
},
);
testWidgets('No duplicate global keys at layout/build time', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/13780
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
// Wrapping with LayoutBuilder or other widgets that augment
// layout/build order should not create duplicate keys
home: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
child: ExpansionPanelList.radio(
expansionCallback: (int index, bool isExpanded) {
if (!isExpanded) {
// setState invocation required to trigger
// _ExpansionPanelListState.didUpdateWidget,
// which causes duplicate keys to be
// generated in the regression
setState(() {});
}
},
children: <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
value: 1,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'F' : 'E');
},
body: const SizedBox(height: 100.0),
value: 2,
),
],
),
);
},
),
);
},
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
expect(find.text('E'), findsOneWidget);
expect(find.text('F'), findsNothing);
// Open a panel
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pumpAndSettle();
final List<bool> panelExpansionState = <bool>[false, false];
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Scaffold(
// Wrapping with LayoutBuilder or other widgets that augment
// layout/build order should not create duplicate keys
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
child: ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
// setState invocation required to trigger
// _ExpansionPanelListState.didUpdateWidget, which
// causes duplicate keys to be generated in the
// regression
setState(() {
panelExpansionState[index] = !isExpanded;
});
},
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
isExpanded: panelExpansionState[0],
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
isExpanded: panelExpansionState[1],
),
],
),
);
},
),
),
);
},
),
);
// initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
// open a panel
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pumpAndSettle();
});
testWidgets('Panel header has semantics, canTapOnHeader = false ', (WidgetTester tester) async {
const Key expandedKey = Key('expanded');
const Key collapsedKey = Key('collapsed');
const DefaultMaterialLocalizations localizations = DefaultMaterialLocalizations();
final SemanticsHandle handle = tester.ensureSemantics();
final List<ExpansionPanel> demoItems = <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('Expanded', key: expandedKey);
},
body: const SizedBox(height: 100.0),
isExpanded: true,
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('Collapsed', key: collapsedKey);
},
body: const SizedBox(height: 100.0),
),
];
final ExpansionPanelList expansionList = ExpansionPanelList(
children: demoItems,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionList,
),
),
);
// Check the semantics of [ExpandIcon] for expanded panel.
final Finder expandedIcon = find.descendant(
of: find.ancestor(
of: find.byKey(expandedKey),
matching: find.byType(Row),
),
matching: find.byType(ExpandIcon),
);
expect(tester.getSemantics(expandedIcon), matchesSemantics(
label: 'Collapse',
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
onTapHint: localizations.expandedIconTapHint,
));
// Check the semantics of the header widget for expanded panel.
final Finder expandedHeader = find.byKey(expandedKey);
expect(tester.getSemantics(expandedHeader), matchesSemantics(
label: 'Expanded',
));
// Check the semantics of [ExpandIcon] for collapsed panel.
final Finder collapsedIcon = find.descendant(
of: find.ancestor(
of: find.byKey(collapsedKey),
matching: find.byType(Row),
),
matching: find.byType(ExpandIcon),
);
expect(tester.getSemantics(collapsedIcon), matchesSemantics(
label: 'Expand',
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
onTapHint: localizations.collapsedIconTapHint,
));
// Check the semantics of the header widget for expanded panel.
final Finder collapsedHeader = find.byKey(collapsedKey);
expect(tester.getSemantics(collapsedHeader), matchesSemantics(
label: 'Collapsed',
));
handle.dispose();
});
testWidgets('Panel header has semantics, canTapOnHeader = true', (WidgetTester tester) async {
const Key expandedKey = Key('expanded');
const Key collapsedKey = Key('collapsed');
final SemanticsHandle handle = tester.ensureSemantics();
final List<ExpansionPanel> demoItems = <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('Expanded', key: expandedKey);
},
canTapOnHeader: true,
body: const SizedBox(height: 100.0),
isExpanded: true,
),
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('Collapsed', key: collapsedKey);
},
canTapOnHeader: true,
body: const SizedBox(height: 100.0),
),
];
final ExpansionPanelList expansionList = ExpansionPanelList(
children: demoItems,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionList,
),
),
);
expect(tester.getSemantics(find.byKey(expandedKey)), matchesSemantics(
label: 'Expanded',
isButton: true,
isFocusable: true,
hasEnabledState: true,
hasTapAction: true,
));
expect(tester.getSemantics(find.byKey(collapsedKey)), matchesSemantics(
label: 'Collapsed',
isButton: true,
isFocusable: true,
hasEnabledState: true,
hasTapAction: true,
));
handle.dispose();
});
testWidgets('Ensure canTapOnHeader is false by default', (WidgetTester tester) async {
final ExpansionPanel expansionPanel = ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) => const Text('Demo'),
body: const SizedBox(height: 100.0),
);
expect(expansionPanel.canTapOnHeader, isFalse);
});
testWidgets('Toggle ExpansionPanelRadio when tapping header and canTapOnHeader is true', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: firstPanelKey);
},
body: const SizedBox(height: 100.0),
value: 0,
canTapOnHeader: true,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: secondPanelKey);
},
body: const SizedBox(height: 100.0),
value: 1,
canTapOnHeader: true,
),
];
final ExpansionPanelList expansionListRadio = ExpansionPanelList.radio(
children: demoItemsRadio,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionListRadio,
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// Now the first panel is open
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// Now the second panel is open
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
});
testWidgets('Toggle ExpansionPanel when tapping header and canTapOnHeader is true', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
await tester.pumpWidget(
const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
firstPanelKey: firstPanelKey,
secondPanelKey: secondPanelKey,
canTapOnHeader: true,
),
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is open
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is open
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsNothing);
expect(find.text('D'), findsOneWidget);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
});
testWidgets('Do not toggle ExpansionPanel when tapping header and canTapOnHeader is false', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
await tester.pumpWidget(
const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
firstPanelKey: firstPanelKey,
secondPanelKey: secondPanelKey,
),
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
});
testWidgets('Do not toggle ExpansionPanelRadio when tapping header and canTapOnHeader is false', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
const Key secondPanelKey = Key('secondPanelKey');
final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: firstPanelKey);
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: secondPanelKey);
},
body: const SizedBox(height: 100.0),
value: 1,
),
];
final ExpansionPanelList expansionListRadio = ExpansionPanelList.radio(
children: demoItemsRadio,
);
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: expansionListRadio,
),
),
);
// Initializes with all panels closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The first panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
await tester.tap(find.byKey(secondPanelKey));
await tester.pumpAndSettle();
// The second panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
});
testWidgets('Correct default header padding', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
await tester.pumpWidget(
const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
firstPanelKey: firstPanelKey,
canTapOnHeader: true,
),
),
),
);
// The panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
// No padding applied to closed header
RenderBox box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
expect(box.size.height, equals(48.0)); // _kPanelHeaderCollapsedHeight
expect(box.size.width, equals(736.0));
// Now, expand the child panel.
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The panel is expanded
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
// Padding is added to expanded header
box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
expect(box.size.height, equals(80.0)); // _kPanelHeaderCollapsedHeight + 32.0 (double default padding)
expect(box.size.width, equals(736.0));
});
testWidgets('Correct custom header padding', (WidgetTester tester) async {
const Key firstPanelKey = Key('firstPanelKey');
await tester.pumpWidget(
const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
firstPanelKey: firstPanelKey,
canTapOnHeader: true,
expandedHeaderPadding: EdgeInsets.symmetric(vertical: 40.0),
),
),
),
);
// The panel is closed
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
// No padding applied to closed header
RenderBox box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
expect(box.size.height, equals(48.0)); // _kPanelHeaderCollapsedHeight
expect(box.size.width, equals(736.0));
// Now, expand the child panel.
await tester.tap(find.byKey(firstPanelKey));
await tester.pumpAndSettle();
// The panel is expanded
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
// Padding is added to expanded header
box = tester.renderObject(find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first);
expect(box.size.height, equals(128.0)); // _kPanelHeaderCollapsedHeight + 80.0 (double padding)
expect(box.size.width, equals(736.0));
});
testWidgets('ExpansionPanelList respects dividerColor', (WidgetTester tester) async {
const Color dividerColor = Colors.red;
await tester.pumpWidget(const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
dividerColor: dividerColor,
),
),
));
final DecoratedBox decoratedBox = tester.widget(find.byType(DecoratedBox).last);
final BoxDecoration decoration = decoratedBox.decoration as BoxDecoration;
// For the last DecoratedBox, we will have a Border.top with the provided dividerColor.
expect(decoration.border!.top.color, dividerColor);
});
testWidgets('ExpansionPanelList.radio respects DividerColor', (WidgetTester tester) async {
const Color dividerColor = Colors.red;
await tester.pumpWidget(MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList.radio(
dividerColor: dividerColor,
children: <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: const Key('firstKey'));
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: const Key('secondKey'));
},
body: const SizedBox(height: 100.0),
value: 1,
),
],
),
),
));
final DecoratedBox decoratedBox = tester.widget(find.byType(DecoratedBox).last);
final BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
// For the last DecoratedBox, we will have a Border.top with the provided dividerColor.
expect(boxDecoration.border!.top.color, dividerColor);
});
testWidgets('elevation is propagated properly to MergeableMaterial', (WidgetTester tester) async {
const double elevation = 8;
// Test for ExpansionPanelList.
await tester.pumpWidget(const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
elevation: elevation,
),
),
));
expect(tester.widget<MergeableMaterial>(find.byType(MergeableMaterial)).elevation, elevation);
// Test for ExpansionPanelList.radio.
await tester.pumpWidget(MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList.radio(
elevation: elevation,
children: <ExpansionPanelRadio>[
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A', key: const Key('firstKey'));
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C', key: const Key('secondKey'));
},
body: const SizedBox(height: 100.0),
value: 1,
),
],
),
),
));
expect(tester.widget<MergeableMaterial>(find.byType(MergeableMaterial)).elevation, elevation);
});
testWidgets('Using a value non defined value throws assertion error', (WidgetTester tester) async {
// It should throw an AssertionError since, 19 is not defined in kElevationToShadow.
await tester.pumpWidget(const MaterialApp(
home: SingleChildScrollView(
child: SimpleExpansionPanelListTestWidget(
elevation: 19,
),
),
));
final dynamic exception = tester.takeException();
expect(exception, isAssertionError);
expect((exception as AssertionError).toString(), contains(
'Invalid value for elevation. See the kElevationToShadow constant for'
' possible elevation values.',
));
});
testWidgets('ExpansionPanel.panelColor test', (WidgetTester tester) async {
const Color firstPanelColor = Colors.red;
const Color secondPanelColor = Colors.brown;
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {},
children: <ExpansionPanel>[
ExpansionPanel(
backgroundColor: firstPanelColor,
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('A');
},
body: const SizedBox(height: 100.0),
),
ExpansionPanel(
backgroundColor: secondPanelColor,
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('B');
},
body: const SizedBox(height: 100.0),
),
],
),
),
),
);
final MergeableMaterial mergeableMaterial = tester.widget(find.byType(MergeableMaterial));
expect((mergeableMaterial.children.first as MaterialSlice).color, firstPanelColor);
expect((mergeableMaterial.children.last as MaterialSlice).color, secondPanelColor);
});
testWidgets('ExpansionPanelRadio.backgroundColor test', (WidgetTester tester) async {
const Color firstPanelColor = Colors.red;
const Color secondPanelColor = Colors.brown;
await tester.pumpWidget(MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList.radio(
children: <ExpansionPanelRadio>[
ExpansionPanelRadio(
backgroundColor: firstPanelColor,
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('A');
},
body: const SizedBox(height: 100.0),
value: 0,
),
ExpansionPanelRadio(
backgroundColor: secondPanelColor,
headerBuilder: (BuildContext context, bool isExpanded) {
return const Text('B');
},
body: const SizedBox(height: 100.0),
value: 1,
),
],
),
),
));
final MergeableMaterial mergeableMaterial = tester.widget(find.byType(MergeableMaterial));
expect((mergeableMaterial.children.first as MaterialSlice).color, firstPanelColor);
expect((mergeableMaterial.children.last as MaterialSlice).color, secondPanelColor);
});
}