Skip to content

Commit 00c8f18

Browse files
gspencergoogBuchimi
authored andcommitted
Add find.backButton finder and StandardComponentType enum to find components in tests. (flutter#149349)
## Description This adds `find.backButton()` in the Common Finders to allow finding different types of standard UI elements. It works by attaching a key made from an enum value in a new enum called `StandardComponentType` to all of the standard widgets that perform the associated function. I also substituted the finder in several places where it is useful in tests. This allows writing tests that want to find the "back" button without having to know exactly which icon the back button uses under what circumstances. To do it correctly is actually quite complicated, since there are several adaptations that occur (based on platform, and whether it is web or not). ## Tests - Added tests.
1 parent 2b9f711 commit 00c8f18

File tree

14 files changed

+170
-28
lines changed

14 files changed

+170
-28
lines changed

dev/integration_tests/new_gallery/lib/studies/fortnightly/shared.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ class NavigationMenu extends StatelessWidget {
305305
Row(
306306
children: <Widget>[
307307
IconButton(
308+
key: StandardComponentType.closeButton.key,
308309
icon: const Icon(Icons.close),
309310
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
310311
onPressed: () => Navigator.pop(context),

packages/flutter/lib/src/cupertino/nav_bar.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1669,7 +1669,10 @@ class _BackChevron extends StatelessWidget {
16691669
break;
16701670
}
16711671

1672-
return iconWidget;
1672+
return KeyedSubtree(
1673+
key: StandardComponentType.backButton.key,
1674+
child: iconWidget,
1675+
);
16731676
}
16741677
}
16751678

packages/flutter/lib/src/material/action_buttons.dart

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ abstract class _ActionButton extends StatelessWidget {
2828
this.color,
2929
required this.icon,
3030
required this.onPressed,
31+
this.standardComponent,
3132
this.style,
3233
});
3334

@@ -56,6 +57,10 @@ abstract class _ActionButton extends StatelessWidget {
5657
/// Null by default.
5758
final ButtonStyle? style;
5859

60+
/// An enum value to use to identify this button as a type of
61+
/// [StandardComponentType].
62+
final StandardComponentType? standardComponent;
63+
5964
/// This returns the appropriate tooltip text for this action button.
6065
String _getTooltip(BuildContext context);
6166

@@ -67,6 +72,7 @@ abstract class _ActionButton extends StatelessWidget {
6772
Widget build(BuildContext context) {
6873
assert(debugCheckHasMaterialLocalizations(context));
6974
return IconButton(
75+
key: standardComponent?.key,
7076
icon: icon,
7177
style: style,
7278
color: color,
@@ -212,7 +218,10 @@ class BackButton extends _ActionButton {
212218
super.color,
213219
super.style,
214220
super.onPressed,
215-
}) : super(icon: const BackButtonIcon());
221+
}) : super(
222+
icon: const BackButtonIcon(),
223+
standardComponent: StandardComponentType.backButton,
224+
);
216225

217226
@override
218227
void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
@@ -281,7 +290,10 @@ class CloseButtonIcon extends StatelessWidget {
281290
class CloseButton extends _ActionButton {
282291
/// Creates a Material Design close icon button.
283292
const CloseButton({ super.key, super.color, super.onPressed, super.style })
284-
: super(icon: const CloseButtonIcon());
293+
: super(
294+
icon: const CloseButtonIcon(),
295+
standardComponent: StandardComponentType.closeButton,
296+
);
285297

286298
@override
287299
void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
@@ -347,7 +359,10 @@ class DrawerButton extends _ActionButton {
347359
super.color,
348360
super.style,
349361
super.onPressed,
350-
}) : super(icon: const DrawerButtonIcon());
362+
}) : super(
363+
icon: const DrawerButtonIcon(),
364+
standardComponent: StandardComponentType.drawerButton,
365+
);
351366

352367
@override
353368
void _onPressedCallback(BuildContext context) => Scaffold.of(context).openDrawer();

packages/flutter/lib/src/material/popup_menu.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
15481548
}
15491549

15501550
return IconButton(
1551+
key: StandardComponentType.moreButton.key,
15511552
icon: widget.icon ?? Icon(Icons.adaptive.more),
15521553
padding: widget.padding,
15531554
splashRadius: widget.splashRadius,

packages/flutter/lib/src/material/search_anchor.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:ui';
88

99
import 'package:flutter/widgets.dart';
1010

11+
import 'back_button.dart';
1112
import 'button_style.dart';
1213
import 'color_scheme.dart';
1314
import 'colors.dart';
@@ -865,11 +866,9 @@ class _ViewContentState extends State<_ViewContent> {
865866

866867
@override
867868
Widget build(BuildContext context) {
868-
final Widget defaultLeading = IconButton(
869-
icon: const Icon(Icons.arrow_back),
870-
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
871-
onPressed: () { Navigator.of(context).pop(); },
869+
final Widget defaultLeading = BackButton(
872870
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
871+
onPressed: () { Navigator.of(context).pop(); },
873872
);
874873

875874
final List<Widget> defaultTrailing = <Widget>[

packages/flutter/lib/src/material/snack_bar.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@ class _SnackBarState extends State<SnackBar> {
678678

679679
final IconButton? iconButton = showCloseIcon
680680
? IconButton(
681+
key: StandardComponentType.closeButton.key,
681682
icon: const Icon(Icons.close),
682683
iconSize: 24.0,
683684
color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor,

packages/flutter/lib/src/material/text_selection_toolbar.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ class _TextSelectionToolbarOverflowableState extends State<_TextSelectionToolbar
220220
// The navButton that shows and hides the overflow menu is the
221221
// first child.
222222
_TextSelectionToolbarOverflowButton(
223+
key: _overflowOpen ? StandardComponentType.backButton.key : StandardComponentType.moreButton.key,
223224
icon: Icon(_overflowOpen ? Icons.arrow_back : Icons.more_vert),
224225
onPressed: () {
225226
setState(() {
@@ -731,6 +732,7 @@ class _TextSelectionToolbarContainer extends StatelessWidget {
731732
// forward and back controls.
732733
class _TextSelectionToolbarOverflowButton extends StatelessWidget {
733734
const _TextSelectionToolbarOverflowButton({
735+
super.key,
734736
required this.icon,
735737
this.onPressed,
736738
this.tooltip,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/foundation.dart';
6+
7+
/// An enum identifying standard UI components.
8+
///
9+
/// This enum is used to attach a key to a widget identifying it as a standard
10+
/// UI component for testing and discovery purposes.
11+
///
12+
/// It is used by the testing infrastructure (e.g. the `find` object in the
13+
/// Flutter test framework) to positively identify and/or activate specific
14+
/// widgets as representing standard UI components, since many of these
15+
/// components vary slightly in the icons or tooltips that they use, and making
16+
/// an effective test matcher for them is fragile and error prone.
17+
///
18+
/// The keys don't have any effect on the functioning of the UI elements, they
19+
/// are just a means of identifying them. A widget won't be treated specially if
20+
/// it has this key, other than to be found by the testing infrastructure. If
21+
/// tests are not searching for them, then adding them to a widget serves no
22+
/// purpose.
23+
///
24+
/// Any widget with the [key] from a value here applied to it will be considered
25+
/// to be that type of standard UI component in tests.
26+
///
27+
/// Types included here are generally only those for which it can be difficult
28+
/// or fragile to create a reliable test matcher for. It is not (nor should it
29+
/// become) an exhaustive list of standard UI components.
30+
///
31+
/// These are typically used in tests via `find.backButton()` or
32+
/// `find.closeButton()`.
33+
enum StandardComponentType {
34+
/// Indicates the associated widget is a standard back button, typically used
35+
/// to navigate back to the previous screen.
36+
backButton,
37+
38+
/// Indicates the associated widget is a close button, typically used to
39+
/// dismiss a dialog or modal sheet.
40+
closeButton,
41+
42+
/// Indicates the associated widget is a "more" button, typically used to
43+
/// display a menu of additional options.
44+
moreButton,
45+
46+
/// Indicates the associated widget is a drawer button, typically used to open
47+
/// a drawer.
48+
drawerButton;
49+
50+
/// Returns a [ValueKey] for this [StandardComponentType].
51+
///
52+
/// Attach this key to a widget to indicate it is a standard UI component.
53+
ValueKey<StandardComponentType> get key => ValueKey<StandardComponentType>(this);
54+
}

packages/flutter/lib/widgets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export 'src/widgets/slotted_render_object_widget.dart';
145145
export 'src/widgets/snapshot_widget.dart';
146146
export 'src/widgets/spacer.dart';
147147
export 'src/widgets/spell_check.dart';
148+
export 'src/widgets/standard_component_type.dart';
148149
export 'src/widgets/status_transitions.dart';
149150
export 'src/widgets/system_context_menu.dart';
150151
export 'src/widgets/table.dart';

packages/flutter/test/cupertino/nav_bar_transition_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ void main() {
463463
expect(
464464
flying(
465465
tester,
466-
find.byWidgetPredicate((Widget widget) => widget.key != null),
466+
find.byWidgetPredicate((Widget widget) => widget.key != null && widget.key is GlobalKey),
467467
),
468468
findsNothing,
469469
);

packages/flutter/test/material/scaffold_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ void main() {
875875

876876
final Icon icon = tester.widget(find.byType(Icon));
877877
expect(icon.icon, expectedIcon, reason: "didn't find close icon for $type");
878-
expect(find.byType(CloseButton), findsOneWidget, reason: "didn't find close button for $type");
878+
expect(find.byKey(StandardComponentType.closeButton.key), findsOneWidget, reason: "didn't find close button for $type");
879879
}
880880

881881
PageRoute<void> materialRouteBuilder() {

packages/flutter/test/material/search_anchor_test.dart

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ void main() {
888888
await tester.pumpAndSettle();
889889
TextField textField = tester.widget(find.byType(TextField));
890890
expect(textField.textCapitalization, TextCapitalization.characters);
891-
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
891+
await tester.tap(find.backButton());
892892
await tester.pump();
893893

894894
await tester.pumpWidget(buildSearchAnchor(TextCapitalization.none));
@@ -981,7 +981,7 @@ void main() {
981981
final TextField textFieldInView = tester.widget<TextField>(textFieldFinder);
982982
expect(textFieldInView.textCapitalization, TextCapitalization.characters);
983983
// Close search view.
984-
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
984+
await tester.tap(find.backButton());
985985
await tester.pumpAndSettle();
986986
final TextField textField = tester.widget(find.byType(TextField));
987987
expect(textField.textCapitalization, TextCapitalization.characters);
@@ -1139,7 +1139,7 @@ void main() {
11391139
expect(decoration.border!.bottom.color, colorScheme.outline);
11401140

11411141
// Default search view has a leading back button on the start of the header.
1142-
expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget);
1142+
expect(find.backButton(), findsOneWidget);
11431143

11441144
final Text helperText = tester.widget(find.text('hint text'));
11451145
expect(helperText.style?.color, colorScheme.onSurfaceVariant);
@@ -1408,13 +1408,13 @@ void main() {
14081408
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
14091409
await tester.pumpAndSettle();
14101410
// Default is a icon button with arrow_back.
1411-
expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget);
1411+
expect(find.backButton(), findsOneWidget);
14121412

14131413
await tester.pumpWidget(Container());
14141414
await tester.pumpWidget(buildAnchor(viewLeading: const Icon(Icons.history)));
14151415
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
14161416
await tester.pumpAndSettle();
1417-
expect(find.byIcon(Icons.arrow_back), findsNothing);
1417+
expect(find.backButton(), findsNothing);
14181418
expect(find.byIcon(Icons.history), findsOneWidget);
14191419
});
14201420

@@ -2189,13 +2189,13 @@ void main() {
21892189
// Open the search view
21902190
await tester.tap(find.byIcon(Icons.search));
21912191
await tester.pumpAndSettle();
2192-
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
2192+
expect(find.backButton(), findsOneWidget);
21932193

21942194
// Change window size
21952195
tester.view.physicalSize = const Size(250.0, 200.0);
21962196
tester.view.devicePixelRatio = 1.0;
21972197
await tester.pumpAndSettle();
2198-
expect(find.byIcon(Icons.arrow_back), findsNothing);
2198+
expect(find.backButton(), findsNothing);
21992199
});
22002200

22012201
testWidgets('Full-screen search view route should stay if the window size changes', (WidgetTester tester) async {
@@ -2230,13 +2230,13 @@ void main() {
22302230
// Open a full-screen search view
22312231
await tester.tap(find.byIcon(Icons.search));
22322232
await tester.pumpAndSettle();
2233-
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
2233+
expect(find.backButton(), findsOneWidget);
22342234

22352235
// Change window size
22362236
tester.view.physicalSize = const Size(250.0, 200.0);
22372237
tester.view.devicePixelRatio = 1.0;
22382238
await tester.pumpAndSettle();
2239-
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
2239+
expect(find.backButton(), findsOneWidget);
22402240
});
22412241

22422242
testWidgets('Search view route does not throw exception during pop animation', (WidgetTester tester) async {
@@ -2275,7 +2275,7 @@ void main() {
22752275
await tester.pumpAndSettle();
22762276

22772277
// Pop search view route
2278-
await tester.tap(find.byIcon(Icons.arrow_back));
2278+
await tester.tap(find.backButton());
22792279
await tester.pumpAndSettle();
22802280

22812281
// No exception.
@@ -2815,7 +2815,7 @@ void main() {
28152815
await tester.pumpAndSettle();
28162816
TextField textField = tester.widget(find.byType(TextField));
28172817
expect(textField.keyboardType, TextInputType.number);
2818-
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
2818+
await tester.tap(find.backButton());
28192819
await tester.pump();
28202820

28212821
await tester.pumpWidget(buildSearchAnchor(TextInputType.phone));
@@ -2849,7 +2849,7 @@ void main() {
28492849
final TextField textFieldInView = tester.widget<TextField>(textFieldFinder);
28502850
expect(textFieldInView.keyboardType, TextInputType.number);
28512851
// Close search view.
2852-
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
2852+
await tester.tap(find.backButton());
28532853
await tester.pumpAndSettle();
28542854
final TextField textField = tester.widget(find.byType(TextField));
28552855
expect(textField.keyboardType, TextInputType.number);
@@ -2907,7 +2907,7 @@ void main() {
29072907
await tester.pumpAndSettle();
29082908
TextField textField = tester.widget(find.byType(TextField));
29092909
expect(textField.textInputAction, TextInputAction.previous);
2910-
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
2910+
await tester.tap(find.backButton());
29112911
await tester.pump();
29122912

29132913
await tester.pumpWidget(buildSearchAnchor(TextInputAction.send));
@@ -2941,7 +2941,7 @@ void main() {
29412941
final TextField textFieldInView = tester.widget<TextField>(textFieldFinder);
29422942
expect(textFieldInView.textInputAction, TextInputAction.previous);
29432943
// Close search view.
2944-
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
2944+
await tester.tap(find.backButton());
29452945
await tester.pumpAndSettle();
29462946
final TextField textField = tester.widget(find.byType(TextField));
29472947
expect(textField.textInputAction, TextInputAction.previous);

packages/flutter/test/widgets/text_selection_toolbar_utils.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,11 @@ void expectMaterialToolbarForFullSelection() {
192192
}
193193

194194
Finder findMaterialOverflowNextButton() {
195-
return find.byIcon(Icons.more_vert);
195+
return find.byKey(StandardComponentType.moreButton.key);
196196
}
197197

198198
Finder findMaterialOverflowBackButton() {
199-
return find.byIcon(Icons.arrow_back);
199+
return find.byKey(StandardComponentType.backButton.key);
200200
}
201201

202202
Future<void> tapMaterialOverflowNextButton(WidgetTester tester) async {

0 commit comments

Comments
 (0)