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.
|
/// 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
|
/// 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`.
|
/// On Android, this is used for `AccessibilityNodeInfo.setViewIdResourceName`.
|
||||||
/// It'll be appear in accessibility hierarchy as `resource-id`.
|
/// It'll be appear in accessibility hierarchy as `resource-id`.
|
||||||
|
@ -470,8 +470,8 @@ class CommonFinders {
|
|||||||
|
|
||||||
/// Finds a standard "back" button.
|
/// Finds a standard "back" button.
|
||||||
///
|
///
|
||||||
/// A common element on many user interfaces is the "back" button. This is the
|
/// A common element on many user interfaces is the "back" button. This is
|
||||||
/// button which takes the user back to the previous page/screen/state.
|
/// 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
|
/// 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
|
/// 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
|
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
/// 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) {
|
if (!SemanticsBinding.instance.semanticsEnabled) {
|
||||||
throw StateError('Semantics are not enabled. '
|
throw StateError(
|
||||||
'Make sure to call tester.ensureSemantics() before using '
|
'Semantics are not enabled. '
|
||||||
'this finder, and call dispose on its return value after.');
|
'Make sure to call tester.ensureSemantics() before using '
|
||||||
|
'this finder, and call dispose on its return value after.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return byElementPredicate(
|
return byElementPredicate(
|
||||||
(Element element) {
|
(Element element) {
|
||||||
@ -568,13 +606,13 @@ class CommonFinders {
|
|||||||
if (element is! RenderObjectElement) {
|
if (element is! RenderObjectElement) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final String? semanticsLabel = element.renderObject.debugSemantics?.label;
|
final String? propertyValue = propertyGetter(element.renderObject.debugSemantics);
|
||||||
if (semanticsLabel == null) {
|
if (propertyValue == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return label is RegExp
|
return pattern is RegExp
|
||||||
? label.hasMatch(semanticsLabel)
|
? pattern.hasMatch(propertyValue)
|
||||||
: label == semanticsLabel;
|
: pattern == propertyValue;
|
||||||
},
|
},
|
||||||
skipOffstage: skipOffstage,
|
skipOffstage: skipOffstage,
|
||||||
);
|
);
|
||||||
|
@ -299,6 +299,55 @@ void main() {
|
|||||||
expect(find.bySemanticsLabel('Foo'), findsOneWidget);
|
expect(find.bySemanticsLabel('Foo'), findsOneWidget);
|
||||||
semanticsHandle.dispose();
|
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', () {
|
group('byTooltip', () {
|
||||||
|
Loading…
Reference in New Issue
Block a user