2076 lines
70 KiB
Dart
2076 lines
70 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.
|
|
|
|
// 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}),
|
|
);
|
|
});
|
|
}
|