mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add bySemanticsIdentifier
finder for finding by identifier (#155571)
## Add `bySemanticsIdentifier` finder for finding by identifier ### Description This pull request introduces a new finder, `CommonFinders.bySemanticsIdentifier`, to the Flutter testing framework. This finder allows developers to locate `Semantics` widgets based on their `identifier` property, enhancing the precision and flexibility of widget tests. ### Motivation Establish a consistent and reliable method for locating elements in integration and end-to-end (e2e) tests. Unlike `label` or `key`, which may carry functional significance within the application, the `identifier` is purely declarative and does not impact functionality. Utilizing the `identifier` for finding semantics widgets ensures that tests can target specific elements without interfering with the app's behavior, thereby enhancing test reliability, maintainability, and reusability across testing frameworks. ### Changes - **semantics.dart** - Updated documentation to mention that `identifier` can be matched using `CommonFinders.bySemanticsIdentifier`. - **finders.dart** - Added the `bySemanticsIdentifier` method to `CommonFinders`. - Supports both exact string matches and regular expression patterns. - Includes error handling to ensure semantics are enabled during tests. - **finders_test.dart** - Added tests to verify that `bySemanticsIdentifier` correctly finds widgets by exact identifier and regex patterns. - Ensures that the finder behaves as expected when semantics are not enabled. ### Usage Developers can use the new finder in their tests as follows: ```dart // Exact match expect(find.bySemanticsIdentifier('Back'), findsOneWidget); // Regular expression match expect(find.bySemanticsIdentifier(RegExp(r'^item-')), findsNWidgets(2)); ```
This commit is contained in:
parent
138144bb2f
commit
37d5dc45d1
@ -1213,7 +1213,8 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
/// This value is not exposed to the users of the app.
|
||||
///
|
||||
/// It's usually used for UI testing with tools that work by querying the
|
||||
/// native accessibility, like UIAutomator, XCUITest, or Appium.
|
||||
/// native accessibility, like UIAutomator, XCUITest, or Appium. It can be
|
||||
/// matched with [CommonFinders.bySemanticsIdentifier].
|
||||
///
|
||||
/// On Android, this is used for `AccessibilityNodeInfo.setViewIdResourceName`.
|
||||
/// It'll be appear in accessibility hierarchy as `resource-id`.
|
||||
|
@ -470,8 +470,8 @@ class CommonFinders {
|
||||
|
||||
/// 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.
|
||||
/// 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
|
||||
@ -555,11 +555,49 @@ class CommonFinders {
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder bySemanticsLabel(Pattern label, { bool skipOffstage = true }) {
|
||||
Finder bySemanticsLabel(Pattern label, {bool skipOffstage = true}) {
|
||||
return _bySemanticsProperty(
|
||||
label,
|
||||
(SemanticsNode? semantics) => semantics?.label,
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds [Semantics] widgets matching the given `identifier`, either by
|
||||
/// [RegExp.hasMatch] or string equality.
|
||||
///
|
||||
/// This allows matching against the identifier of a [Semantics] widget, which
|
||||
/// is a unique identifier for the widget in the semantics tree. This is
|
||||
/// exposed to offer a unified way widget tests and e2e tests can match
|
||||
/// against a [Semantics] widget.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.bySemanticsIdentifier('Back'), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder bySemanticsIdentifier(Pattern identifier, {bool skipOffstage = true}) {
|
||||
return _bySemanticsProperty(
|
||||
identifier,
|
||||
(SemanticsNode? semantics) => semantics?.identifier,
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
}
|
||||
|
||||
Finder _bySemanticsProperty(
|
||||
Pattern pattern,
|
||||
String? Function(SemanticsNode?) propertyGetter,
|
||||
{bool skipOffstage = true}
|
||||
) {
|
||||
if (!SemanticsBinding.instance.semanticsEnabled) {
|
||||
throw StateError('Semantics are not enabled. '
|
||||
'Make sure to call tester.ensureSemantics() before using '
|
||||
'this finder, and call dispose on its return value after.');
|
||||
throw StateError(
|
||||
'Semantics are not enabled. '
|
||||
'Make sure to call tester.ensureSemantics() before using '
|
||||
'this finder, and call dispose on its return value after.',
|
||||
);
|
||||
}
|
||||
return byElementPredicate(
|
||||
(Element element) {
|
||||
@ -568,13 +606,13 @@ class CommonFinders {
|
||||
if (element is! RenderObjectElement) {
|
||||
return false;
|
||||
}
|
||||
final String? semanticsLabel = element.renderObject.debugSemantics?.label;
|
||||
if (semanticsLabel == null) {
|
||||
final String? propertyValue = propertyGetter(element.renderObject.debugSemantics);
|
||||
if (propertyValue == null) {
|
||||
return false;
|
||||
}
|
||||
return label is RegExp
|
||||
? label.hasMatch(semanticsLabel)
|
||||
: label == semanticsLabel;
|
||||
return pattern is RegExp
|
||||
? pattern.hasMatch(propertyValue)
|
||||
: pattern == propertyValue;
|
||||
},
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
|
@ -299,6 +299,55 @@ void main() {
|
||||
expect(find.bySemanticsLabel('Foo'), findsOneWidget);
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Throws StateError if semantics are not enabled (bySemanticsIdentifier)', (WidgetTester tester) async {
|
||||
expect(
|
||||
() => find.bySemanticsIdentifier('Add'),
|
||||
throwsA(
|
||||
isA<StateError>().having(
|
||||
(StateError e) => e.message,
|
||||
'message',
|
||||
contains('Semantics are not enabled'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, semanticsEnabled: false);
|
||||
|
||||
testWidgets('finds Semantically labeled widgets by identifier', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
await tester.pumpWidget(_boilerplate(
|
||||
Semantics(
|
||||
identifier: 'Add',
|
||||
button: true,
|
||||
child: const TextButton(
|
||||
onPressed: null,
|
||||
child: Text('+'),
|
||||
),
|
||||
),
|
||||
));
|
||||
expect(find.bySemanticsIdentifier('Add'), findsOneWidget);
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('finds Semantically labeled widgets by identifier RegExp', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
// list of elements with a prefixed identifier
|
||||
await tester.pumpWidget(_boilerplate(
|
||||
Row(children: <Widget>[
|
||||
Semantics(
|
||||
identifier: 'item-1',
|
||||
child: const Text('Item 1'),
|
||||
),
|
||||
Semantics(
|
||||
identifier: 'item-2',
|
||||
child: const Text('Item 2'),
|
||||
),
|
||||
]),
|
||||
));
|
||||
expect(find.bySemanticsIdentifier('item'), findsNothing);
|
||||
expect(find.bySemanticsIdentifier(RegExp(r'^item-')), findsNWidgets(2));
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
group('byTooltip', () {
|
||||
|
Loading…
Reference in New Issue
Block a user