diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index 6ec09ec9a28..63323b4058c 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -128,6 +128,24 @@ class CommonFinders { /// nodes that are [Offstage] or that are from inactive [Route]s. Finder byKey(Key key, { bool skipOffstage = true }) => _KeyFinder(key, skipOffstage: skipOffstage); + /// Finds widgets by searching for widgets implementing a particular type. + /// + /// This matcher accepts subtypes. For example a + /// `bySubtype()` will find any stateful widget. + /// + /// ## Sample code + /// + /// ```dart + /// expect(find.bySubtype(), findsOneWidget); + /// ``` + /// + /// If the `skipOffstage` argument is true (the default), then this skips + /// nodes that are [Offstage] or that are from inactive [Route]s. + /// + /// See also: + /// * [byType], which does not do subtype tests. + Finder bySubtype({ bool skipOffstage = true }) => _WidgetSubtypeFinder(skipOffstage: skipOffstage); + /// Finds widgets by searching for widgets with a particular type. /// /// This does not do subclass tests, so for example @@ -144,6 +162,9 @@ class CommonFinders { /// /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. + /// + /// See also: + /// * [bySubtype], which allows subtype tests. Finder byType(Type type, { bool skipOffstage = true }) => _WidgetTypeFinder(type, skipOffstage: skipOffstage); /// Finds [Icon] widgets containing icon data equal to the `icon` @@ -713,6 +734,18 @@ class _KeyFinder extends MatchFinder { } } +class _WidgetSubtypeFinder extends MatchFinder { + _WidgetSubtypeFinder({ bool skipOffstage = true }) : super(skipOffstage: skipOffstage); + + @override + String get description => 'is "$T"'; + + @override + bool matches(Element candidate) { + return candidate.widget is T; + } +} + class _WidgetTypeFinder extends MatchFinder { _WidgetTypeFinder(this.widgetType, { bool skipOffstage = true }) : super(skipOffstage: skipOffstage); diff --git a/packages/flutter_test/test/finders_test.dart b/packages/flutter_test/test/finders_test.dart index 1c4342b3d16..f810b198f26 100644 --- a/packages/flutter_test/test/finders_test.dart +++ b/packages/flutter_test/test/finders_test.dart @@ -11,17 +11,18 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('image', () { testWidgets('finds Image widgets', (WidgetTester tester) async { - await tester.pumpWidget(_boilerplate( - Image(image: FileImage(File('test'))) - )); + await tester + .pumpWidget(_boilerplate(Image(image: FileImage(File('test'))))); expect(find.image(FileImage(File('test'))), findsOneWidget); }); testWidgets('finds Button widgets with Image', (WidgetTester tester) async { - await tester.pumpWidget(_boilerplate( - ElevatedButton(onPressed: null, child: Image(image: FileImage(File('test'))),) - )); - expect(find.widgetWithImage(ElevatedButton, FileImage(File('test'))), findsOneWidget); + await tester.pumpWidget(_boilerplate(ElevatedButton( + onPressed: null, + child: Image(image: FileImage(File('test'))), + ))); + expect(find.widgetWithImage(ElevatedButton, FileImage(File('test'))), + findsOneWidget); }); }); @@ -34,9 +35,10 @@ void main() { }); testWidgets('finds Text.rich widgets', (WidgetTester tester) async { - await tester.pumpWidget(_boilerplate( - const Text.rich( - TextSpan(text: 't', children: [ + await tester.pumpWidget(_boilerplate(const Text.rich( + TextSpan( + text: 't', + children: [ TextSpan(text: 'e'), TextSpan(text: 'st'), ], @@ -137,15 +139,16 @@ void main() { }); testWidgets('finds Text.rich widgets', (WidgetTester tester) async { - await tester.pumpWidget(_boilerplate( - const Text.rich( - TextSpan(text: 'this', children: [ - TextSpan(text: 'is'), - TextSpan(text: 'a'), - TextSpan(text: 'test'), - ], - ), - ))); + await tester.pumpWidget(_boilerplate(const Text.rich( + TextSpan( + text: 'this', + children: [ + TextSpan(text: 'is'), + TextSpan(text: 'a'), + TextSpan(text: 'test'), + ], + ), + ))); expect(find.textContaining(RegExp(r'isatest')), findsOneWidget); expect(find.textContaining('isatest'), findsOneWidget); @@ -166,11 +169,13 @@ void main() { }); group('semantics', () { - testWidgets('Throws StateError if semantics are not enabled', (WidgetTester tester) async { + testWidgets('Throws StateError if semantics are not enabled', + (WidgetTester tester) async { expect(() => find.bySemanticsLabel('Add'), throwsStateError); }, semanticsEnabled: false); - testWidgets('finds Semantically labeled widgets', (WidgetTester tester) async { + testWidgets('finds Semantically labeled widgets', + (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget(_boilerplate( Semantics( @@ -186,7 +191,8 @@ void main() { semanticsHandle.dispose(); }); - testWidgets('finds Semantically labeled widgets by RegExp', (WidgetTester tester) async { + testWidgets('finds Semantically labeled widgets by RegExp', + (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget(_boilerplate( Semantics( @@ -202,18 +208,19 @@ void main() { semanticsHandle.dispose(); }); - testWidgets('finds Semantically labeled widgets without explicit Semantics', (WidgetTester tester) async { + testWidgets('finds Semantically labeled widgets without explicit Semantics', + (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); - await tester.pumpWidget(_boilerplate( - const SimpleCustomSemanticsWidget('Foo') - )); + await tester + .pumpWidget(_boilerplate(const SimpleCustomSemanticsWidget('Foo'))); expect(find.bySemanticsLabel('Foo'), findsOneWidget); semanticsHandle.dispose(); }); }); group('hitTestable', () { - testWidgets('excludes non-hit-testable widgets', (WidgetTester tester) async { + testWidgets('excludes non-hit-testable widgets', + (WidgetTester tester) async { await tester.pumpWidget( _boilerplate(IndexedStack( sizing: StackFit.expand, @@ -221,13 +228,13 @@ void main() { GestureDetector( key: const ValueKey(0), behavior: HitTestBehavior.opaque, - onTap: () { }, + onTap: () {}, child: const SizedBox.expand(), ), GestureDetector( key: const ValueKey(1), behavior: HitTestBehavior.opaque, - onTap: () { }, + onTap: () {}, child: const SizedBox.expand(), ), ], @@ -258,13 +265,52 @@ void main() { // candidates, it should find 1 instead of 2. If the _LastFinder wasn't // correctly chained after the descendant's candidates, the last element // with a Text widget would have been 2. - final Text text = find.descendant( - of: find.byKey(key1), - matching: find.byType(Text), - ).last.evaluate().single.widget as Text; + final Text text = find + .descendant( + of: find.byKey(key1), + matching: find.byType(Text), + ) + .last + .evaluate() + .single + .widget as Text; expect(text.data, '1'); }); + + testWidgets('finds multiple subtypes', (WidgetTester tester) async { + await tester.pumpWidget(_boilerplate( + Row(children: [ + Column(children: const [ + Text('Hello'), + Text('World'), + ]), + Column(children: [ + Image(image: FileImage(File('test'))), + ]), + Column(children: const [ + SimpleGenericWidget(child: Text('one')), + SimpleGenericWidget(child: Text('pi')), + SimpleGenericWidget(child: Text('two')), + ]), + ]), + )); + + expect(find.bySubtype(), findsOneWidget); + expect(find.bySubtype(), findsNWidgets(3)); + // Finds both rows and columns. + expect(find.bySubtype(), findsNWidgets(4)); + + // Finds only the requested generic subtypes. + expect(find.bySubtype>(), findsOneWidget); + expect(find.bySubtype>(), findsNWidgets(2)); + expect(find.bySubtype>(), findsNWidgets(3)); + + // Finds all widgets. + final int totalWidgetCount = + find.byWidgetPredicate((_) => true).evaluate().length; + expect(find.bySubtype(), findsNWidgets(totalWidgetCount)); + }); } Widget _boilerplate(Widget child) { @@ -280,7 +326,8 @@ class SimpleCustomSemanticsWidget extends LeafRenderObjectWidget { final String label; @override - RenderObject createRenderObject(BuildContext context) => SimpleCustomSemanticsRenderObject(label); + RenderObject createRenderObject(BuildContext context) => + SimpleCustomSemanticsRenderObject(label); } class SimpleCustomSemanticsRenderObject extends RenderBox { @@ -299,6 +346,21 @@ class SimpleCustomSemanticsRenderObject extends RenderBox { @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); - config..label = label..textDirection = TextDirection.ltr; + config + ..label = label + ..textDirection = TextDirection.ltr; + } +} + +class SimpleGenericWidget extends StatelessWidget { + const SimpleGenericWidget({required Widget child, Key? key}) + : _child = child, + super(key: key); + + final Widget _child; + + @override + Widget build(BuildContext context) { + return _child; } }