// 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_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; class TestIcon extends StatefulWidget { const TestIcon({Key? key}) : super(key: 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, {Key? key}) : super(key: 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 Color _dividerColor = Color(0x1f333333); const Color _accentColor = Colors.blueAccent; const Color _unselectedWidgetColor = Colors.black54; const Color _headerColor = Colors.black45; testWidgets('ExpansionTile initial state', (WidgetTester tester) async { final Key topKey = UniqueKey(); const Key expandedKey = PageStorageKey<String>('expanded'); const Key collapsedKey = PageStorageKey<String>('collapsed'); const Key defaultKey = PageStorageKey<String>('default'); final Key tileKey = UniqueKey(); 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, initiallyExpanded: false, 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; Container getContainer(Key key) => tester.firstWidget(find.descendant( of: find.byKey(key), matching: find.byType(Container), )); expect(getHeight(topKey), getHeight(expandedKey) - getHeight(tileKey) - 2.0); expect(getHeight(topKey), getHeight(collapsedKey) - 2.0); expect(getHeight(topKey), getHeight(defaultKey) - 2.0); BoxDecoration expandedContainerDecoration = getContainer(expandedKey).decoration! as BoxDecoration; expect(expandedContainerDecoration.color, Colors.red); expect(expandedContainerDecoration.border!.top.color, _dividerColor); expect(expandedContainerDecoration.border!.bottom.color, _dividerColor); BoxDecoration collapsedContainerDecoration = getContainer(collapsedKey).decoration! as BoxDecoration; expect(collapsedContainerDecoration.color, Colors.transparent); expect(collapsedContainerDecoration.border!.top.color, Colors.transparent); expect(collapsedContainerDecoration.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 BoxDecoration collapsingContainerDecoration = getContainer(collapsedKey).decoration! as BoxDecoration; expect(collapsingContainerDecoration.color, Colors.transparent); // Opacity should change but color component should remain the same. expect(collapsingContainerDecoration.border!.top.color, const Color(0x15333333)); expect(collapsingContainerDecoration.border!.bottom.color, const Color(0x15333333)); // 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 = getContainer(expandedKey).decoration! as BoxDecoration; expect(expandedContainerDecoration.color, Colors.transparent); expect(expandedContainerDecoration.border!.top.color, Colors.transparent); expect(expandedContainerDecoration.border!.bottom.color, Colors.transparent); // Collapsed should be expanded now. collapsedContainerDecoration = getContainer(collapsedKey).decoration! as BoxDecoration; expect(collapsedContainerDecoration.color, Colors.transparent); expect(collapsedContainerDecoration.border!.top.color, _dividerColor); expect(collapsedContainerDecoration.border!.bottom.color, _dividerColor); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets('ListTileTheme', (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( accentColor: _accentColor, unselectedWidgetColor: _unselectedWidgetColor, textTheme: const TextTheme(subtitle1: 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, children: const <Widget>[ListTile(title: Text('0'))], trailing: TestIcon(key: expandedIconKey), ), ExpansionTile( initiallyExpanded: false, title: TestText('Collapsed', key: collapsedTitleKey), children: const <Widget>[ListTile(title: Text('0'))], trailing: TestIcon(key: collapsedIconKey), ), ], ), ), ), ), ); 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), _accentColor); expect(textColor(collapsedTitleKey), _headerColor); expect(iconColor(expandedIconKey), _accentColor); 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), _accentColor); expect(iconColor(expandedIconKey), _unselectedWidgetColor); expect(iconColor(collapsedIconKey), _accentColor); }, 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: Material( child: SingleChildScrollView( child: Column( children: const <Widget>[ ExpansionTile( title: Text('Tile 1'), initiallyExpanded: false, maintainState: true, children: <Widget>[ Text('Maintaining State'), ], ), ExpansionTile( title: Text('Title 2'), initiallyExpanded: false, maintainState: false, 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 Rect 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(MaterialApp( home: Material( child: Center( child: ExpansionTile( title: const Text('title'), expandedAlignment: Alignment.centerLeft, children: <Widget>[ Container(height: 100, width: 100), Container(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 Key child0Key = Key('child0'); const Key child1Key = Key('child1'); await tester.pumpWidget(MaterialApp( home: Material( child: Center( child: ExpansionTile( title: const 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>[ Container(height: 100, width: 100, key: child0Key), Container(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 { try { MaterialApp( home: Material( child: ExpansionTile( initiallyExpanded: true, title: const Text('title'), expandedCrossAxisAlignment: CrossAxisAlignment.baseline, ), ), ); } on AssertionError catch (error) { expect(error.toString(), contains('CrossAxisAlignment.baseline is not supported since the expanded' ' children are aligned in a column, not a row. Try to use another constant.')); return; } fail('AssertionError was not thrown when expandedCrossAxisAlignment is CrossAxisAlignment.baseline.'); }); testWidgets('expandedCrossAxisAlignment and expandedAlignment default values', (WidgetTester tester) async { const Key child1Key = Key('child1'); await tester.pumpWidget(MaterialApp( home: Material( child: Center( child: ExpansionTile( title: const Text('title'), children: <Widget>[ Container(height: 100, width: 100), Container(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(MaterialApp( home: Material( child: Center( child: ExpansionTile( title: const Text('title'), children: <Widget>[ Container(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(MaterialApp( home: Material( child: Center( child: ExpansionTile( title: const Text('title'), childrenPadding: const EdgeInsets.fromLTRB(10, 8, 12, 4), children: <Widget>[ Container(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 Key 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), ], ), ), )); BoxDecoration boxDecoration = tester.firstWidget<Container>(find.descendant( of: find.byKey(expansionTileKey), matching: find.byType(Container), )).decoration! as BoxDecoration; expect(boxDecoration.color, collapsedBackgroundColor); await tester.tap(find.text('Title')); await tester.pumpAndSettle(); boxDecoration = tester.firstWidget<Container>(find.descendant( of: find.byKey(expansionTileKey), matching: find.byType(Container), )).decoration! as BoxDecoration; expect(boxDecoration.color, backgroundColor); }); }