Add find.backButton
finder and StandardComponentType
enum to find components in tests. (#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.
This commit is contained in:
parent
1030429e1f
commit
b05c2fad0c
@ -305,6 +305,7 @@ class NavigationMenu extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
key: StandardComponentType.closeButton.key,
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
|
@ -1669,7 +1669,10 @@ class _BackChevron extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
|
||||
return iconWidget;
|
||||
return KeyedSubtree(
|
||||
key: StandardComponentType.backButton.key,
|
||||
child: iconWidget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ abstract class _ActionButton extends StatelessWidget {
|
||||
this.color,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.standardComponent,
|
||||
this.style,
|
||||
});
|
||||
|
||||
@ -56,6 +57,10 @@ abstract class _ActionButton extends StatelessWidget {
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// An enum value to use to identify this button as a type of
|
||||
/// [StandardComponentType].
|
||||
final StandardComponentType? standardComponent;
|
||||
|
||||
/// This returns the appropriate tooltip text for this action button.
|
||||
String _getTooltip(BuildContext context);
|
||||
|
||||
@ -67,6 +72,7 @@ abstract class _ActionButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
return IconButton(
|
||||
key: standardComponent?.key,
|
||||
icon: icon,
|
||||
style: style,
|
||||
color: color,
|
||||
@ -212,7 +218,10 @@ class BackButton extends _ActionButton {
|
||||
super.color,
|
||||
super.style,
|
||||
super.onPressed,
|
||||
}) : super(icon: const BackButtonIcon());
|
||||
}) : super(
|
||||
icon: const BackButtonIcon(),
|
||||
standardComponent: StandardComponentType.backButton,
|
||||
);
|
||||
|
||||
@override
|
||||
void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
|
||||
@ -281,7 +290,10 @@ class CloseButtonIcon extends StatelessWidget {
|
||||
class CloseButton extends _ActionButton {
|
||||
/// Creates a Material Design close icon button.
|
||||
const CloseButton({ super.key, super.color, super.onPressed, super.style })
|
||||
: super(icon: const CloseButtonIcon());
|
||||
: super(
|
||||
icon: const CloseButtonIcon(),
|
||||
standardComponent: StandardComponentType.closeButton,
|
||||
);
|
||||
|
||||
@override
|
||||
void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
|
||||
@ -347,7 +359,10 @@ class DrawerButton extends _ActionButton {
|
||||
super.color,
|
||||
super.style,
|
||||
super.onPressed,
|
||||
}) : super(icon: const DrawerButtonIcon());
|
||||
}) : super(
|
||||
icon: const DrawerButtonIcon(),
|
||||
standardComponent: StandardComponentType.drawerButton,
|
||||
);
|
||||
|
||||
@override
|
||||
void _onPressedCallback(BuildContext context) => Scaffold.of(context).openDrawer();
|
||||
|
@ -1548,6 +1548,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
key: StandardComponentType.moreButton.key,
|
||||
icon: widget.icon ?? Icon(Icons.adaptive.more),
|
||||
padding: widget.padding,
|
||||
splashRadius: widget.splashRadius,
|
||||
|
@ -8,6 +8,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'back_button.dart';
|
||||
import 'button_style.dart';
|
||||
import 'color_scheme.dart';
|
||||
import 'colors.dart';
|
||||
@ -865,11 +866,9 @@ class _ViewContentState extends State<_ViewContent> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget defaultLeading = IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: () { Navigator.of(context).pop(); },
|
||||
final Widget defaultLeading = BackButton(
|
||||
style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||
onPressed: () { Navigator.of(context).pop(); },
|
||||
);
|
||||
|
||||
final List<Widget> defaultTrailing = <Widget>[
|
||||
|
@ -678,6 +678,7 @@ class _SnackBarState extends State<SnackBar> {
|
||||
|
||||
final IconButton? iconButton = showCloseIcon
|
||||
? IconButton(
|
||||
key: StandardComponentType.closeButton.key,
|
||||
icon: const Icon(Icons.close),
|
||||
iconSize: 24.0,
|
||||
color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor,
|
||||
|
@ -220,6 +220,7 @@ class _TextSelectionToolbarOverflowableState extends State<_TextSelectionToolbar
|
||||
// The navButton that shows and hides the overflow menu is the
|
||||
// first child.
|
||||
_TextSelectionToolbarOverflowButton(
|
||||
key: _overflowOpen ? StandardComponentType.backButton.key : StandardComponentType.moreButton.key,
|
||||
icon: Icon(_overflowOpen ? Icons.arrow_back : Icons.more_vert),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -731,6 +732,7 @@ class _TextSelectionToolbarContainer extends StatelessWidget {
|
||||
// forward and back controls.
|
||||
class _TextSelectionToolbarOverflowButton extends StatelessWidget {
|
||||
const _TextSelectionToolbarOverflowButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.tooltip,
|
||||
|
@ -0,0 +1,54 @@
|
||||
// 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/foundation.dart';
|
||||
|
||||
/// An enum identifying standard UI components.
|
||||
///
|
||||
/// This enum is used to attach a key to a widget identifying it as a standard
|
||||
/// UI component for testing and discovery purposes.
|
||||
///
|
||||
/// It is used by the testing infrastructure (e.g. the `find` object in the
|
||||
/// Flutter test framework) to positively identify and/or activate specific
|
||||
/// widgets as representing standard UI components, since many of these
|
||||
/// components vary slightly in the icons or tooltips that they use, and making
|
||||
/// an effective test matcher for them is fragile and error prone.
|
||||
///
|
||||
/// The keys don't have any effect on the functioning of the UI elements, they
|
||||
/// are just a means of identifying them. A widget won't be treated specially if
|
||||
/// it has this key, other than to be found by the testing infrastructure. If
|
||||
/// tests are not searching for them, then adding them to a widget serves no
|
||||
/// purpose.
|
||||
///
|
||||
/// Any widget with the [key] from a value here applied to it will be considered
|
||||
/// to be that type of standard UI component in tests.
|
||||
///
|
||||
/// Types included here are generally only those for which it can be difficult
|
||||
/// or fragile to create a reliable test matcher for. It is not (nor should it
|
||||
/// become) an exhaustive list of standard UI components.
|
||||
///
|
||||
/// These are typically used in tests via `find.backButton()` or
|
||||
/// `find.closeButton()`.
|
||||
enum StandardComponentType {
|
||||
/// Indicates the associated widget is a standard back button, typically used
|
||||
/// to navigate back to the previous screen.
|
||||
backButton,
|
||||
|
||||
/// Indicates the associated widget is a close button, typically used to
|
||||
/// dismiss a dialog or modal sheet.
|
||||
closeButton,
|
||||
|
||||
/// Indicates the associated widget is a "more" button, typically used to
|
||||
/// display a menu of additional options.
|
||||
moreButton,
|
||||
|
||||
/// Indicates the associated widget is a drawer button, typically used to open
|
||||
/// a drawer.
|
||||
drawerButton;
|
||||
|
||||
/// Returns a [ValueKey] for this [StandardComponentType].
|
||||
///
|
||||
/// Attach this key to a widget to indicate it is a standard UI component.
|
||||
ValueKey<StandardComponentType> get key => ValueKey<StandardComponentType>(this);
|
||||
}
|
@ -145,6 +145,7 @@ export 'src/widgets/slotted_render_object_widget.dart';
|
||||
export 'src/widgets/snapshot_widget.dart';
|
||||
export 'src/widgets/spacer.dart';
|
||||
export 'src/widgets/spell_check.dart';
|
||||
export 'src/widgets/standard_component_type.dart';
|
||||
export 'src/widgets/status_transitions.dart';
|
||||
export 'src/widgets/system_context_menu.dart';
|
||||
export 'src/widgets/table.dart';
|
||||
|
@ -463,7 +463,7 @@ void main() {
|
||||
expect(
|
||||
flying(
|
||||
tester,
|
||||
find.byWidgetPredicate((Widget widget) => widget.key != null),
|
||||
find.byWidgetPredicate((Widget widget) => widget.key != null && widget.key is GlobalKey),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
|
@ -875,7 +875,7 @@ void main() {
|
||||
|
||||
final Icon icon = tester.widget(find.byType(Icon));
|
||||
expect(icon.icon, expectedIcon, reason: "didn't find close icon for $type");
|
||||
expect(find.byType(CloseButton), findsOneWidget, reason: "didn't find close button for $type");
|
||||
expect(find.byKey(StandardComponentType.closeButton.key), findsOneWidget, reason: "didn't find close button for $type");
|
||||
}
|
||||
|
||||
PageRoute<void> materialRouteBuilder() {
|
||||
|
@ -888,7 +888,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.textCapitalization, TextCapitalization.characters);
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
|
||||
await tester.tap(find.backButton());
|
||||
await tester.pump();
|
||||
|
||||
await tester.pumpWidget(buildSearchAnchor(TextCapitalization.none));
|
||||
@ -981,7 +981,7 @@ void main() {
|
||||
final TextField textFieldInView = tester.widget<TextField>(textFieldFinder);
|
||||
expect(textFieldInView.textCapitalization, TextCapitalization.characters);
|
||||
// Close search view.
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
|
||||
await tester.tap(find.backButton());
|
||||
await tester.pumpAndSettle();
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.textCapitalization, TextCapitalization.characters);
|
||||
@ -1139,7 +1139,7 @@ void main() {
|
||||
expect(decoration.border!.bottom.color, colorScheme.outline);
|
||||
|
||||
// Default search view has a leading back button on the start of the header.
|
||||
expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget);
|
||||
expect(find.backButton(), findsOneWidget);
|
||||
|
||||
final Text helperText = tester.widget(find.text('hint text'));
|
||||
expect(helperText.style?.color, colorScheme.onSurfaceVariant);
|
||||
@ -1408,13 +1408,13 @@ void main() {
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
// Default is a icon button with arrow_back.
|
||||
expect(find.widgetWithIcon(IconButton, Icons.arrow_back), findsOneWidget);
|
||||
expect(find.backButton(), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(Container());
|
||||
await tester.pumpWidget(buildAnchor(viewLeading: const Icon(Icons.history)));
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.arrow_back), findsNothing);
|
||||
expect(find.backButton(), findsNothing);
|
||||
expect(find.byIcon(Icons.history), findsOneWidget);
|
||||
});
|
||||
|
||||
@ -2189,13 +2189,13 @@ void main() {
|
||||
// Open the search view
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
|
||||
expect(find.backButton(), findsOneWidget);
|
||||
|
||||
// Change window size
|
||||
tester.view.physicalSize = const Size(250.0, 200.0);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.arrow_back), findsNothing);
|
||||
expect(find.backButton(), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Full-screen search view route should stay if the window size changes', (WidgetTester tester) async {
|
||||
@ -2230,13 +2230,13 @@ void main() {
|
||||
// Open a full-screen search view
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
|
||||
expect(find.backButton(), findsOneWidget);
|
||||
|
||||
// Change window size
|
||||
tester.view.physicalSize = const Size(250.0, 200.0);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
|
||||
expect(find.backButton(), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Search view route does not throw exception during pop animation', (WidgetTester tester) async {
|
||||
@ -2275,7 +2275,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Pop search view route
|
||||
await tester.tap(find.byIcon(Icons.arrow_back));
|
||||
await tester.tap(find.backButton());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// No exception.
|
||||
@ -2815,7 +2815,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.keyboardType, TextInputType.number);
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
|
||||
await tester.tap(find.backButton());
|
||||
await tester.pump();
|
||||
|
||||
await tester.pumpWidget(buildSearchAnchor(TextInputType.phone));
|
||||
@ -2849,7 +2849,7 @@ void main() {
|
||||
final TextField textFieldInView = tester.widget<TextField>(textFieldFinder);
|
||||
expect(textFieldInView.keyboardType, TextInputType.number);
|
||||
// Close search view.
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
|
||||
await tester.tap(find.backButton());
|
||||
await tester.pumpAndSettle();
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.keyboardType, TextInputType.number);
|
||||
@ -2907,7 +2907,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.textInputAction, TextInputAction.previous);
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
|
||||
await tester.tap(find.backButton());
|
||||
await tester.pump();
|
||||
|
||||
await tester.pumpWidget(buildSearchAnchor(TextInputAction.send));
|
||||
@ -2941,7 +2941,7 @@ void main() {
|
||||
final TextField textFieldInView = tester.widget<TextField>(textFieldFinder);
|
||||
expect(textFieldInView.textInputAction, TextInputAction.previous);
|
||||
// Close search view.
|
||||
await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_back));
|
||||
await tester.tap(find.backButton());
|
||||
await tester.pumpAndSettle();
|
||||
final TextField textField = tester.widget(find.byType(TextField));
|
||||
expect(textField.textInputAction, TextInputAction.previous);
|
||||
|
@ -192,11 +192,11 @@ void expectMaterialToolbarForFullSelection() {
|
||||
}
|
||||
|
||||
Finder findMaterialOverflowNextButton() {
|
||||
return find.byIcon(Icons.more_vert);
|
||||
return find.byKey(StandardComponentType.moreButton.key);
|
||||
}
|
||||
|
||||
Finder findMaterialOverflowBackButton() {
|
||||
return find.byIcon(Icons.arrow_back);
|
||||
return find.byKey(StandardComponentType.backButton.key);
|
||||
}
|
||||
|
||||
Future<void> tapMaterialOverflowNextButton(WidgetTester tester) async {
|
||||
|
@ -468,11 +468,77 @@ class CommonFinders {
|
||||
return _AncestorWidgetFinder(of, matching, matchLeaves: matchRoot);
|
||||
}
|
||||
|
||||
/// Finds a standard "back" button.
|
||||
///
|
||||
/// A common element on many user interfaces is the "back" button. This is the
|
||||
/// button which takes the user back to the previous page/screen/state.
|
||||
///
|
||||
/// It is useful in tests to be able to find these buttons, both for tapping
|
||||
/// them or verifying their existence, but because different platforms and
|
||||
/// locales have different icons representing them with different labels and
|
||||
/// tooltips, it's not desirable to have to look them up by these attributes.
|
||||
///
|
||||
/// This finder uses the [StandardComponentType] enum to look for buttons that
|
||||
/// have the key associated with [StandardComponentType.backButton]. If
|
||||
/// another widget is assigned that key, then it too will be considered an
|
||||
/// "official" back button in the widget tree, allowing this matcher to still
|
||||
/// find it even though it might use a different icon or tooltip.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.backButton(), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [StandardComponentType], the enum that enumerates components that are
|
||||
/// both common in user interfaces, but which also can vary slightly in
|
||||
/// presentation across different platforms, locales, and devices.
|
||||
/// * [BackButton], the Flutter Material widget that represents the back
|
||||
/// button.
|
||||
Finder backButton() {
|
||||
return byKey(StandardComponentType.backButton.key);
|
||||
}
|
||||
|
||||
/// Finds a standard "close" button.
|
||||
///
|
||||
/// A common element on many user interfaces is the "close" button. This is
|
||||
/// the button which closes or cancels whatever it is attached to.
|
||||
///
|
||||
/// It is useful in tests to be able to find these buttons, both for tapping
|
||||
/// them or verifying their existence, but because different platforms and
|
||||
/// locales have different icons representing them with different labels and
|
||||
/// tooltips, it's not desirable to have to look them up by these attributes.
|
||||
///
|
||||
/// This finder uses the [StandardComponentType] enum to look for buttons that
|
||||
/// have the key associated with [StandardComponentType.closeButton]. If
|
||||
/// another widget is assigned that key, then it too will be considered an
|
||||
/// "official" close button in the widget tree, allowing this matcher to still
|
||||
/// find it even though it might use a different icon or tooltip.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.closeButton(), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [StandardComponentType], the enum that enumerates components that are
|
||||
/// both common in user interfaces, but which also can vary slightly in
|
||||
/// presentation across different platforms, locales, and devices.
|
||||
/// * [CloseButton], the Flutter Material widget that represents a close
|
||||
/// button.
|
||||
Finder closeButton() {
|
||||
return byKey(StandardComponentType.closeButton.key);
|
||||
}
|
||||
|
||||
/// Finds [Semantics] widgets matching the given `label`, either by
|
||||
/// [RegExp.hasMatch] or string equality.
|
||||
///
|
||||
/// The framework may combine semantics labels in certain scenarios, such as
|
||||
/// when multiple [Text] widgets are in a [MaterialButton] widget. In such a
|
||||
/// when multiple [Text] widgets are in a [TextButton] widget. In such a
|
||||
/// case, it may be preferable to match by regular expression. Consumers of
|
||||
/// this API __must not__ introduce unsuitable content into the semantics tree
|
||||
/// for the purposes of testing; in particular, you should prefer matching by
|
||||
@ -515,7 +581,6 @@ class CommonFinders {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Provides lightweight syntax for getting frequently used semantics finders.
|
||||
///
|
||||
/// This class is instantiated once, as [CommonFinders.semantics], under [find].
|
||||
|
Loading…
x
Reference in New Issue
Block a user