| // 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. |
| |
| // This file is run as part of a reduced test set in CI on Mac and Windows |
| // machines. |
| @Tags(<String>['reduced-test-set']) |
| library; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| class TestIcon extends StatefulWidget { |
| const TestIcon({super.key}); |
| |
| @override |
| TestIconState createState() => TestIconState(); |
| } |
| |
| class TestIconState extends State<TestIcon> { |
| late IconThemeData iconTheme; |
| |
| @override |
| Widget build(BuildContext context) { |
| iconTheme = IconTheme.of(context); |
| return const Icon(Icons.expand_more); |
| } |
| } |
| |
| class TestText extends StatefulWidget { |
| const TestText(this.text, {super.key}); |
| |
| final String text; |
| |
| @override |
| TestTextState createState() => TestTextState(); |
| } |
| |
| class TestTextState extends State<TestText> { |
| late TextStyle textStyle; |
| |
| @override |
| Widget build(BuildContext context) { |
| textStyle = DefaultTextStyle.of(context).style; |
| return Text(widget.text); |
| } |
| } |
| |
| void main() { |
| const dividerColor = Color(0x1f333333); |
| const Color foregroundColor = Colors.blueAccent; |
| const Color unselectedWidgetColor = Colors.black54; |
| const Color headerColor = Colors.black45; |
| |
| Material getMaterial(WidgetTester tester) { |
| return tester.widget<Material>( |
| find.descendant(of: find.byType(ExpansionTile), matching: find.byType(Material)), |
| ); |
| } |
| |
| testWidgets( |
| 'ExpansionTile initial state', |
| (WidgetTester tester) async { |
| final Key topKey = UniqueKey(); |
| final Key tileKey = UniqueKey(); |
| const Key expandedKey = PageStorageKey<String>('expanded'); |
| const Key collapsedKey = PageStorageKey<String>('collapsed'); |
| const Key defaultKey = PageStorageKey<String>('default'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(dividerColor: dividerColor), |
| home: Material( |
| child: SingleChildScrollView( |
| child: Column( |
| children: <Widget>[ |
| ListTile(title: const Text('Top'), key: topKey), |
| ExpansionTile( |
| key: expandedKey, |
| initiallyExpanded: true, |
| title: const Text('Expanded'), |
| backgroundColor: Colors.red, |
| children: <Widget>[ListTile(key: tileKey, title: const Text('0'))], |
| ), |
| ExpansionTile( |
| key: collapsedKey, |
| title: const Text('Collapsed'), |
| children: <Widget>[ListTile(key: tileKey, title: const Text('0'))], |
| ), |
| const ExpansionTile( |
| key: defaultKey, |
| title: Text('Default'), |
| children: <Widget>[ListTile(title: Text('0'))], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| double getHeight(Key key) => tester.getSize(find.byKey(key)).height; |
| DecoratedBox getDecoratedBox(Key key) => tester.firstWidget( |
| find.descendant(of: find.byKey(key), matching: find.byType(DecoratedBox)), |
| ); |
| |
| expect(getHeight(topKey), getHeight(expandedKey) - getHeight(tileKey) - 2.0); |
| expect(getHeight(topKey), getHeight(collapsedKey) - 2.0); |
| expect(getHeight(topKey), getHeight(defaultKey) - 2.0); |
| |
| var expandedContainerDecoration = getDecoratedBox(expandedKey).decoration as ShapeDecoration; |
| expect(expandedContainerDecoration.color, Colors.red); |
| expect((expandedContainerDecoration.shape as Border).top.color, dividerColor); |
| expect((expandedContainerDecoration.shape as Border).bottom.color, dividerColor); |
| |
| var collapsedContainerDecoration = |
| getDecoratedBox(collapsedKey).decoration as ShapeDecoration; |
| expect(collapsedContainerDecoration.color, Colors.transparent); |
| expect((collapsedContainerDecoration.shape as Border).top.color, Colors.transparent); |
| expect((collapsedContainerDecoration.shape as Border).bottom.color, Colors.transparent); |
| |
| await tester.tap(find.text('Expanded')); |
| await tester.tap(find.text('Collapsed')); |
| await tester.tap(find.text('Default')); |
| |
| await tester.pump(); |
| |
| // Pump to the middle of the animation for expansion. |
| await tester.pump(const Duration(milliseconds: 100)); |
| final collapsingContainerDecoration = |
| getDecoratedBox(collapsedKey).decoration as ShapeDecoration; |
| expect(collapsingContainerDecoration.color, Colors.transparent); |
| expect( |
| (collapsingContainerDecoration.shape as Border).top.color, |
| isSameColorAs(const Color(0x15222222)), |
| ); |
| expect( |
| (collapsingContainerDecoration.shape as Border).bottom.color, |
| isSameColorAs(const Color(0x15222222)), |
| ); |
| |
| // Pump all the way to the end now. |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(getHeight(topKey), getHeight(expandedKey) - 2.0); |
| expect(getHeight(topKey), getHeight(collapsedKey) - getHeight(tileKey) - 2.0); |
| expect(getHeight(topKey), getHeight(defaultKey) - getHeight(tileKey) - 2.0); |
| |
| // Expanded should be collapsed now. |
| expandedContainerDecoration = getDecoratedBox(expandedKey).decoration as ShapeDecoration; |
| expect(expandedContainerDecoration.color, Colors.transparent); |
| expect((expandedContainerDecoration.shape as Border).top.color, Colors.transparent); |
| expect((expandedContainerDecoration.shape as Border).bottom.color, Colors.transparent); |
| |
| // Collapsed should be expanded now. |
| collapsedContainerDecoration = getDecoratedBox(collapsedKey).decoration as ShapeDecoration; |
| expect(collapsedContainerDecoration.color, Colors.transparent); |
| expect((collapsedContainerDecoration.shape as Border).top.color, dividerColor); |
| expect((collapsedContainerDecoration.shape as Border).bottom.color, dividerColor); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.iOS, |
| TargetPlatform.macOS, |
| }), |
| ); |
| |
| testWidgets( |
| 'ExpansionTile Theme dependencies', |
| (WidgetTester tester) async { |
| final Key expandedTitleKey = UniqueKey(); |
| final Key collapsedTitleKey = UniqueKey(); |
| final Key expandedIconKey = UniqueKey(); |
| final Key collapsedIconKey = UniqueKey(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData( |
| useMaterial3: false, |
| colorScheme: ColorScheme.fromSwatch().copyWith(primary: foregroundColor), |
| unselectedWidgetColor: unselectedWidgetColor, |
| textTheme: const TextTheme(titleMedium: TextStyle(color: headerColor)), |
| ), |
| home: Material( |
| child: SingleChildScrollView( |
| child: Column( |
| children: <Widget>[ |
| const ListTile(title: Text('Top')), |
| ExpansionTile( |
| initiallyExpanded: true, |
| title: TestText('Expanded', key: expandedTitleKey), |
| backgroundColor: Colors.red, |
| trailing: TestIcon(key: expandedIconKey), |
| children: const <Widget>[ListTile(title: Text('0'))], |
| ), |
| ExpansionTile( |
| title: TestText('Collapsed', key: collapsedTitleKey), |
| trailing: TestIcon(key: collapsedIconKey), |
| children: const <Widget>[ListTile(title: Text('0'))], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| Color iconColor(Key key) => tester.state<TestIconState>(find.byKey(key)).iconTheme.color!; |
| Color textColor(Key key) => tester.state<TestTextState>(find.byKey(key)).textStyle.color!; |
| |
| expect(textColor(expandedTitleKey), foregroundColor); |
| expect(textColor(collapsedTitleKey), headerColor); |
| expect(iconColor(expandedIconKey), foregroundColor); |
| expect(iconColor(collapsedIconKey), unselectedWidgetColor); |
| |
| // Tap both tiles to change their state: collapse and extend respectively |
| await tester.tap(find.text('Expanded')); |
| await tester.tap(find.text('Collapsed')); |
| await tester.pump(); |
| await tester.pump(const Duration(seconds: 1)); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| expect(textColor(expandedTitleKey), headerColor); |
| expect(textColor(collapsedTitleKey), foregroundColor); |
| expect(iconColor(expandedIconKey), unselectedWidgetColor); |
| expect(iconColor(collapsedIconKey), foregroundColor); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.iOS, |
| TargetPlatform.macOS, |
| }), |
| ); |
| |
| testWidgets('ExpansionTile subtitle', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Scaffold( |
| body: ExpansionTile( |
| title: Text('Title'), |
| subtitle: Text('Subtitle'), |
| children: <Widget>[ListTile(title: Text('0'))], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Subtitle'), findsOneWidget); |
| }); |
| |
| testWidgets('ExpansionTile maintainState', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.iOS, dividerColor: dividerColor), |
| home: const Material( |
| child: SingleChildScrollView( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile( |
| title: Text('Tile 1'), |
| maintainState: true, |
| children: <Widget>[Text('Maintaining State')], |
| ), |
| ExpansionTile(title: Text('Title 2'), children: <Widget>[Text('Discarding State')]), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // This text should be offstage while ExpansionTile collapsed |
| expect(find.text('Maintaining State', skipOffstage: false), findsOneWidget); |
| expect(find.text('Maintaining State'), findsNothing); |
| // This text shouldn't be there while ExpansionTile collapsed |
| expect(find.text('Discarding State'), findsNothing); |
| }); |
| |
| testWidgets('ExpansionTile padding test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: ExpansionTile( |
| title: Text('Hello'), |
| tilePadding: EdgeInsets.fromLTRB(8, 12, 4, 10), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final Rect titleRect = tester.getRect(find.text('Hello')); |
| final Rect trailingRect = tester.getRect(find.byIcon(Icons.expand_more)); |
| final Rect listTileRect = tester.getRect(find.byType(ListTile)); |
| final tallerWidget = titleRect.height > trailingRect.height ? titleRect : trailingRect; |
| |
| // Check the positions of title and trailing Widgets, after padding is applied. |
| expect(listTileRect.left, titleRect.left - 8); |
| expect(listTileRect.right, trailingRect.right + 4); |
| |
| // Calculate the remaining height of ListTile from the default height. |
| final double remainingHeight = 56 - tallerWidget.height; |
| expect(listTileRect.top, tallerWidget.top - remainingHeight / 2 - 12); |
| expect(listTileRect.bottom, tallerWidget.bottom + remainingHeight / 2 + 10); |
| }); |
| |
| testWidgets('ExpansionTile expandedAlignment test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: ExpansionTile( |
| title: Text('title'), |
| expandedAlignment: Alignment.centerLeft, |
| children: <Widget>[ |
| SizedBox(height: 100, width: 100), |
| SizedBox(height: 100, width: 80), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| final Rect columnRect = tester.getRect(find.byType(Column).last); |
| |
| // The expandedAlignment is used to define the alignment of the Column widget in |
| // expanded tile, not the alignment of the children inside the Column. |
| expect(columnRect.left, 0.0); |
| // The width of the Column is the width of the largest child. The largest width |
| // being 100.0, the offset of the right edge of Column from X-axis should be 100.0. |
| expect(columnRect.right, 100.0); |
| }); |
| |
| testWidgets('ExpansionTile expandedCrossAxisAlignment test', (WidgetTester tester) async { |
| const child0Key = Key('child0'); |
| const child1Key = Key('child1'); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: ExpansionTile( |
| title: Text('title'), |
| // Set the column's alignment to Alignment.centerRight to test CrossAxisAlignment |
| // of children widgets. This helps distinguish the effect of expandedAlignment |
| // and expandedCrossAxisAlignment later in the test. |
| expandedAlignment: Alignment.centerRight, |
| expandedCrossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| SizedBox(height: 100, width: 100, key: child0Key), |
| SizedBox(height: 100, width: 80, key: child1Key), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| final Rect columnRect = tester.getRect(find.byType(Column).last); |
| final Rect child0Rect = tester.getRect(find.byKey(child0Key)); |
| final Rect child1Rect = tester.getRect(find.byKey(child1Key)); |
| |
| // Since expandedAlignment is set to Alignment.centerRight, the column of children |
| // should be aligned to the center right of the expanded tile. This provides confirmation |
| // that the expandedCrossAxisAlignment.start is 700.0, where columnRect.left is. |
| expect(columnRect.right, 800.0); |
| // The width of the Column is the width of the largest child. The largest width |
| // being 100.0, the offset of the left edge of Column from X-axis should be 700.0. |
| expect(columnRect.left, 700.0); |
| |
| // Considering the value of expandedCrossAxisAlignment is CrossAxisAlignment.start, |
| // the offset of the left edge of both the children from X-axis should be 700.0. |
| expect(child0Rect.left, 700.0); |
| expect(child1Rect.left, 700.0); |
| }); |
| |
| testWidgets('CrossAxisAlignment.baseline is not allowed', (WidgetTester tester) async { |
| expect( |
| () { |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| initiallyExpanded: true, |
| title: const Text('title'), |
| expandedCrossAxisAlignment: CrossAxisAlignment.baseline, |
| ), |
| ), |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| '.toString()', |
| contains( |
| 'CrossAxisAlignment.baseline is not supported since the expanded' |
| ' children are aligned in a column, not a row. Try to use another constant.', |
| ), |
| ), |
| ), |
| ); |
| }); |
| |
| testWidgets('expandedCrossAxisAlignment and expandedAlignment default values', ( |
| WidgetTester tester, |
| ) async { |
| const child1Key = Key('child1'); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: ExpansionTile( |
| title: Text('title'), |
| children: <Widget>[ |
| SizedBox(height: 100, width: 100), |
| SizedBox(height: 100, width: 80, key: child1Key), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| final Rect columnRect = tester.getRect(find.byType(Column).last); |
| final Rect child1Rect = tester.getRect(find.byKey(child1Key)); |
| |
| // The default viewport size is Size(800, 600). |
| // By default the value of extendedAlignment is Alignment.center, hence the offset |
| // of left and right edges from x axis should be equal. |
| expect(columnRect.left, 800 - columnRect.right); |
| |
| // By default the value of extendedCrossAxisAlignment is CrossAxisAlignment.center, hence |
| // the offset of left and right edges from Column should be equal. |
| expect(child1Rect.left - columnRect.left, columnRect.right - child1Rect.right); |
| }); |
| |
| testWidgets('childrenPadding default value', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: ExpansionTile( |
| title: Text('title'), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| final Rect columnRect = tester.getRect(find.byType(Column).last); |
| final Rect paddingRect = tester.getRect(find.byType(Padding).last); |
| |
| // By default, the value of childrenPadding is EdgeInsets.zero, hence offset |
| // of all the edges from x-axis and y-axis should be equal for Padding and Column. |
| expect(columnRect.top, paddingRect.top); |
| expect(columnRect.left, paddingRect.left); |
| expect(columnRect.right, paddingRect.right); |
| expect(columnRect.bottom, paddingRect.bottom); |
| }); |
| |
| testWidgets('ExpansionTile childrenPadding test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: ExpansionTile( |
| title: Text('title'), |
| childrenPadding: EdgeInsets.fromLTRB(10, 8, 12, 4), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| final Rect columnRect = tester.getRect(find.byType(Column).last); |
| final Rect paddingRect = tester.getRect(find.byType(Padding).last); |
| |
| // Check the offset of all the edges from x-axis and y-axis after childrenPadding |
| // is applied. |
| expect(columnRect.left, paddingRect.left + 10); |
| expect(columnRect.top, paddingRect.top + 8); |
| expect(columnRect.right, paddingRect.right - 12); |
| expect(columnRect.bottom, paddingRect.bottom - 4); |
| }); |
| |
| testWidgets('ExpansionTile.collapsedBackgroundColor', (WidgetTester tester) async { |
| const expansionTileKey = Key('expansionTileKey'); |
| const Color backgroundColor = Colors.red; |
| const Color collapsedBackgroundColor = Colors.brown; |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| key: expansionTileKey, |
| title: Text('Title'), |
| backgroundColor: backgroundColor, |
| collapsedBackgroundColor: collapsedBackgroundColor, |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ); |
| |
| var shapeDecoration = |
| tester |
| .firstWidget<DecoratedBox>( |
| find.descendant( |
| of: find.byKey(expansionTileKey), |
| matching: find.byType(DecoratedBox), |
| ), |
| ) |
| .decoration |
| as ShapeDecoration; |
| |
| expect(shapeDecoration.color, collapsedBackgroundColor); |
| |
| await tester.tap(find.text('Title')); |
| await tester.pumpAndSettle(); |
| |
| shapeDecoration = |
| tester |
| .firstWidget<DecoratedBox>( |
| find.descendant( |
| of: find.byKey(expansionTileKey), |
| matching: find.byType(DecoratedBox), |
| ), |
| ) |
| .decoration |
| as ShapeDecoration; |
| |
| expect(shapeDecoration.color, backgroundColor); |
| }); |
| |
| testWidgets('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { |
| final theme = ThemeData(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: const Material( |
| child: ExpansionTile( |
| title: TestText('title'), |
| trailing: TestIcon(), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ); |
| |
| Color getIconColor() => tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!; |
| Color getTextColor() => tester.state<TestTextState>(find.byType(TestText)).textStyle.color!; |
| |
| expect(getIconColor(), theme.colorScheme.onSurfaceVariant); |
| expect(getTextColor(), theme.colorScheme.onSurface); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| expect(getIconColor(), theme.colorScheme.primary); |
| expect(getTextColor(), theme.colorScheme.onSurface); |
| }); |
| |
| testWidgets('ExpansionTile iconColor, textColor', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/pull/78281 |
| |
| const iconColor = Color(0xff00ff00); |
| const collapsedIconColor = Color(0xff0000ff); |
| const textColor = Color(0xff00ffff); |
| const collapsedTextColor = Color(0xffff00ff); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| iconColor: iconColor, |
| collapsedIconColor: collapsedIconColor, |
| textColor: textColor, |
| collapsedTextColor: collapsedTextColor, |
| title: TestText('title'), |
| trailing: TestIcon(), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ); |
| |
| Color getIconColor() => tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!; |
| Color getTextColor() => tester.state<TestTextState>(find.byType(TestText)).textStyle.color!; |
| |
| expect(getIconColor(), collapsedIconColor); |
| expect(getTextColor(), collapsedTextColor); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| expect(getIconColor(), iconColor); |
| expect(getTextColor(), textColor); |
| }); |
| |
| testWidgets('ExpansionTile Border', (WidgetTester tester) async { |
| const Key expansionTileKey = PageStorageKey<String>('expansionTile'); |
| |
| const collapsedShape = Border( |
| top: BorderSide(color: Colors.blue), |
| bottom: BorderSide(color: Colors.green), |
| ); |
| final shape = Border.all(color: Colors.red); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| key: expansionTileKey, |
| title: const Text('ExpansionTile'), |
| collapsedShape: collapsedShape, |
| shape: shape, |
| children: const <Widget>[ListTile(title: Text('0'))], |
| ), |
| ), |
| ), |
| ); |
| |
| // When a custom shape is provided, ExpansionTile will use the |
| // Material widget to draw the shape and background color |
| // instead of a Container. |
| Material material = getMaterial(tester); |
| // ExpansionTile should be collapsed initially. |
| expect(material.shape, collapsedShape); |
| expect(material.clipBehavior, Clip.antiAlias); |
| |
| await tester.tap(find.text('ExpansionTile')); |
| await tester.pumpAndSettle(); |
| |
| // ExpansionTile should be Expanded now. |
| material = getMaterial(tester); |
| expect(material.shape, shape); |
| expect(material.clipBehavior, Clip.antiAlias); |
| }); |
| |
| testWidgets('ExpansionTile platform controlAffinity test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material(child: ExpansionTile(title: Text('Title'))), |
| ), |
| ); |
| |
| final ListTile listTile = tester.widget(find.byType(ListTile)); |
| expect(listTile.leading, isNull); |
| expect(listTile.trailing.runtimeType, RotationTransition); |
| }); |
| |
| testWidgets('ExpansionTile trailing controlAffinity test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| title: Text('Title'), |
| controlAffinity: ListTileControlAffinity.trailing, |
| ), |
| ), |
| ), |
| ); |
| |
| final ListTile listTile = tester.widget(find.byType(ListTile)); |
| expect(listTile.leading, isNull); |
| expect(listTile.trailing.runtimeType, RotationTransition); |
| }); |
| |
| testWidgets('ExpansionTile leading controlAffinity test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| title: Text('Title'), |
| controlAffinity: ListTileControlAffinity.leading, |
| ), |
| ), |
| ), |
| ); |
| |
| final ListTile listTile = tester.widget(find.byType(ListTile)); |
| expect(listTile.leading.runtimeType, RotationTransition); |
| expect(listTile.trailing, isNull); |
| }); |
| |
| testWidgets('ExpansionTile override rotating icon test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| title: Text('Title'), |
| leading: Icon(Icons.info), |
| controlAffinity: ListTileControlAffinity.leading, |
| ), |
| ), |
| ), |
| ); |
| |
| final ListTile listTile = tester.widget(find.byType(ListTile)); |
| expect(listTile.leading.runtimeType, Icon); |
| expect(listTile.trailing, isNull); |
| }); |
| |
| testWidgets('Nested ListTile Semantics', (WidgetTester tester) async { |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile(title: Text('First Expansion Tile'), internalAddSemanticForOnTap: true), |
| ExpansionTile( |
| initiallyExpanded: true, |
| title: Text('Second Expansion Tile'), |
| internalAddSemanticForOnTap: true, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpAndSettle(); |
| |
| // Focus the first ExpansionTile. |
| tester.binding.focusManager.primaryFocus?.nextFocus(); |
| await tester.pumpAndSettle(); |
| |
| // The first list tile is focused. |
| expect( |
| tester.getSemantics(find.byType(ListTile).first), |
| matchesSemantics( |
| isButton: true, |
| hasTapAction: true, |
| hasFocusAction: true, |
| hasEnabledState: true, |
| hasSelectedState: true, |
| isEnabled: true, |
| isFocused: true, |
| isFocusable: true, |
| label: 'First Expansion Tile', |
| textDirection: TextDirection.ltr, |
| ), |
| ); |
| |
| // The first list tile is not focused. |
| expect( |
| tester.getSemantics(find.byType(ListTile).last), |
| matchesSemantics( |
| isButton: true, |
| hasTapAction: true, |
| hasFocusAction: true, |
| hasEnabledState: true, |
| hasSelectedState: true, |
| isEnabled: true, |
| isFocusable: true, |
| label: 'Second Expansion Tile', |
| textDirection: TextDirection.ltr, |
| ), |
| ); |
| handle.dispose(); |
| }); |
| |
| testWidgets( |
| 'ExpansionTile Semantics announcement', |
| (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| title: Text('Title'), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ); |
| |
| // There is no semantics announcement without tap action. |
| expect(tester.takeAnnouncements(), isEmpty); |
| |
| // Tap the title to expand ExpansionTile. |
| await tester.tap(find.text('Title')); |
| await tester.pumpAndSettle(); |
| |
| // The announcement should be the opposite of the current state. |
| // The ExpansionTile is expanded, so the announcement should be |
| // "Expanded". |
| expect( |
| tester.takeAnnouncements().first, |
| isAccessibilityAnnouncement(localizations.collapsedHint), |
| ); |
| |
| // Tap the title to collapse ExpansionTile. |
| await tester.tap(find.text('Title')); |
| await tester.pumpAndSettle(); |
| |
| // The announcement should be the opposite of the current state. |
| // The ExpansionTile is collapsed, so the announcement should be |
| // "Collapsed". |
| expect( |
| tester.takeAnnouncements().first, |
| isAccessibilityAnnouncement(localizations.expandedHint), |
| ); |
| handle.dispose(); |
| }, |
| // [intended] iOS: https://github.com/flutter/flutter/issues/122101. |
| // android: https://github.com/flutter/flutter/issues/165510 |
| skip: |
| defaultTargetPlatform == TargetPlatform.iOS || |
| defaultTargetPlatform == TargetPlatform.android, |
| ); |
| |
| // This is a regression test for https://github.com/flutter/flutter/issues/132264. |
| testWidgets( |
| 'ExpansionTile Semantics announcement is delayed on iOS', |
| (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| title: Text('Title'), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ); |
| |
| // There is no semantics announcement without tap action. |
| expect(tester.takeAnnouncements(), isEmpty); |
| |
| // Tap the title to expand ExpansionTile. |
| await tester.tap(find.text('Title')); |
| await tester.pump(const Duration(seconds: 1)); // Wait for the announcement to be made. |
| |
| expect( |
| tester.takeAnnouncements().first, |
| isAccessibilityAnnouncement(localizations.collapsedHint), |
| ); |
| |
| // Tap the title to collapse ExpansionTile. |
| await tester.tap(find.text('Title')); |
| await tester.pump(const Duration(seconds: 1)); // Wait for the announcement to be made. |
| |
| expect( |
| tester.takeAnnouncements().first, |
| isAccessibilityAnnouncement(localizations.expandedHint), |
| ); |
| handle.dispose(); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.iOS), |
| ); |
| |
| testWidgets('Semantics with the onTapHint is an ancestor of ListTile', ( |
| WidgetTester tester, |
| ) async { |
| // This is a regression test for https://github.com/flutter/flutter/pull/121624 |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile(title: Text('First Expansion Tile')), |
| ExpansionTile(initiallyExpanded: true, title: Text('Second Expansion Tile')), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| SemanticsNode semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).first, matching: find.byType(Semantics)).first, |
| ); |
| expect(semantics, isNotNull); |
| // The onTapHint is passed to semantics properties's hintOverrides. |
| expect(semantics.hintOverrides, isNotNull); |
| // The hint should be the opposite of the current state. |
| // The first ExpansionTile is collapsed, so the hint should be |
| // "double tap to expand". |
| expect(semantics.hintOverrides!.onTapHint, localizations.expansionTileCollapsedTapHint); |
| |
| semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).last, matching: find.byType(Semantics)).first, |
| ); |
| |
| expect(semantics, isNotNull); |
| // The onTapHint is passed to semantics properties's hintOverrides. |
| expect(semantics.hintOverrides, isNotNull); |
| // The hint should be the opposite of the current state. |
| // The second ExpansionTile is expanded, so the hint should be |
| // "double tap to collapse". |
| expect(semantics.hintOverrides!.onTapHint, localizations.expansionTileExpandedTapHint); |
| handle.dispose(); |
| }); |
| |
| testWidgets( |
| 'Semantics hint for iOS and macOS', |
| (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile(title: Text('First Expansion Tile')), |
| ExpansionTile(initiallyExpanded: true, title: Text('Second Expansion Tile')), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| SemanticsNode semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).first, matching: find.byType(Semantics)).first, |
| ); |
| |
| expect(semantics, isNotNull); |
| expect( |
| semantics.hint, |
| '${localizations.expandedHint}\n ${localizations.expansionTileCollapsedHint}', |
| ); |
| |
| semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).last, matching: find.byType(Semantics)).first, |
| ); |
| |
| expect(semantics, isNotNull); |
| expect( |
| semantics.hint, |
| '${localizations.collapsedHint}\n ${localizations.expansionTileExpandedHint}', |
| ); |
| handle.dispose(); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.iOS, |
| TargetPlatform.macOS, |
| }), |
| ); |
| |
| testWidgets('Collapsed ExpansionTile properties can be updated with setState', ( |
| WidgetTester tester, |
| ) async { |
| const expansionTileKey = Key('expansionTileKey'); |
| ShapeBorder collapsedShape = const RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(4)), |
| ); |
| var collapsedTextColor = const Color(0xffffffff); |
| var collapsedBackgroundColor = const Color(0xffff0000); |
| var collapsedIconColor = const Color(0xffffffff); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Column( |
| children: <Widget>[ |
| ExpansionTile( |
| key: expansionTileKey, |
| collapsedShape: collapsedShape, |
| collapsedTextColor: collapsedTextColor, |
| collapsedBackgroundColor: collapsedBackgroundColor, |
| collapsedIconColor: collapsedIconColor, |
| title: const TestText('title'), |
| trailing: const TestIcon(), |
| children: const <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| // This button is used to update the ExpansionTile properties. |
| FilledButton( |
| onPressed: () { |
| setState(() { |
| collapsedShape = const RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(16)), |
| ); |
| collapsedTextColor = const Color(0xff000000); |
| collapsedBackgroundColor = const Color(0xffffff00); |
| collapsedIconColor = const Color(0xff000000); |
| }); |
| }, |
| child: const Text('Update collapsed properties'), |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // When a custom shape is provided, ExpansionTile will use the |
| // Material widget to draw the shape and background color |
| // instead of a Container. |
| Material material = getMaterial(tester); |
| |
| // Test initial ExpansionTile properties. |
| expect( |
| material.shape, |
| const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), |
| ); |
| expect(material.color, const Color(0xffff0000)); |
| expect(material.clipBehavior, Clip.antiAlias); |
| expect( |
| tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, |
| const Color(0xffffffff), |
| ); |
| expect( |
| tester.state<TestTextState>(find.byType(TestText)).textStyle.color, |
| const Color(0xffffffff), |
| ); |
| |
| // Tap the button to update the ExpansionTile properties. |
| await tester.tap(find.text('Update collapsed properties')); |
| await tester.pumpAndSettle(); |
| |
| material = getMaterial(tester); |
| |
| // Test updated ExpansionTile properties. |
| expect( |
| material.shape, |
| const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), |
| ); |
| expect(material.color, const Color(0xffffff00)); |
| expect(material.clipBehavior, Clip.antiAlias); |
| expect( |
| tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, |
| const Color(0xff000000), |
| ); |
| expect( |
| tester.state<TestTextState>(find.byType(TestText)).textStyle.color, |
| const Color(0xff000000), |
| ); |
| }); |
| |
| testWidgets('Expanded ExpansionTile properties can be updated with setState', ( |
| WidgetTester tester, |
| ) async { |
| const expansionTileKey = Key('expansionTileKey'); |
| ShapeBorder shape = const RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(12)), |
| ); |
| var textColor = const Color(0xff00ffff); |
| var backgroundColor = const Color(0xff0000ff); |
| var iconColor = const Color(0xff00ffff); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Column( |
| children: <Widget>[ |
| ExpansionTile( |
| key: expansionTileKey, |
| shape: shape, |
| textColor: textColor, |
| backgroundColor: backgroundColor, |
| iconColor: iconColor, |
| title: const TestText('title'), |
| trailing: const TestIcon(), |
| children: const <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| // This button is used to update the ExpansionTile properties. |
| FilledButton( |
| onPressed: () { |
| setState(() { |
| shape = const RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(6)), |
| ); |
| textColor = const Color(0xffffffff); |
| backgroundColor = const Color(0xff123456); |
| iconColor = const Color(0xffffffff); |
| }); |
| }, |
| child: const Text('Update collapsed properties'), |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Tap to expand the ExpansionTile. |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| // When a custom shape is provided, ExpansionTile will use the |
| // Material widget to draw the shape and background color |
| // instead of a Container. |
| Material material = getMaterial(tester); |
| |
| // Test initial ExpansionTile properties. |
| expect( |
| material.shape, |
| const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), |
| ); |
| expect(material.color, const Color(0xff0000ff)); |
| expect(material.clipBehavior, Clip.antiAlias); |
| expect( |
| tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, |
| const Color(0xff00ffff), |
| ); |
| expect( |
| tester.state<TestTextState>(find.byType(TestText)).textStyle.color, |
| const Color(0xff00ffff), |
| ); |
| |
| // Tap the button to update the ExpansionTile properties. |
| await tester.tap(find.text('Update collapsed properties')); |
| await tester.pumpAndSettle(); |
| |
| material = getMaterial(tester); |
| iconColor = tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!; |
| textColor = tester.state<TestTextState>(find.byType(TestText)).textStyle.color!; |
| |
| // Test updated ExpansionTile properties. |
| expect( |
| material.shape, |
| const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6))), |
| ); |
| expect(material.color, const Color(0xff123456)); |
| expect(material.clipBehavior, Clip.antiAlias); |
| expect( |
| tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color, |
| const Color(0xffffffff), |
| ); |
| expect( |
| tester.state<TestTextState>(find.byType(TestText)).textStyle.color, |
| const Color(0xffffffff), |
| ); |
| }); |
| |
| testWidgets('Override ExpansionTile animation using AnimationStyle', (WidgetTester tester) async { |
| const expansionTileKey = Key('expansionTileKey'); |
| |
| Widget buildExpansionTile({AnimationStyle? animationStyle}) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: ExpansionTile( |
| key: expansionTileKey, |
| expansionAnimationStyle: animationStyle, |
| title: const TestText('title'), |
| children: const <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildExpansionTile()); |
| |
| double getHeight(Key key) => tester.getSize(find.byKey(key)).height; |
| |
| // Test initial ExpansionTile height. |
| expect(getHeight(expansionTileKey), 58.0); |
| |
| // Test the default expansion animation. |
| await tester.tap(find.text('title')); |
| await tester.pump(); |
| await tester.pump( |
| const Duration(milliseconds: 50), |
| ); // Advance the animation by 1/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(67.4, 0.1)); |
| |
| await tester.pump( |
| const Duration(milliseconds: 50), |
| ); // Advance the animation by 2/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(89.6, 0.1)); |
| |
| await tester.pumpAndSettle(); // Advance the animation to the end. |
| |
| expect(getHeight(expansionTileKey), 158.0); |
| |
| // Tap to collapse the ExpansionTile. |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| // Override the animation duration. |
| await tester.pumpWidget( |
| buildExpansionTile( |
| animationStyle: const AnimationStyle(duration: Duration(milliseconds: 800)), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| // Test the overridden animation duration. |
| await tester.tap(find.text('title')); |
| await tester.pump(); |
| await tester.pump( |
| const Duration(milliseconds: 200), |
| ); // Advance the animation by 1/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(67.4, 0.1)); |
| |
| await tester.pump( |
| const Duration(milliseconds: 200), |
| ); // Advance the animation by 2/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(89.6, 0.1)); |
| |
| await tester.pumpAndSettle(); // Advance the animation to the end. |
| |
| expect(getHeight(expansionTileKey), 158.0); |
| |
| // Tap to collapse the ExpansionTile. |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| // Override the animation curve. |
| await tester.pumpWidget( |
| buildExpansionTile( |
| animationStyle: const AnimationStyle( |
| curve: Easing.emphasizedDecelerate, |
| reverseCurve: Easing.emphasizedAccelerate, |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| // Test the overridden animation curve. |
| await tester.tap(find.text('title')); |
| await tester.pump(); |
| await tester.pump( |
| const Duration(milliseconds: 50), |
| ); // Advance the animation by 1/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(141.2, 0.1)); |
| |
| await tester.pump( |
| const Duration(milliseconds: 50), |
| ); // Advance the animation by 2/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(153, 0.1)); |
| |
| await tester.pumpAndSettle(); // Advance the animation to the end. |
| |
| expect(getHeight(expansionTileKey), 158.0); |
| |
| // Test the overridden reverse (collapse) animation curve. |
| await tester.tap(find.text('title')); |
| await tester.pump(); |
| await tester.pump( |
| const Duration(milliseconds: 50), |
| ); // Advance the animation by 1/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(98.6, 0.1)); |
| |
| await tester.pump( |
| const Duration(milliseconds: 50), |
| ); // Advance the animation by 2/4 of its duration. |
| |
| expect(getHeight(expansionTileKey), closeTo(73.4, 0.1)); |
| |
| await tester.pumpAndSettle(); // Advance the animation to the end. |
| |
| expect(getHeight(expansionTileKey), 58.0); |
| |
| // Test no animation. |
| await tester.pumpWidget(buildExpansionTile(animationStyle: AnimationStyle.noAnimation)); |
| |
| // Tap to expand the ExpansionTile. |
| await tester.tap(find.text('title')); |
| await tester.pump(); |
| |
| expect(getHeight(expansionTileKey), 158.0); |
| }); |
| |
| testWidgets('Material3 - ExpansionTile draws Inkwell splash on top of background color', ( |
| WidgetTester tester, |
| ) async { |
| const expansionTileKey = Key('expansionTileKey'); |
| const ShapeBorder shape = RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(16)), |
| ); |
| const ShapeBorder collapsedShape = RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(16)), |
| ); |
| const collapsedBackgroundColor = Color(0xff00ff00); |
| const backgroundColor = Color(0xffff0000); |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Center( |
| child: Padding( |
| padding: EdgeInsets.symmetric(horizontal: 24.0), |
| child: ExpansionTile( |
| key: expansionTileKey, |
| shape: shape, |
| collapsedBackgroundColor: collapsedBackgroundColor, |
| backgroundColor: backgroundColor, |
| collapsedShape: collapsedShape, |
| title: TestText('title'), |
| trailing: TestIcon(), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Tap and hold the ExpansionTile to trigger ink splash. |
| final Offset center = tester.getCenter(find.byKey(expansionTileKey)); |
| final TestGesture gesture = await tester.startGesture(center); |
| await tester.pump(); // Start the splash animation. |
| await tester.pump(const Duration(milliseconds: 100)); // Splash is underway. |
| |
| // Material 3 uses the InkSparkle which uses a shader, so we can't capture |
| // the effect with paint methods. Use a golden test instead. |
| // Check if the ink sparkle is drawn on top of the background color. |
| await expectLater( |
| find.byKey(expansionTileKey), |
| matchesGoldenFile('expansion_tile.ink_splash.drawn_on_top_of_background_color.png'), |
| ); |
| |
| // Finish gesture to release resources. |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('Default clipBehavior when a shape is provided', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Scaffold( |
| body: ExpansionTile( |
| title: Text('Title'), |
| subtitle: Text('Subtitle'), |
| shape: StadiumBorder(), |
| children: <Widget>[ListTile(title: Text('0'))], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(getMaterial(tester).clipBehavior, Clip.antiAlias); |
| }); |
| |
| testWidgets('Can override clipBehavior when a shape is provided', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Scaffold( |
| body: ExpansionTile( |
| title: Text('Title'), |
| subtitle: Text('Subtitle'), |
| shape: StadiumBorder(), |
| clipBehavior: Clip.none, |
| children: <Widget>[ListTile(title: Text('0'))], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(getMaterial(tester).clipBehavior, Clip.none); |
| }); |
| |
| group('Material 2', () { |
| // These tests are only relevant for Material 2. Once Material 2 |
| // support is deprecated and the APIs are removed, these tests |
| // can be deleted. |
| |
| testWidgets('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { |
| final theme = ThemeData(useMaterial3: false); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: const Material( |
| child: ExpansionTile( |
| title: TestText('title'), |
| trailing: TestIcon(), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ); |
| |
| Color getIconColor() => tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!; |
| Color getTextColor() => tester.state<TestTextState>(find.byType(TestText)).textStyle.color!; |
| |
| expect(getIconColor(), theme.unselectedWidgetColor); |
| expect(getTextColor(), theme.textTheme.titleMedium!.color); |
| |
| await tester.tap(find.text('title')); |
| await tester.pumpAndSettle(); |
| |
| expect(getIconColor(), theme.colorScheme.primary); |
| expect(getTextColor(), theme.colorScheme.primary); |
| }); |
| |
| testWidgets('Material2 - ExpansionTile draws inkwell splash on top of background color', ( |
| WidgetTester tester, |
| ) async { |
| const expansionTileKey = Key('expansionTileKey'); |
| final theme = ThemeData(useMaterial3: false); |
| const ShapeBorder shape = RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(16)), |
| ); |
| const ShapeBorder collapsedShape = RoundedRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(16)), |
| ); |
| const collapsedBackgroundColor = Color(0xff00ff00); |
| const backgroundColor = Color(0xffff0000); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: const Material( |
| child: Center( |
| child: Padding( |
| padding: EdgeInsets.symmetric(horizontal: 24.0), |
| child: ExpansionTile( |
| key: expansionTileKey, |
| shape: shape, |
| collapsedBackgroundColor: collapsedBackgroundColor, |
| backgroundColor: backgroundColor, |
| collapsedShape: collapsedShape, |
| title: TestText('title'), |
| trailing: TestIcon(), |
| children: <Widget>[SizedBox(height: 100, width: 100)], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Tap and hold the ExpansionTile to trigger ink splash. |
| final Offset center = tester.getCenter(find.byKey(expansionTileKey)); |
| final TestGesture gesture = await tester.startGesture(center); |
| await tester.pump(); // Start the splash animation. |
| await tester.pump(const Duration(milliseconds: 100)); // Splash is underway. |
| |
| final RenderObject inkFeatures = tester.allRenderObjects.firstWhere( |
| (RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures', |
| ); |
| // Check if the ink splash is drawn on top of the background color. |
| expect( |
| inkFeatures, |
| paints |
| ..path(color: collapsedBackgroundColor) |
| ..circle(color: theme.splashColor), |
| ); |
| |
| // Finish gesture to release resources. |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| }); |
| }); |
| |
| testWidgets('ExpansionTileController isExpanded, expand() and collapse()', ( |
| WidgetTester tester, |
| ) async { |
| final controller = ExpansionTileController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| controller: controller, |
| title: const Text('Title'), |
| children: const <Widget>[Text('Child 0')], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Child 0'), findsNothing); |
| expect(controller.isExpanded, isFalse); |
| controller.expand(); |
| expect(controller.isExpanded, isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsOneWidget); |
| expect(controller.isExpanded, isTrue); |
| controller.collapse(); |
| expect(controller.isExpanded, isFalse); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsNothing); |
| |
| controller.dispose(); |
| }); |
| |
| testWidgets( |
| 'Calling ExpansionTileController.expand/collapsed has no effect if it is already expanded/collapsed', |
| (WidgetTester tester) async { |
| final controller = ExpansionTileController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| controller: controller, |
| title: const Text('Title'), |
| initiallyExpanded: true, |
| children: const <Widget>[Text('Child 0')], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Child 0'), findsOneWidget); |
| expect(controller.isExpanded, isTrue); |
| controller.expand(); |
| expect(controller.isExpanded, isTrue); |
| await tester.pump(); |
| expect(tester.hasRunningAnimations, isFalse); |
| expect(find.text('Child 0'), findsOneWidget); |
| controller.collapse(); |
| expect(controller.isExpanded, isFalse); |
| await tester.pump(); |
| expect(tester.hasRunningAnimations, isTrue); |
| await tester.pumpAndSettle(); |
| expect(controller.isExpanded, isFalse); |
| expect(find.text('Child 0'), findsNothing); |
| controller.collapse(); |
| expect(controller.isExpanded, isFalse); |
| await tester.pump(); |
| expect(tester.hasRunningAnimations, isFalse); |
| |
| controller.dispose(); |
| }, |
| ); |
| |
| testWidgets('Call to ExpansionTileController.of()', (WidgetTester tester) async { |
| final GlobalKey titleKey = GlobalKey(); |
| final GlobalKey childKey = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| initiallyExpanded: true, |
| title: Text('Title', key: titleKey), |
| children: <Widget>[Text('Child 0', key: childKey)], |
| ), |
| ), |
| ), |
| ); |
| |
| final ExpansionTileController controller1 = ExpansionTileController.of( |
| childKey.currentContext!, |
| ); |
| expect(controller1.isExpanded, isTrue); |
| |
| final ExpansionTileController controller2 = ExpansionTileController.of( |
| titleKey.currentContext!, |
| ); |
| expect(controller2.isExpanded, isTrue); |
| |
| expect(controller1, controller2); |
| }); |
| |
| testWidgets('Call to ExpansionTile.maybeOf()', (WidgetTester tester) async { |
| final GlobalKey titleKey = GlobalKey(); |
| final GlobalKey nonDescendantKey = GlobalKey(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile( |
| title: Text('Title', key: titleKey), |
| children: const <Widget>[Text('Child 0')], |
| ), |
| Text('Non descendant', key: nonDescendantKey), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| final ExpansionTileController? controller1 = ExpansionTileController.maybeOf( |
| titleKey.currentContext!, |
| ); |
| expect(controller1, isNotNull); |
| expect(controller1?.isExpanded, isFalse); |
| |
| final ExpansionTileController? controller2 = ExpansionTileController.maybeOf( |
| nonDescendantKey.currentContext!, |
| ); |
| expect(controller2, isNull); |
| }); |
| |
| testWidgets('Check if dense, splashColor, enableFeedback, visualDensity parameter is working', ( |
| WidgetTester tester, |
| ) async { |
| final GlobalKey titleKey = GlobalKey(); |
| final GlobalKey nonDescendantKey = GlobalKey(); |
| |
| const dense = true; |
| const Color splashColor = Colors.blue; |
| const enableFeedback = false; |
| const VisualDensity visualDensity = VisualDensity.compact; |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile( |
| dense: dense, |
| splashColor: splashColor, |
| enableFeedback: enableFeedback, |
| visualDensity: visualDensity, |
| title: Text('Title', key: titleKey), |
| children: const <Widget>[Text('Child 0')], |
| ), |
| Text('Non descendant', key: nonDescendantKey), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| final Finder tileFinder = find.byType(ListTile); |
| final ListTile tileWidget = tester.widget<ListTile>(tileFinder); |
| expect(tileWidget.dense, dense); |
| expect(tileWidget.splashColor, splashColor); |
| expect(tileWidget.enableFeedback, enableFeedback); |
| expect(tileWidget.visualDensity, visualDensity); |
| }); |
| |
| testWidgets('ExpansionTileController should not toggle if disabled', (WidgetTester tester) async { |
| final controller = ExpansionTileController(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| enabled: false, |
| controller: controller, |
| title: const Text('Title'), |
| children: const <Widget>[Text('Child 0')], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Child 0'), findsNothing); |
| expect(controller.isExpanded, isFalse); |
| await tester.tap(find.widgetWithText(ExpansionTile, 'Title')); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsNothing); |
| expect(controller.isExpanded, isFalse); |
| controller.expand(); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsOneWidget); |
| expect(controller.isExpanded, isTrue); |
| await tester.tap(find.widgetWithText(ExpansionTile, 'Title')); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsOneWidget); |
| expect(controller.isExpanded, isTrue); |
| |
| controller.dispose(); |
| }); |
| |
| testWidgets( |
| 'ExpansionTile does not include the default trailing icon when showTrailingIcon: false (#145268)', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| enabled: false, |
| tilePadding: EdgeInsets.zero, |
| title: ColoredBox(color: Colors.red, child: Text('Title')), |
| showTrailingIcon: false, |
| ), |
| ), |
| ), |
| ); |
| |
| final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); |
| final Size titleSize = tester.getSize( |
| find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), |
| ); |
| |
| expect(titleSize.width, materialAppSize.width); |
| }, |
| ); |
| |
| testWidgets( |
| 'ExpansionTile with smaller trailing widget allocates at least 32.0 units of space (preserves original behavior) (#145268)', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| enabled: false, |
| tilePadding: EdgeInsets.zero, |
| title: ColoredBox(color: Colors.red, child: Text('Title')), |
| trailing: SizedBox.shrink(), |
| ), |
| ), |
| ), |
| ); |
| |
| final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); |
| final Size titleSize = tester.getSize( |
| find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), |
| ); |
| |
| expect(titleSize.width, materialAppSize.width - 32.0); |
| }, |
| ); |
| |
| testWidgets('ExpansionTile uses ListTileTheme controlAffinity', (WidgetTester tester) async { |
| Widget buildView(ListTileControlAffinity controlAffinity) { |
| return MaterialApp( |
| home: ListTileTheme( |
| data: ListTileThemeData(controlAffinity: controlAffinity), |
| child: const Material(child: ExpansionTile(title: Text('ExpansionTile'))), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildView(ListTileControlAffinity.leading)); |
| final Finder leading = find.text('ExpansionTile'); |
| final Offset offsetLeading = tester.getTopLeft(leading); |
| expect(offsetLeading, const Offset(56.0, 17.0)); |
| |
| await tester.pumpWidget(buildView(ListTileControlAffinity.trailing)); |
| final Finder trailing = find.text('ExpansionTile'); |
| final Offset offsetTrailing = tester.getTopLeft(trailing); |
| expect(offsetTrailing, const Offset(16.0, 17.0)); |
| |
| await tester.pumpWidget(buildView(ListTileControlAffinity.platform)); |
| final Finder platform = find.text('ExpansionTile'); |
| final Offset offsetPlatform = tester.getTopLeft(platform); |
| expect(offsetPlatform, const Offset(16.0, 17.0)); |
| }); |
| |
| testWidgets('ExpansionTile can accept a new controller', (WidgetTester tester) async { |
| final controller1 = ExpansibleController(); |
| final controller2 = ExpansibleController(); |
| addTearDown(() { |
| controller1.dispose(); |
| controller2.dispose(); |
| }); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| controller: controller1, |
| title: const Text('Title'), |
| initiallyExpanded: true, |
| children: const <Widget>[Text('Child 0')], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Child 0'), findsOne); |
| expect(controller1.isExpanded, isTrue); |
| controller1.collapse(); |
| expect(controller1.isExpanded, isFalse); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsNothing); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| controller: controller2, |
| title: const Text('Title'), |
| initiallyExpanded: true, |
| children: const <Widget>[Text('Child 0')], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Child 0'), findsNothing); |
| controller2.expand(); |
| expect(controller2.isExpanded, isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsOne); |
| }); |
| |
| testWidgets('ExpansionTile can accept a new controller with a different state', ( |
| WidgetTester tester, |
| ) async { |
| final controller1 = ExpansibleController(); |
| final controller2 = ExpansibleController(); |
| addTearDown(() { |
| controller1.dispose(); |
| controller2.dispose(); |
| }); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| controller: controller1, |
| title: const Text('Title'), |
| children: const <Widget>[Text('Child 0')], |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Child 0'), findsNothing); |
| expect(controller1.isExpanded, isFalse); |
| controller1.expand(); |
| expect(controller1.isExpanded, isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsOne); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: ExpansionTile( |
| controller: controller2, |
| title: const Text('Title'), |
| children: const <Widget>[Text('Child 0')], |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.text('Child 0'), |
| findsNothing, |
| reason: 'The widget should update to the state of the new controller', |
| ); |
| controller2.expand(); |
| expect(controller2.isExpanded, isTrue); |
| await tester.pumpAndSettle(); |
| expect(find.text('Child 0'), findsOne); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/176566 |
| testWidgets( |
| 'ExpansionTile semantics hint uses defaultTargetPlatform for VoiceOver regardless of theme platform', |
| (WidgetTester tester) async { |
| // Regression test for VoiceOver accessibility when theme platform differs from device platform. |
| // When someone sets theme.platform to TargetPlatform.android on an iOS device, |
| // VoiceOver should still work correctly by using the actual device platform for semantics hints. |
| |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android), |
| home: const Material( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile(title: Text('First Expansion Tile')), |
| ExpansionTile(initiallyExpanded: true, title: Text('Second Expansion Tile')), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| SemanticsNode semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).first, matching: find.byType(Semantics)).first, |
| ); |
| |
| expect(semantics, isNotNull); |
| // On iOS/macOS platform, the semantics hint should include expanded/collapsed state guidance |
| // even theme platform is set to Android. |
| expect( |
| semantics.hint, |
| '${localizations.expandedHint}\n ${localizations.expansionTileCollapsedHint}', |
| ); |
| |
| semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).last, matching: find.byType(Semantics)).first, |
| ); |
| |
| expect(semantics, isNotNull); |
| expect( |
| semantics.hint, |
| '${localizations.collapsedHint}\n ${localizations.expansionTileExpandedHint}', |
| ); |
| handle.dispose(); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.iOS, |
| TargetPlatform.macOS, |
| }), |
| ); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/173060 |
| group('Semantics tests for non-iOS/macOS/android platforms', () { |
| testWidgets( |
| 'Semantics hint should show current state', |
| (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: Column( |
| children: <Widget>[ |
| ExpansionTile(title: Text('First Expansion Tile')), |
| ExpansionTile(initiallyExpanded: true, title: Text('Second Expansion Tile')), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // Test collapsed tile - should show "Collapsed" hint. |
| SemanticsNode semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).first, matching: find.byType(Semantics)).first, |
| ); |
| expect(semantics, isNotNull); |
| expect(semantics.hint, localizations.expandedHint); |
| |
| // Test expanded tile - should show "Expanded" hint. |
| semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile).last, matching: find.byType(Semantics)).first, |
| ); |
| expect(semantics, isNotNull); |
| expect(semantics.hint, localizations.collapsedHint); |
| |
| handle.dispose(); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.android, |
| TargetPlatform.fuchsia, |
| TargetPlatform.linux, |
| TargetPlatform.windows, |
| }), |
| ); |
| |
| testWidgets( |
| 'Semantics hint updates when expansion state changes', |
| (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile(title: Text('Test Tile'), children: <Widget>[Text('Child')]), |
| ), |
| ), |
| ); |
| |
| // Initially collapsed - should show "Collapsed". |
| SemanticsNode semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile), matching: find.byType(Semantics)).first, |
| ); |
| expect(semantics.hint, localizations.expandedHint); |
| |
| // Tap to expand. |
| await tester.tap(find.text('Test Tile')); |
| await tester.pumpAndSettle(); |
| |
| // Now expanded - should show "Expanded". |
| semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile), matching: find.byType(Semantics)).first, |
| ); |
| expect(semantics.hint, localizations.collapsedHint); |
| |
| // Tap to collapse. |
| await tester.tap(find.text('Test Tile')); |
| await tester.pumpAndSettle(); |
| |
| // Back to collapsed - should show "Collapsed" again. |
| semantics = tester.getSemantics( |
| find.ancestor(of: find.byType(ListTile), matching: find.byType(Semantics)).first, |
| ); |
| expect(semantics.hint, localizations.expandedHint); |
| |
| handle.dispose(); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.android, |
| TargetPlatform.fuchsia, |
| TargetPlatform.linux, |
| TargetPlatform.windows, |
| }), |
| ); |
| }); |
| group('Semantics tests for android platform', () { |
| testWidgets( |
| 'Semantics liveregion updates when expansion state changes', |
| (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| const localizations = DefaultMaterialLocalizations(); |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Material( |
| child: ExpansionTile(title: Text('Test Tile'), children: <Widget>[Text('Child')]), |
| ), |
| ), |
| ); |
| |
| // Initially collapsed - live region label is "Collapsed". |
| |
| SemanticsNode liveRegionSemantics = tester.getSemantics( |
| find.ancestor( |
| of: find.byType(ListTile), |
| matching: find.byWidgetPredicate( |
| (Widget widget) => widget is Semantics && (widget.properties.liveRegion ?? false), |
| ), |
| ), |
| ); |
| expect(liveRegionSemantics.label, localizations.expandedHint); |
| |
| // Tap to expand. |
| await tester.tap(find.text('Test Tile')); |
| await tester.pumpAndSettle(); |
| |
| // Now expanded - should show "Expanded". |
| liveRegionSemantics = tester.getSemantics( |
| find.ancestor( |
| of: find.byType(ListTile), |
| matching: find.byWidgetPredicate( |
| (Widget widget) => widget is Semantics && (widget.properties.liveRegion ?? false), |
| ), |
| ), |
| ); |
| expect(liveRegionSemantics.label, localizations.collapsedHint); |
| |
| // Tap to collapse. |
| await tester.tap(find.text('Test Tile')); |
| await tester.pumpAndSettle(); |
| |
| // Back to collapsed - should show "Collapsed" again. |
| liveRegionSemantics = tester.getSemantics( |
| find.ancestor( |
| of: find.byType(ListTile), |
| matching: find.byWidgetPredicate( |
| (Widget widget) => widget is Semantics && (widget.properties.liveRegion ?? false), |
| ), |
| ), |
| ); |
| expect(liveRegionSemantics.label, localizations.expandedHint); |
| |
| handle.dispose(); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}), |
| ); |
| }); |
| } |