// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; const List<Widget> fooBarTexts = <Text>[ Text('foo', textDirection: TextDirection.ltr), Text('bar', textDirection: TextDirection.ltr), ]; void main() { group('image', () { testWidgets('finds Image widgets', (WidgetTester tester) async { 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); }); }); group('text', () { testWidgets('finds Text widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Text('test'), )); expect(find.text('test'), findsOneWidget); }); testWidgets('finds Text.rich widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 't', children: <TextSpan>[ TextSpan(text: 'e'), TextSpan(text: 'st'), ], ), ))); 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>[ 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>[ TextSpan(text: 'est'), ], ), ))); expect(find.text('test'), 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>[ 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>[ TextSpan(text: 'est'), TextSpan(text: '3'), ], ), ))); expect(find.text('test3'), findsOneWidget); }); }); }); group('textContaining', () { testWidgets('finds Text widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Text('this is a test'), )); expect(find.textContaining(RegExp(r'test')), findsOneWidget); expect(find.textContaining('test'), findsOneWidget); expect(find.textContaining('a'), findsOneWidget); expect(find.textContaining('s'), findsOneWidget); }); testWidgets('finds Text.rich widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 'this', children: <TextSpan>[ TextSpan(text: 'is'), TextSpan(text: 'a'), TextSpan(text: 'test'), ], ), ))); expect(find.textContaining(RegExp(r'isatest')), findsOneWidget); expect(find.textContaining('isatest'), findsOneWidget); }); testWidgets('finds EditableText widgets', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: _boilerplate(TextField( controller: TextEditingController()..text = 'this is test', )), ), )); expect(find.textContaining(RegExp(r'test')), findsOneWidget); expect(find.textContaining('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>[ TextSpan(text: 'est'), ], ), ))); expect(find.textContaining('te', findRichText: true), findsOneWidget); }); testWidgets('finds Text widgets once when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text('test2'))); expect(find.textContaining('tes', 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>[ TextSpan(text: 'est'), ], ), ))); expect(find.textContaining('te'), 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.textContaining('tes'), findsOneWidget); }); testWidgets('finds Text.rich widgets when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 't', children: <TextSpan>[ TextSpan(text: 'est'), TextSpan(text: '3'), ], ), ))); expect(find.textContaining('t3', 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>[ TextSpan(text: 'est'), TextSpan(text: '3'), ], ), ))); expect(find.textContaining('t3'), findsOneWidget); }); }); }); group('semantics', () { 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 { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget(_boilerplate( Semantics( label: 'Add', button: true, child: const TextButton( onPressed: null, child: Text('+'), ), ), )); expect(find.bySemanticsLabel('Add'), findsOneWidget); semanticsHandle.dispose(); }); testWidgets('finds Semantically labeled widgets by RegExp', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget(_boilerplate( Semantics( container: true, child: const Row(children: <Widget>[ Text('Hello'), Text('World'), ]), ), )); expect(find.bySemanticsLabel('Hello'), findsNothing); expect(find.bySemanticsLabel(RegExp(r'^Hello')), findsOneWidget); semanticsHandle.dispose(); }); testWidgets('finds Semantically labeled widgets without explicit Semantics', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); 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 { await tester.pumpWidget( _boilerplate(IndexedStack( sizing: StackFit.expand, children: <Widget>[ GestureDetector( key: const ValueKey<int>(0), behavior: HitTestBehavior.opaque, onTap: () {}, child: const SizedBox.expand(), ), GestureDetector( key: const ValueKey<int>(1), behavior: HitTestBehavior.opaque, onTap: () {}, child: const SizedBox.expand(), ), ], )), ); expect(find.byType(GestureDetector), findsOneWidget); expect(find.byType(GestureDetector, skipOffstage: false), findsNWidgets(2)); final Finder hitTestable = find.byType(GestureDetector, skipOffstage: false).hitTestable(); expect(hitTestable, findsOneWidget); expect(tester.widget(hitTestable).key, const ValueKey<int>(0)); }); }); testWidgets('ChainedFinders chain properly', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); await tester.pumpWidget( _boilerplate(Column( children: <Widget>[ Container( key: key1, child: const Text('1'), ), const Text('2'), ], )), ); // Get the text back. By correctly chaining the descendant finder's // 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; expect(text.data, '1'); }); testWidgets('finds multiple subtypes', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( Row(children: <Widget>[ const Column(children: <Widget>[ Text('Hello'), Text('World'), ]), Column(children: <Widget>[ Image(image: FileImage(File('test'))), ]), const Column(children: <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)); }); group('find.byElementPredicate', () { testWidgets('fails with a custom description in the message', (WidgetTester tester) async { await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); const String customDescription = 'custom description'; late TestFailure failure; try { expect(find.byElementPredicate((_) => false, description: customDescription), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect(failure.message, contains('Actual: _ElementPredicateWidgetFinder:<Found 0 widgets with $customDescription')); }); }); group('find.byWidgetPredicate', () { testWidgets('fails with a custom description in the message', (WidgetTester tester) async { await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); const String customDescription = 'custom description'; late TestFailure failure; try { expect(find.byWidgetPredicate((_) => false, description: customDescription), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect(failure.message, contains('Actual: _WidgetPredicateWidgetFinder:<Found 0 widgets with $customDescription')); }); }); group('find.descendant', () { testWidgets('finds one descendant', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: <Widget>[ Column(children: fooBarTexts), ], )); expect(find.descendant( of: find.widgetWithText(Row, 'foo'), matching: find.text('bar'), ), findsOneWidget); }); testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: <Widget>[ Column(children: fooBarTexts), Column(children: fooBarTexts), ], )); expect(find.descendant( of: find.widgetWithText(Column, 'foo'), matching: find.text('bar'), ), findsNWidgets(2)); }); testWidgets('fails with a descriptive message', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: <Widget>[ Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), Text('bar', textDirection: TextDirection.ltr), ], )); late TestFailure failure; try { expect(find.descendant( of: find.widgetWithText(Column, 'foo'), matching: find.text('bar'), ), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect( failure.message, contains( 'Actual: _DescendantWidgetFinder:<Found 0 widgets with text "bar" descending from widgets with type "Column" that are ancestors of widgets with text "foo"', ), ); }); }); group('find.ancestor', () { testWidgets('finds one ancestor', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: <Widget>[ Column(children: fooBarTexts), ], )); expect(find.ancestor( of: find.text('bar'), matching: find.widgetWithText(Row, 'foo'), ), findsOneWidget); }); testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Row( children: <Widget>[ Row(children: fooBarTexts), ], ), ), ); expect(find.ancestor( of: find.text('bar'), matching: find.byType(Row), ), findsNWidgets(2)); }); testWidgets('fails with a descriptive message', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: <Widget>[ Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), Text('bar', textDirection: TextDirection.ltr), ], )); late TestFailure failure; try { expect(find.ancestor( of: find.text('bar'), matching: find.widgetWithText(Column, 'foo'), ), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect( failure.message, contains( 'Actual: _AncestorWidgetFinder:<Found 0 widgets with type "Column" that are ancestors of widgets with text "foo" that are ancestors of widgets with text "bar"', ), ); }); testWidgets('Root not matched by default', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: <Widget>[ Column(children: fooBarTexts), ], )); expect(find.ancestor( of: find.byType(Column), matching: find.widgetWithText(Column, 'foo'), ), findsNothing); }); testWidgets('Match the root', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: <Widget>[ Column(children: fooBarTexts), ], )); expect(find.descendant( of: find.byType(Column), matching: find.widgetWithText(Column, 'foo'), matchRoot: true, ), findsOneWidget); }); testWidgets('is fast in deep tree', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: _deepWidgetTree( depth: 1000, child: Row( children: <Widget>[ _deepWidgetTree( depth: 1000, child: const Column(children: fooBarTexts), ), ], ), ), ), ); expect(find.ancestor( of: find.text('bar'), matching: find.byType(Row), ), findsOneWidget); }); }); group('CommonSemanticsFinders', () { final Widget semanticsTree = _boilerplate( Semantics( container: true, header: true, readOnly: true, onCopy: () {}, onLongPress: () {}, value: 'value1', hint: 'hint1', label: 'label1', child: Semantics( container: true, textField: true, onSetText: (_) { }, onPaste: () { }, onLongPress: () { }, value: 'value2', hint: 'hint2', label: 'label2', child: Semantics( container: true, readOnly: true, onCopy: () {}, value: 'value3', hint: 'hint3', label: 'label3', child: Semantics( container: true, readOnly: true, onLongPress: () { }, value: 'value4', hint: 'hint4', label: 'label4', child: Semantics( container: true, onLongPress: () { }, onCopy: () {}, value: 'value5', hint: 'hint5', label: 'label5' ), ), ) ), ), ); group('ancestor', () { testWidgets('finds matching ancestor nodes', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final FinderBase<SemanticsNode> finder = find.semantics.ancestor( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); expect(finder, findsExactly(2)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final FinderBase<SemanticsNode> finder = find.semantics.ancestor( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); try { expect(finder, findsExactly(3)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _AncestorSemanticsFinder:<Found 2 SemanticsNodes with action "SemanticsAction.copy" that are ancestors of SemanticsNodes with label "label4"')); }); }); group('descendant', () { testWidgets('finds matching descendant nodes', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final FinderBase<SemanticsNode> finder = find.semantics.descendant( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); expect(finder, findsOne); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final FinderBase<SemanticsNode> finder = find.semantics.descendant( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _DescendantSemanticsFinder:<Found 1 SemanticsNode with action "SemanticsAction.copy" descending from SemanticsNode with label "label4"')); }); }); group('byPredicate', () { testWidgets('finds nodes matching given predicate', (WidgetTester tester) async { final RegExp replaceRegExp = RegExp(r'^[^\d]+'); await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byPredicate( (SemanticsNode node) { final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; return labelNum > 1; }, ); expect(finder, findsExactly(4)); }); testWidgets('fails with default message', (WidgetTester tester) async { late TestFailure failure; final RegExp replaceRegExp = RegExp(r'^[^\d]+'); await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byPredicate( (SemanticsNode node) { final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; return labelNum > 1; }, ); try { expect(finder, findsExactly(5)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 4 matching semantics predicate')); }); testWidgets('fails with given message', (WidgetTester tester) async { late TestFailure failure; const String expected = 'custom error message'; final RegExp replaceRegExp = RegExp(r'^[^\d]+'); await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byPredicate( (SemanticsNode node) { final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; return labelNum > 1; }, describeMatch: (_) => expected, ); try { expect(finder, findsExactly(5)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains(expected)); }); }); group('byLabel', () { testWidgets('finds nodes with matching label using String', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byLabel('label3'); expect(finder, findsOne); expect(finder.found.first.label, 'label3'); }); testWidgets('finds nodes with matching label using RegEx', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byLabel(RegExp('^label.*')); expect(finder, findsExactly(5)); expect(finder.found.every((SemanticsNode node) => node.label.startsWith('label')), isTrue); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byLabel('label3'); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with label "label3"')); }); }); group('byValue', () { testWidgets('finds nodes with matching value using String', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byValue('value3'); expect(finder, findsOne); expect(finder.found.first.value, 'value3'); }); testWidgets('finds nodes with matching value using RegEx', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byValue(RegExp('^value.*')); expect(finder, findsExactly(5)); expect(finder.found.every((SemanticsNode node) => node.value.startsWith('value')), isTrue); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byValue('value3'); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with value "value3"')); }); }); group('byHint', () { testWidgets('finds nodes with matching hint using String', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byHint('hint3'); expect(finder, findsOne); expect(finder.found.first.hint, 'hint3'); }); testWidgets('finds nodes with matching hint using RegEx', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byHint(RegExp('^hint.*')); expect(finder, findsExactly(5)); expect(finder.found.every((SemanticsNode node) => node.hint.startsWith('hint')), isTrue); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byHint('hint3'); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with hint "hint3"')); }); }); group('byAction', () { testWidgets('finds nodes with matching action', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy); expect(finder, findsExactly(3)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy); try { expect(finder, findsExactly(4)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 3 SemanticsNodes with action "SemanticsAction.copy"')); }); }); group('byAnyAction', () { testWidgets('finds nodes with any matching actions', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[ SemanticsAction.paste, SemanticsAction.longPress, ]); expect(finder, findsExactly(4)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[ SemanticsAction.paste, SemanticsAction.longPress, ]); try { expect(finder, findsExactly(5)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 4 SemanticsNodes with any of the following actions: [SemanticsAction.paste, SemanticsAction.longPress]:')); }); }); group('byFlag', () { testWidgets('finds nodes with matching flag', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly); expect(finder, findsExactly(3)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly); try { expect(finder, findsExactly(4)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('_PredicateSemanticsFinder:<Found 3 SemanticsNodes with flag "SemanticsFlag.isReadOnly":')); }); }); group('byAnyFlag', () { testWidgets('finds nodes with any matching flag', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[ SemanticsFlag.isHeader, SemanticsFlag.isTextField, ]); expect(finder, findsExactly(2)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[ SemanticsFlag.isHeader, SemanticsFlag.isTextField, ]); try { expect(finder, findsExactly(3)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:<Found 2 SemanticsNodes with any of the following flags: [SemanticsFlag.isHeader, SemanticsFlag.isTextField]:')); }); }); }); group('FinderBase', () { group('describeMatch', () { test('is used for Finder and results', () { const String expected = 'Fake finder describe match'; final _FakeFinder finder = _FakeFinder(describeMatchCallback: (_) { return expected; }); expect(finder.evaluate().toString(), contains(expected)); expect(finder.toString(describeSelf: true), contains(expected)); }); for (int i = 0; i < 4; i++) { test('gets expected plurality for $i when reporting results from find', () { final Plurality expected = switch (i) { 0 => Plurality.zero, 1 => Plurality.one, _ => Plurality.many, }; late final Plurality actual; final _FakeFinder finder = _FakeFinder( describeMatchCallback: (Plurality plurality) { actual = plurality; return 'Fake description'; }, findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()), ); finder.evaluate().toString(); expect(actual, expected); }); test('gets expected plurality for $i when reporting results from toString', () { final Plurality expected = switch (i) { 0 => Plurality.zero, 1 => Plurality.one, _ => Plurality.many, }; late final Plurality actual; final _FakeFinder finder = _FakeFinder( describeMatchCallback: (Plurality plurality) { actual = plurality; return 'Fake description'; }, findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()), ); finder.toString(); expect(actual, expected); }); test('always gets many when describing finder', () { const Plurality expected = Plurality.many; late final Plurality actual; final _FakeFinder finder = _FakeFinder( describeMatchCallback: (Plurality plurality) { actual = plurality; return 'Fake description'; }, findInCandidatesCallback: (_) => Iterable<String>.generate(i, (int index) => index.toString()), ); finder.toString(describeSelf: true); expect(actual, expected); }); } }); test('findInCandidates gets allCandidates', () { final List<String> expected = <String>['Test1', 'Test2', 'Test3', 'Test4']; late final List<String> actual; final _FakeFinder finder = _FakeFinder( allCandidatesCallback: () => expected, findInCandidatesCallback: (Iterable<String> candidates) { actual = candidates.toList(); return candidates; }, ); finder.evaluate(); expect(actual, expected); }); test('allCandidates calculated for each find', () { const int expectedCallCount = 3; int actualCallCount = 0; final _FakeFinder finder = _FakeFinder( allCandidatesCallback: () { actualCallCount++; return <String>['test']; }, ); for (int i = 0; i < expectedCallCount; i++) { finder.evaluate(); } expect(actualCallCount, expectedCallCount); }); test('allCandidates only called once while caching', () { int actualCallCount = 0; final _FakeFinder finder = _FakeFinder( allCandidatesCallback: () { actualCallCount++; return <String>['test']; }, ); finder.runCached(() { for (int i = 0; i < 5; i++) { finder.evaluate(); finder.tryEvaluate(); final FinderResult<String> _ = finder.found; } }); expect(actualCallCount, 1); }); group('tryFind', () { test('returns false if no results', () { final _FakeFinder finder = _FakeFinder( findInCandidatesCallback: (_) => <String>[], ); expect(finder.tryEvaluate(), false); }); test('returns true if results are available', () { final _FakeFinder finder = _FakeFinder( findInCandidatesCallback: (_) => <String>['Results'], ); expect(finder.tryEvaluate(), true); }); }); group('found', () { test('throws before any calls to evaluate or tryEvaluate', () { final _FakeFinder finder = _FakeFinder(); expect(finder.hasFound, false); expect(() => finder.found, throwsAssertionError); }); test('has same results as evaluate after call to evaluate', () { final _FakeFinder finder = _FakeFinder(); final FinderResult<String> expected = finder.evaluate(); expect(finder.hasFound, true); expect(finder.found, expected); }); test('has expected results after call to tryFind', () { final Iterable<String> expected = Iterable<String>.generate(10, (int i) => i.toString()); final _FakeFinder finder = _FakeFinder(findInCandidatesCallback: (_) => expected); finder.tryEvaluate(); expect(finder.hasFound, true); expect(finder.found, orderedEquals(expected)); }); }); }); } Widget _boilerplate(Widget child) { return Directionality( textDirection: TextDirection.ltr, child: child, ); } class SimpleCustomSemanticsWidget extends LeafRenderObjectWidget { const SimpleCustomSemanticsWidget(this.label, {super.key}); final String label; @override RenderObject createRenderObject(BuildContext context) => SimpleCustomSemanticsRenderObject(label); } class SimpleCustomSemanticsRenderObject extends RenderBox { SimpleCustomSemanticsRenderObject(this.label); final String label; @override bool get sizedByParent => true; @override Size computeDryLayout(BoxConstraints constraints) { return constraints.smallest; } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); config ..label = label ..textDirection = TextDirection.ltr; } } class SimpleGenericWidget<T> extends StatelessWidget { const SimpleGenericWidget({required Widget child, super.key}) : _child = child; final Widget _child; @override Widget build(BuildContext context) { return _child; } } /// Wraps [child] in [depth] layers of [SizedBox] Widget _deepWidgetTree({required int depth, required Widget child}) { Widget tree = child; for (int i = 0; i < depth; i += 1) { tree = SizedBox(child: tree); } return tree; } class _FakeFinder extends FinderBase<String> { _FakeFinder({ this.allCandidatesCallback, this.describeMatchCallback, this.findInCandidatesCallback, }); final Iterable<String> Function()? allCandidatesCallback; final DescribeMatchCallback? describeMatchCallback; final Iterable<String> Function(Iterable<String> candidates)? findInCandidatesCallback; @override Iterable<String> get allCandidates { return allCandidatesCallback?.call() ?? <String>[ 'String 1', 'String 2', 'String 3', ]; } @override String describeMatch(Plurality plurality) { return describeMatchCallback?.call(plurality) ?? switch (plurality) { Plurality.one => 'String', Plurality.many || Plurality.zero => 'Strings', }; } @override Iterable<String> findInCandidates(Iterable<String> candidates) { return findInCandidatesCallback?.call(candidates) ?? candidates; } }