flutter_flutter/packages/flutter/test/material/expansion_panel_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

2091 lines
71 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';
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, isTrue);
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('Material2 - 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(
theme: ThemeData(useMaterial3: false),
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,
hasFocusAction: 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('Material3 - 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',
children: <Matcher>[
matchesSemantics(
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
hasFocusAction: 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 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, 48.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(1)),
const Rect.fromLTWH(0.0, 97.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(2)),
const Rect.fromLTWH(0.0, 146.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, 48.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(1)),
const Rect.fromLTWH(0.0, 97.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(2)),
const Rect.fromLTWH(0.0, 146.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, 48.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(1)),
const Rect.fromLTWH(0.0, 97.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(2)),
const Rect.fromLTWH(0.0, 146.0, 800.0, 0.0),
);
await tester.pump(kSizeAnimationDuration ~/ 2);
expect(
tester.getRect(find.byType(AnimatedSize).at(0)),
const Rect.fromLTWH(0.0, 48.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 + 24.0),
); // 16.0 material gap, plus 12.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 + 48.0, 800.0, 0.0),
); // the 16.0 comes from the MaterialGap being introduced, the 48.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, 48.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, 48.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, 48.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, 48.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(1)),
const Rect.fromLTWH(0.0, 48.0 + 1.0 + 48.0, 800.0, 0.0),
);
expect(
tester.getRect(find.byType(AnimatedSize).at(2)),
const Rect.fromLTWH(0.0, 48.0 + 1.0 + 48.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 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 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 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 callbackHistory = <Map<String, dynamic>>[];
final 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(true));
// 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(false));
});
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 callbackHistory = <Map<String, dynamic>>[];
Map<String, dynamic> callbackResults;
final 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(true));
// 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(1));
expect(callbackResults['isExpanded'], equals(false));
// Callback is invoked the second time with correct arguments
callbackResults = callbackHistory[callbackHistory.length - 1];
expect(callbackResults['index'], equals(2));
expect(callbackResults['isExpanded'], equals(true));
});
testWidgets(
'ExpansionPanelList.radio callback displays true or false based on the visibility of a list item',
(WidgetTester tester) async {
late int lastExpanded;
var topElementExpanded = false;
var bottomElementExpanded = false;
final List<ExpansionPanel> demoItemsRadio = <ExpansionPanelRadio>[
// topElement
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'B' : 'A');
},
body: const SizedBox(height: 100.0),
value: 0,
),
// bottomElement
ExpansionPanelRadio(
headerBuilder: (BuildContext context, bool isExpanded) {
return Text(isExpanded ? 'D' : 'C');
},
body: const SizedBox(height: 100.0),
value: 1,
),
];
final expansionListRadio = ExpansionPanelList.radio(
children: demoItemsRadio,
expansionCallback: (int index, bool isExpanded) {
lastExpanded = index;
if (index == 0) {
topElementExpanded = isExpanded;
bottomElementExpanded = false;
} else {
topElementExpanded = false;
bottomElementExpanded = isExpanded;
}
},
);
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.byType(ExpandIcon).at(0));
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(lastExpanded, 0);
expect(topElementExpanded, true);
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pump(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
// Open the other panel and ensure the first is now closed.
expect(lastExpanded, 1);
expect(bottomElementExpanded, true);
expect(topElementExpanded, false);
expect(find.text('D'), findsOneWidget);
expect(find.text('A'), findsOneWidget);
await tester.tap(find.byType(ExpandIcon).at(1));
await tester.pump(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
// Close the item that was expanded should now be false.
expect(lastExpanded, 1);
expect(bottomElementExpanded, false);
// All panels should be closed.
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('C'), findsOneWidget);
expect(find.text('D'), findsNothing);
},
);
testWidgets('didUpdateWidget accounts for toggling between ExpansionPanelList '
'and ExpansionPaneList.radio', (WidgetTester tester) async {
var isRadioList = false;
final 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 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('Material2 - Panel header has semantics, canTapOnHeader = false', (
WidgetTester tester,
) async {
const expandedKey = Key('expanded');
const collapsedKey = Key('collapsed');
const localizations = DefaultMaterialLocalizations();
final SemanticsHandle handle = tester.ensureSemantics();
final 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 expansionList = ExpansionPanelList(children: demoItems);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
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,
hasFocusAction: 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,
hasFocusAction: 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('Material3 - Panel header has semantics, canTapOnHeader = false', (
WidgetTester tester,
) async {
const expandedKey = Key('expanded');
const collapsedKey = Key('collapsed');
const localizations = DefaultMaterialLocalizations();
final SemanticsHandle handle = tester.ensureSemantics();
final 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 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',
onTapHint: localizations.expandedIconTapHint,
children: <Matcher>[
matchesSemantics(
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
],
),
);
// 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',
onTapHint: localizations.collapsedIconTapHint,
children: <Matcher>[
matchesSemantics(
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
],
),
);
// 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 expandedKey = Key('expanded');
const collapsedKey = Key('collapsed');
final SemanticsHandle handle = tester.ensureSemantics();
final 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 expansionList = ExpansionPanelList(children: demoItems);
await tester.pumpWidget(MaterialApp(home: SingleChildScrollView(child: expansionList)));
expect(
tester.getSemantics(find.byKey(expandedKey)),
matchesSemantics(
label: 'Expanded',
isButton: true,
isEnabled: true,
isFocusable: true,
hasEnabledState: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
tester.getSemantics(find.byKey(collapsedKey)),
matchesSemantics(
label: 'Collapsed',
isButton: true,
isFocusable: true,
isEnabled: true,
hasEnabledState: true,
hasTapAction: true,
hasFocusAction: true,
),
);
handle.dispose();
});
testWidgets('Ensure canTapOnHeader is false by default', (WidgetTester tester) async {
final 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 firstPanelKey = Key('firstPanelKey');
const 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 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 firstPanelKey = Key('firstPanelKey');
const 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 firstPanelKey = Key('firstPanelKey');
const 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 firstPanelKey = Key('firstPanelKey');
const 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 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 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(744.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 + 24.0 (double default padding)
expect(box.size.width, equals(744.0));
});
// Regression test for https://github.com/flutter/flutter/issues/5848.
testWidgets('The AnimatedContainer and IconButton have the same height of 48px', (
WidgetTester tester,
) async {
const 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.
final RenderBox boxOfContainer = tester.renderObject(
find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first,
);
final RenderBox boxOfIconButton = tester.renderObject(find.byType(IconButton).first);
expect(boxOfContainer.size.height, equals(boxOfIconButton.size.height));
expect(
boxOfContainer.size.height,
equals(48.0),
); // Header should have 48px height according to Material 2 Design spec.
});
testWidgets("The AnimatedContainer's height is at least kMinInteractiveDimension", (
WidgetTester tester,
) async {
const 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
final RenderBox box = tester.renderObject(
find.ancestor(of: find.byKey(firstPanelKey), matching: find.byType(AnimatedContainer)).first,
);
expect(box.size.height, greaterThanOrEqualTo(kMinInteractiveDimension));
});
testWidgets('Correct custom header padding', (WidgetTester tester) async {
const 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(744.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(744.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 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 = 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('ExpansionPanelList respects expandIconColor', (WidgetTester tester) async {
const Color expandIconColor = Colors.blue;
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
expandIconColor: expandIconColor,
children: <ExpansionPanel>[
ExpansionPanel(
canTapOnHeader: true,
body: const SizedBox.shrink(),
headerBuilder: (BuildContext context, bool isExpanded) {
return const SizedBox.shrink();
},
),
],
),
),
),
);
final ExpandIcon expandIcon = tester.widget(find.byType(ExpandIcon));
expect(expandIcon.color, expandIconColor);
});
testWidgets('ExpansionPanelList.radio respects expandIconColor', (WidgetTester tester) async {
const Color expandIconColor = Colors.blue;
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList.radio(
expandIconColor: expandIconColor,
children: <ExpansionPanelRadio>[
ExpansionPanelRadio(
canTapOnHeader: true,
body: const SizedBox.shrink(),
headerBuilder: (BuildContext context, bool isExpanded) {
return const SizedBox.shrink();
},
value: true,
),
],
),
),
),
);
final ExpandIcon expandIcon = tester.widget(find.byType(ExpandIcon));
expect(expandIcon.color, expandIconColor);
});
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);
});
testWidgets('ExpansionPanelList.materialGapSize defaults to 16.0', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
children: <ExpansionPanel>[
ExpansionPanel(
canTapOnHeader: true,
body: const SizedBox.shrink(),
headerBuilder: (BuildContext context, bool isExpanded) {
return const SizedBox.shrink();
},
),
],
),
),
),
);
final ExpansionPanelList expansionPanelList = tester.widget(find.byType(ExpansionPanelList));
expect(expansionPanelList.materialGapSize, 16);
});
testWidgets('ExpansionPanelList respects materialGapSize', (WidgetTester tester) async {
Widget buildWidgetForTest({double materialGapSize = 16}) {
return MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
materialGapSize: materialGapSize,
children: <ExpansionPanel>[
ExpansionPanel(
isExpanded: true,
canTapOnHeader: true,
body: const SizedBox.shrink(),
headerBuilder: (BuildContext context, bool isExpanded) {
return const SizedBox.shrink();
},
),
ExpansionPanel(
canTapOnHeader: true,
body: const SizedBox.shrink(),
headerBuilder: (BuildContext context, bool isExpanded) {
return const SizedBox.shrink();
},
),
],
),
),
);
}
await tester.pumpWidget(buildWidgetForTest(materialGapSize: 0));
await tester.pumpAndSettle();
final MergeableMaterial mergeableMaterial = tester.widget(find.byType(MergeableMaterial));
expect(mergeableMaterial.children.length, 3);
expect(mergeableMaterial.children.whereType<MaterialGap>().length, 1);
expect(mergeableMaterial.children.whereType<MaterialSlice>().length, 2);
for (final MergeableMaterialItem e in mergeableMaterial.children) {
if (e is MaterialGap) {
expect(e.size, 0);
}
}
await tester.pumpWidget(buildWidgetForTest(materialGapSize: 20));
await tester.pumpAndSettle();
final MergeableMaterial mergeableMaterial2 = tester.widget(find.byType(MergeableMaterial));
expect(mergeableMaterial2.children.length, 3);
expect(mergeableMaterial2.children.whereType<MaterialGap>().length, 1);
expect(mergeableMaterial2.children.whereType<MaterialSlice>().length, 2);
for (final MergeableMaterialItem e in mergeableMaterial2.children) {
if (e is MaterialGap) {
expect(e.size, 20);
}
}
await tester.pumpWidget(buildWidgetForTest());
await tester.pumpAndSettle();
final MergeableMaterial mergeableMaterial3 = tester.widget(find.byType(MergeableMaterial));
expect(mergeableMaterial3.children.length, 3);
expect(mergeableMaterial3.children.whereType<MaterialGap>().length, 1);
expect(mergeableMaterial3.children.whereType<MaterialSlice>().length, 2);
for (final MergeableMaterialItem e in mergeableMaterial3.children) {
if (e is MaterialGap) {
expect(e.size, 16);
}
}
});
testWidgets(
'Ensure IconButton splashColor and highlightColor are correctly set when canTapOnHeader is false',
(WidgetTester tester) async {
const Color expectedSplashColor = Colors.green;
const Color expectedHighlightColor = Colors.yellow;
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
children: <ExpansionPanel>[
ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return const ListTile(title: Text('Panel 1'));
},
body: const ListTile(title: Text('Content for Panel 1')),
splashColor: expectedSplashColor,
highlightColor: expectedHighlightColor,
),
],
),
),
),
);
await tester.tap(find.text('Panel 1'));
await tester.pumpAndSettle();
final IconButton iconButton = tester.widget(find.byType(IconButton).first);
expect(iconButton.splashColor, expectedSplashColor);
expect(iconButton.highlightColor, expectedHighlightColor);
},
);
testWidgets(
'Ensure InkWell splashColor and highlightColor are correctly set when canTapOnHeader is true',
(WidgetTester tester) async {
const Color expectedSplashColor = Colors.green;
const Color expectedHighlightColor = Colors.yellow;
await tester.pumpWidget(
MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
children: <ExpansionPanel>[
ExpansionPanel(
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.centerLeft,
child: const Text('Panel 1'),
);
},
body: const ListTile(title: Text('Content for Panel 1')),
splashColor: expectedSplashColor,
highlightColor: expectedHighlightColor,
),
],
),
),
),
);
await tester.pumpAndSettle();
final Finder inkWellFinder = find.descendant(
of: find.byType(ExpansionPanelList),
matching: find.byWidgetPredicate(
(Widget widget) => widget is InkWell && widget.onTap != null,
),
);
final InkWell inkWell = tester.widget<InkWell>(inkWellFinder.first);
expect(inkWell.splashColor, expectedSplashColor);
expect(inkWell.highlightColor, expectedHighlightColor);
},
);
testWidgets('ExpandIcon ignores pointer/tap events when canTapOnHeader is true', (
WidgetTester tester,
) async {
Widget buildWidget({bool canTapOnHeader = false}) {
return MaterialApp(
home: SingleChildScrollView(
child: ExpansionPanelList(
children: <ExpansionPanel>[
ExpansionPanel(
canTapOnHeader: canTapOnHeader,
headerBuilder: (BuildContext context, bool isExpanded) {
return const ListTile(title: Text('Panel'));
},
body: const ListTile(title: Text('Content for Panel')),
),
],
),
),
);
}
await tester.pumpWidget(buildWidget());
final Finder ignorePointerFinder = find
.descendant(of: find.byType(ExpansionPanelList), matching: find.byType(IgnorePointer))
.first;
final IgnorePointer ignorePointerFalse = tester.widget(ignorePointerFinder);
expect(ignorePointerFalse.ignoring, isFalse);
await tester.pumpWidget(buildWidget(canTapOnHeader: true));
await tester.pumpAndSettle();
final IgnorePointer ignorePointerTrue = tester.widget(ignorePointerFinder);
expect(ignorePointerTrue.ignoring, isTrue);
});
}