Unverified Commit b8fd21b0 authored by Lasse R.H. Nielsen's avatar Lasse R.H. Nielsen Committed by GitHub

Adds `CommonFinders.bySubtype<T extends Widget>()` finder. (#91415)

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