Skip to content

Commit 37d5dc4

Browse files
authored
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)); ```
1 parent 138144b commit 37d5dc4

File tree

3 files changed

+100
-12
lines changed

3 files changed

+100
-12
lines changed

packages/flutter/lib/src/semantics/semantics.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1213,7 +1213,8 @@ class SemanticsProperties extends DiagnosticableTree {
12131213
/// This value is not exposed to the users of the app.
12141214
///
12151215
/// It's usually used for UI testing with tools that work by querying the
1216-
/// native accessibility, like UIAutomator, XCUITest, or Appium.
1216+
/// native accessibility, like UIAutomator, XCUITest, or Appium. It can be
1217+
/// matched with [CommonFinders.bySemanticsIdentifier].
12171218
///
12181219
/// On Android, this is used for `AccessibilityNodeInfo.setViewIdResourceName`.
12191220
/// It'll be appear in accessibility hierarchy as `resource-id`.

packages/flutter_test/lib/src/finders.dart

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,8 @@ class CommonFinders {
470470

471471
/// Finds a standard "back" button.
472472
///
473-
/// A common element on many user interfaces is the "back" button. This is the
474-
/// button which takes the user back to the previous page/screen/state.
473+
/// A common element on many user interfaces is the "back" button. This is
474+
/// the button which takes the user back to the previous page/screen/state.
475475
///
476476
/// It is useful in tests to be able to find these buttons, both for tapping
477477
/// them or verifying their existence, but because different platforms and
@@ -555,11 +555,49 @@ class CommonFinders {
555555
///
556556
/// If the `skipOffstage` argument is true (the default), then this skips
557557
/// nodes that are [Offstage] or that are from inactive [Route]s.
558-
Finder bySemanticsLabel(Pattern label, { bool skipOffstage = true }) {
558+
Finder bySemanticsLabel(Pattern label, {bool skipOffstage = true}) {
559+
return _bySemanticsProperty(
560+
label,
561+
(SemanticsNode? semantics) => semantics?.label,
562+
skipOffstage: skipOffstage,
563+
);
564+
}
565+
566+
/// Finds [Semantics] widgets matching the given `identifier`, either by
567+
/// [RegExp.hasMatch] or string equality.
568+
///
569+
/// This allows matching against the identifier of a [Semantics] widget, which
570+
/// is a unique identifier for the widget in the semantics tree. This is
571+
/// exposed to offer a unified way widget tests and e2e tests can match
572+
/// against a [Semantics] widget.
573+
///
574+
/// ## Sample code
575+
///
576+
/// ```dart
577+
/// expect(find.bySemanticsIdentifier('Back'), findsOneWidget);
578+
/// ```
579+
///
580+
/// If the `skipOffstage` argument is true (the default), then this skips
581+
/// nodes that are [Offstage] or that are from inactive [Route]s.
582+
Finder bySemanticsIdentifier(Pattern identifier, {bool skipOffstage = true}) {
583+
return _bySemanticsProperty(
584+
identifier,
585+
(SemanticsNode? semantics) => semantics?.identifier,
586+
skipOffstage: skipOffstage,
587+
);
588+
}
589+
590+
Finder _bySemanticsProperty(
591+
Pattern pattern,
592+
String? Function(SemanticsNode?) propertyGetter,
593+
{bool skipOffstage = true}
594+
) {
559595
if (!SemanticsBinding.instance.semanticsEnabled) {
560-
throw StateError('Semantics are not enabled. '
561-
'Make sure to call tester.ensureSemantics() before using '
562-
'this finder, and call dispose on its return value after.');
596+
throw StateError(
597+
'Semantics are not enabled. '
598+
'Make sure to call tester.ensureSemantics() before using '
599+
'this finder, and call dispose on its return value after.',
600+
);
563601
}
564602
return byElementPredicate(
565603
(Element element) {
@@ -568,13 +606,13 @@ class CommonFinders {
568606
if (element is! RenderObjectElement) {
569607
return false;
570608
}
571-
final String? semanticsLabel = element.renderObject.debugSemantics?.label;
572-
if (semanticsLabel == null) {
609+
final String? propertyValue = propertyGetter(element.renderObject.debugSemantics);
610+
if (propertyValue == null) {
573611
return false;
574612
}
575-
return label is RegExp
576-
? label.hasMatch(semanticsLabel)
577-
: label == semanticsLabel;
613+
return pattern is RegExp
614+
? pattern.hasMatch(propertyValue)
615+
: pattern == propertyValue;
578616
},
579617
skipOffstage: skipOffstage,
580618
);

packages/flutter_test/test/finders_test.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,55 @@ void main() {
299299
expect(find.bySemanticsLabel('Foo'), findsOneWidget);
300300
semanticsHandle.dispose();
301301
});
302+
303+
testWidgets('Throws StateError if semantics are not enabled (bySemanticsIdentifier)', (WidgetTester tester) async {
304+
expect(
305+
() => find.bySemanticsIdentifier('Add'),
306+
throwsA(
307+
isA<StateError>().having(
308+
(StateError e) => e.message,
309+
'message',
310+
contains('Semantics are not enabled'),
311+
),
312+
),
313+
);
314+
}, semanticsEnabled: false);
315+
316+
testWidgets('finds Semantically labeled widgets by identifier', (WidgetTester tester) async {
317+
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
318+
await tester.pumpWidget(_boilerplate(
319+
Semantics(
320+
identifier: 'Add',
321+
button: true,
322+
child: const TextButton(
323+
onPressed: null,
324+
child: Text('+'),
325+
),
326+
),
327+
));
328+
expect(find.bySemanticsIdentifier('Add'), findsOneWidget);
329+
semanticsHandle.dispose();
330+
});
331+
332+
testWidgets('finds Semantically labeled widgets by identifier RegExp', (WidgetTester tester) async {
333+
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
334+
// list of elements with a prefixed identifier
335+
await tester.pumpWidget(_boilerplate(
336+
Row(children: <Widget>[
337+
Semantics(
338+
identifier: 'item-1',
339+
child: const Text('Item 1'),
340+
),
341+
Semantics(
342+
identifier: 'item-2',
343+
child: const Text('Item 2'),
344+
),
345+
]),
346+
));
347+
expect(find.bySemanticsIdentifier('item'), findsNothing);
348+
expect(find.bySemanticsIdentifier(RegExp(r'^item-')), findsNWidgets(2));
349+
semanticsHandle.dispose();
350+
});
302351
});
303352

304353
group('byTooltip', () {

0 commit comments

Comments
 (0)