diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index 8d31e978666..b8a81c5d34c 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -23,8 +23,20 @@ const CommonFinders find = CommonFinders._(); class CommonFinders { const CommonFinders._(); - /// Finds [Text] and [EditableText] widgets containing string equal to the - /// `text` argument. + /// Finds [Text], [EditableText], and optionally [RichText] widgets + /// containing string equal to the `text` argument. + /// + /// If `findRichText` is false, all standalone [RichText] widgets are + /// ignored and `text` is matched with [Text.data] or [Text.textSpan]. + /// If `findRichText` is true, [RichText] widgets (and therefore also + /// [Text] and [Text.rich] widgets) are matched by comparing the + /// [InlineSpan.toPlainText] with the given `text`. + /// + /// For [EditableText] widgets, the `text` is always compared to the current + /// value of the [EditableText.controller]. + /// + /// If the `skipOffstage` argument is true (the default), then this skips + /// nodes that are [Offstage] or that are from inactive [Route]s. /// /// ## Sample code /// @@ -32,9 +44,26 @@ class CommonFinders { /// expect(find.text('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 text(String text, { bool skipOffstage = true }) => _TextFinder(text, skipOffstage: skipOffstage); + /// This will match [Text], [Text.rich], and [EditableText] widgets that + /// contain the "Back" string. + /// + /// ```dart + /// expect(find.text('Close', findRichText: true), findsOneWidget); + /// ``` + /// + /// This will match [Text], [Text.rich], [EditableText], as well as standalone + /// [RichText] widgets that contain the "Close" string. + Finder text( + String text, { + bool findRichText = false, + bool skipOffstage = true, + }) { + return _TextFinder( + text, + findRichText: findRichText, + skipOffstage: skipOffstage, + ); + } /// Finds [Text] and [EditableText] widgets which contain the given /// `pattern` argument. @@ -548,26 +577,65 @@ abstract class MatchFinder extends Finder { } class _TextFinder extends MatchFinder { - _TextFinder(this.text, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage); + _TextFinder( + this.text, { + this.findRichText = false, + bool skipOffstage = true, + }) : super(skipOffstage: skipOffstage); final String text; + /// Whether standalone [RichText] widgets should be found or not. + /// + /// Defaults to `false`. + /// + /// If disabled, only [Text] widgets will be matched. [RichText] widgets + /// *without* a [Text] ancestor will be ignored. + /// If enabled, only [RichText] widgets will be matched. This *implicitly* + /// matches [Text] widgets as well since they always insert a [RichText] + /// child. + /// + /// In either case, [EditableText] widgets will also be matched. + final bool findRichText; + @override String get description => 'text "$text"'; @override bool matches(Element candidate) { final Widget widget = candidate.widget; + if (widget is EditableText) + return _matchesEditableText(widget); + + if (!findRichText) + return _matchesNonRichText(widget); + // It would be sufficient to always use _matchesRichText if we wanted to + // match both standalone RichText widgets as well as Text widgets. However, + // the find.text() finder used to always ignore standalone RichText widgets, + // which is why we need the _matchesNonRichText method in order to not be + // backwards-compatible and not break existing tests. + return _matchesRichText(widget); + } + + bool _matchesRichText(Widget widget) { + if (widget is RichText) + return widget.text.toPlainText() == text; + return false; + } + + bool _matchesNonRichText(Widget widget) { if (widget is Text) { if (widget.data != null) return widget.data == text; assert(widget.textSpan != null); return widget.textSpan!.toPlainText() == text; - } else if (widget is EditableText) { - return widget.controller.text == text; } return false; } + + bool _matchesEditableText(EditableText widget) { + return widget.controller.text == text; + } } class _TextContainingFinder extends MatchFinder { diff --git a/packages/flutter_test/test/finders_test.dart b/packages/flutter_test/test/finders_test.dart index 657ee2b1b84..6c07bd09eaa 100644 --- a/packages/flutter_test/test/finders_test.dart +++ b/packages/flutter_test/test/finders_test.dart @@ -27,6 +27,84 @@ void main() { expect(find.text('test'), findsOneWidget); }); + + group('findRichText', () { + testWidgets('finds RichText widgets when enabled', + (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(RichText( + text: const TextSpan( + text: 't', + children: [ + TextSpan(text: 'est'), + ], + ), + ))); + + expect(find.text('test', findRichText: true), findsOneWidget); + }); + + testWidgets('finds Text widgets once when enabled', + (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const Text('test2'))); + + expect(find.text('test2', findRichText: true), findsOneWidget); + }); + + testWidgets('does not find RichText widgets when disabled', + (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(RichText( + text: const TextSpan( + text: 't', + children: [ + TextSpan(text: 'est'), + ], + ), + ))); + + expect(find.text('test', findRichText: false), findsNothing); + }); + + testWidgets( + 'does not find Text and RichText separated by semantics widgets twice', + (WidgetTester tester) async { + // If rich: true found both Text and RichText, this would find two widgets. + await tester.pumpWidget(_boilerplate( + const Text('test', semanticsLabel: 'foo'), + )); + + expect(find.text('test'), findsOneWidget); + }); + + testWidgets('finds Text.rich widgets when enabled', + (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const Text.rich( + TextSpan( + text: 't', + children: [ + TextSpan(text: 'est'), + TextSpan(text: '3'), + ], + ), + ))); + + expect(find.text('test3', findRichText: true), findsOneWidget); + }); + + testWidgets('finds Text.rich widgets when disabled', + (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate(const Text.rich( + TextSpan( + text: 't', + children: [ + TextSpan(text: 'est'), + TextSpan(text: '3'), + ], + ), + ))); + + expect(find.text('test3', findRichText: false), findsOneWidget); + }); + }); }); group('textContaining', () {